UNPKG

@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
"use strict"; 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;