@prestodb/presto-js-client
Version:
This is a Presto JavaScript client that connects to Presto via Presto's REST API to run queries.
328 lines (327 loc) • 15.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: all[name]
});
}
_export(exports, {
PrestoClient: function() {
return PrestoClient;
},
default: function() {
return _default;
}
});
const _constants = require("./constants");
const _types = require("./types");
const _utils = require("./utils");
function _extends() {
_extends = Object.assign || function(target) {
for(var i = 1; i < arguments.length; i++){
var source = arguments[i];
for(var key in source){
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
let PrestoClient = class PrestoClient {
/**
* Retrieves all catalogs.
* @returns {Promise<string[] | undefined>} An array of all the catalog names.
*/ async getCatalogs() {
var _data;
return (_data = (await this.query('SHOW CATALOGS')).data) == null ? void 0 : _data.map(([catalog])=>catalog);
}
/**
* Retrieves a list of columns filtered for the given catalog and optional filters.
* @param {Object} options - The options for retrieving columns.
* @param {string} options.catalog - The catalog name.
* @param {string} [options.schema] - The schema name. Optional.
* @param {string} [options.table] - The table name. Optional.
* @returns {Promise<Column[] | undefined>} An array of all the columns that match the given filters.
*/ async getColumns({ catalog, schema, table }) {
const whereCondition = this.getWhereCondition([
{
key: 'table_schema',
value: schema
},
{
key: 'table_name',
value: table
}
]);
// The order of the select expression columns is important since we destructure them in the same order below
const query = `SELECT table_catalog, table_schema, table_name, column_name, column_default, is_nullable, data_type, comment, extra_info FROM information_schema.columns ${whereCondition}`;
const rawResult = (await this.query(query, {
catalog
})).data;
return rawResult == null ? void 0 : rawResult.map(([// This destructuring names the fields properly for each row, and converts them to camelCase
tableCatalog, tableSchema, tableName, columnName, columnDefault, isNullable, dataType, comment, extraInfo])=>({
tableCatalog,
tableSchema,
tableName,
columnName,
columnDefault,
isNullable,
dataType,
comment,
extraInfo
}));
}
/**
* Retrieves all the information for a given query
* @param {string} queryId The query identifier string
* @returns {Promise<QueryInfo | undefined>} All the query information
*/ async getQueryInfo(queryId) {
const queryInfoResponse = await this.request({
headers: this.headers,
method: 'GET',
url: `${this.baseUrl}${_constants.QUERY_INFO_URL}${queryId}`
});
if (queryInfoResponse.status !== 200) {
throw new Error(`Query failed: ${JSON.stringify(await queryInfoResponse.text())}`);
}
return await queryInfoResponse.json();
}
/**
* Retrieves all schemas within a given catalog.
* @param {string} catalog - The name of the catalog for which to retrieve schemas.
* @returns {Promise<string[] | undefined>} An array of schema names within the specified catalog.
*/ async getSchemas(catalog) {
var _data;
return (_data = (await this.query('SHOW SCHEMAS', {
catalog
})).data) == null ? void 0 : _data.map(([schema])=>schema);
}
/**
* Retrieves a list of tables filtered by the given catalog and optional schema.
* @param {Object} options - The options for retrieving tables.
* @param {string} options.catalog - The catalog name.
* @param {string} [options.schema] - The schema name. Optional.
* @returns {Promise<Table[] | undefined>} An array of tables that match the given filters.
*/ async getTables({ catalog, schema }) {
const whereCondition = this.getWhereCondition([
{
key: 'table_schema',
value: schema
}
]);
// The order of the select expression columns is important since we destructure them in the same order below
const query = `SELECT table_catalog, table_schema, table_name, table_type FROM information_schema.tables ${whereCondition}`;
const rawResult = (await this.query(query, {
catalog
})).data;
// This destructuring names the fields properly for each row, and converts them to camelCase
return rawResult == null ? void 0 : rawResult.map(([tableCatalog, tableSchema, tableName, tableType])=>({
tableCatalog,
tableSchema,
tableName,
tableType
}));
}
/**
* Generates a stream of query results in two parts: the query ID and the query result.
* @param {string} query - The SQL query string to be executed.
* @param {Object} [options] - Optional parameters for the query.
* @param {string} [options.catalog] - The catalog to be used for the query. Optional.
* @param {string} [options.schema] - The schema to be used for the query. Optional.
* @returns {AsyncGenerator<PrestoQuery>} A generator that yields the query ID and the query result.
*/ async *queryGenerator(query, options) {
const prestoResp = await this.queryFirst(query, options);
yield prestoResp.id;
yield await this.queryResult(prestoResp, options);
}
/**
* Retrieves the first query response from the Presto server.
* @param {string} query - The SQL query string to be executed.
* @param {Object} [options] - Optional parameters for the query.
* @param {string} [options.catalog] - The catalog to be used for the query. Optional.
* @param {string} [options.schema] - The schema to be used for the query. Optional.
* @returns {Promise<PrestoResponse>} A promise that resolves to the result of the query execution
* @throws {PrestoError} If the underlying Presto engine returns an error or a response is empty
*/ async queryFirst(query, options) {
const headers = this.getHeaders(options);
const firstResponse = await this.request({
body: query,
headers,
method: 'POST',
url: `${this.baseUrl}${_constants.STATEMENT_URL}`
});
if (firstResponse.status !== 200) {
throw new Error(`Query failed: ${JSON.stringify(await firstResponse.text())}`);
}
return await (firstResponse == null ? void 0 : firstResponse.json());
}
/**
* Retrieves the query result from the Presto server.
* @param {PrestoResponse} prestoResp - A Presto response object from making the initial request (queryFirst).
* @param {Object} [options] - Optional parameters for the query.
* @param {string} [options.catalog] - The catalog to be used for the query. Optional.
* @param {string} [options.schema] - The schema to be used for the query. Optional.
* @returns {Promise<PrestoQuery>} A promise that resolves to the result of the query execution
* @throws {PrestoError} If the underlying Presto engine returns an error or a response is empty
*/ async queryResult(prestoResp, options) {
const headers = this.getHeaders(options);
let nextUri = prestoResp.nextUri;
let queryId = undefined;
const columns = [];
const data = [];
if (!nextUri) {
throw new Error(`nextUri not provided in the presto response`);
}
do {
var _prestoResponse_columns, _prestoResponse_data;
const response = await this.request({
headers,
method: 'GET',
url: nextUri
});
// Server is overloaded, wait a bit
if (response.status === 503) {
await this.delay(this.retryInterval);
continue;
}
if (response.status !== 200) {
throw new Error(`Query failed: ${JSON.stringify(await response.text())}`);
}
const prestoResponse = await this.prestoConversionToJSON({
response
});
if (!prestoResponse) {
throw new Error(`Query failed with an empty response from the server.`);
}
if (prestoResponse.error) {
// Throw back the whole error object which contains all error information
throw new _types.PrestoError(prestoResponse.error);
}
nextUri = prestoResponse == null ? void 0 : prestoResponse.nextUri;
queryId = prestoResponse == null ? void 0 : prestoResponse.id;
const columnsAreEmpty = !columns.length;
if (columnsAreEmpty && ((_prestoResponse_columns = prestoResponse.columns) == null ? void 0 : _prestoResponse_columns.length)) {
columns.push(...prestoResponse.columns);
}
if ((_prestoResponse_data = prestoResponse.data) == null ? void 0 : _prestoResponse_data.length) {
data.push(...prestoResponse.data);
}
if (this.interval) {
await this.delay(this.interval);
}
}while (nextUri !== undefined)
return {
columns,
data,
queryId
};
}
/**
* Builds the headers for the request.
* @param {Object} options - Optional parameters for the query.
* @param {string} [options.catalog] - The catalog to be used for the query. Optional.
* @param {string} [options.schema] - The schema to be used for the query. Optional.
* @returns {Record<string, string>} The headers for the request.
*/ getHeaders(options) {
const catalog = (options == null ? void 0 : options.catalog) || this.catalog;
const schema = (options == null ? void 0 : options.schema) || this.schema;
const headers = _extends({}, this.headers);
if (catalog) {
headers['X-Presto-Catalog'] = catalog;
}
if (schema) {
headers['X-Presto-Schema'] = schema;
}
return headers;
}
/**
* Executes a given query with optional catalog and schema settings.
* @param {string} query - The SQL query string to be executed.
* @param {Object} [options] - Optional parameters for the query.
* @param {string} [options.catalog] - The catalog to be used for the query. Optional.
* @param {string} [options.schema] - The schema to be used for the query. Optional.
* @returns {Promise<PrestoQuery>} A promise that resolves to the result of the query execution.
* @throws {PrestoError} If the underlying Presto engine returns an error
*/ async query(query, options) {
const prestoResp = await this.queryFirst(query, options);
if (!(prestoResp == null ? void 0 : prestoResp.nextUri)) {
throw new Error(`Didn't receive the first nextUri`);
}
return await this.queryResult(prestoResp, options);
}
delay(milliseconds) {
return new Promise((resolve)=>setTimeout(resolve, milliseconds));
}
// This builds a WHERE statement if one or more of the conditions contain non-undefined values
// Currently only works for string values (need more conditions for number and boolean)
getWhereCondition(conditions) {
const filteredConditions = conditions.filter(({ value })=>Boolean(value));
if (filteredConditions.length) {
return `WHERE ${filteredConditions.map(({ key, value })=>`${key} = '${value}'`).join(' AND ')}`;
}
return '';
}
request({ body, headers, method, url }) {
return fetch(url, {
body,
headers,
method
});
}
async prestoConversionToJSON({ response }) {
const text = await response.text();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore JSON.parse with a 3 argument reviver is a stage 3 proposal with some support, allow it here.
return JSON.parse(text, _utils.parseWithBigInts);
}
/**
* Creates an instance of PrestoClient.
* @param {PrestoClientConfig} config - Configuration object for the PrestoClient.
* @param {Object} config.basicAuthorization - Optional object for basic authorization.
* @param {Object} config.basicAuthorization.user - The basic auth user name.
* @param {Object} config.basicAuthorization.password - The basic auth password.
* @param {string} config.authorizationToken - An optional token to be sent in the authorization header. Takes precedence over the basic auth.
* @param {string} config.catalog - The default catalog to be used.
* @param {Record<string, string>} config.extraHeaders - Any extra headers to include in the API requests. Optional.
* @param {string} config.host - The host address of the Presto server.
* @param {number} config.interval - The polling interval in milliseconds for query status checks.
* @param {number} config.port - The port number on which the Presto server is listening.
* @param {string} [config.schema] - The default schema to be used. Optional.
* @param {string} [config.source] - The name of the source making the query. Optional.
* @param {string} [config.timezone] - The timezone to be used for the session. Optional.
* @param {string} config.user - The username to be used for the Presto session.
*/ constructor({ basicAuthentication, authorizationToken, catalog, extraHeaders, host, interval, port, schema, source, timezone, user }){
this.baseUrl = `${host || 'http://localhost'}:${port || 8080}`;
this.catalog = catalog;
this.interval = interval;
this.schema = schema;
this.source = source;
this.timezone = timezone;
this.user = user;
this.retryInterval = 500;
this.headers = {
'X-Presto-Client-Info': 'presto-js-client',
'X-Presto-Source': this.source || 'presto-js-client'
};
if (this.user) {
this.headers['X-Presto-User'] = this.user;
}
if (this.timezone) {
this.headers['X-Presto-Time-Zone'] = this.timezone;
}
if (authorizationToken) {
this.headers['Authorization'] = `Bearer ${authorizationToken}`;
} else if (basicAuthentication) {
// Note this is only available for Node.js
this.headers['Authorization'] = `Basic ${Buffer.from(`${basicAuthentication.user}:${basicAuthentication.password}`).toString('base64')}`;
}
this.headers = _extends({}, extraHeaders, this.headers);
}
};
const _default = PrestoClient;