sdk.js

import axios from 'axios';
import dayjs from 'dayjs';
import isoWeek from 'dayjs/plugin/isoWeek';
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
dayjs.extend(isoWeek);
dayjs.extend(quarterOfYear);

let ObjUtils = {
    firstKey(obj) {
        let k = Object.keys(obj);
        return k.length ? k[0] : null;
    },
    replaceKey(obj, keyOld, keyNew) {
        obj[keyNew] = obj[keyOld];
        if (keyOld != keyNew) {
            delete obj[keyOld];
        }
        return obj;
    }
};

/**
 * @namespace
 */
let Dremio = {
    /**
     * @enum {String}
     */
    DATASET_TYPE: {
        /** @member {String} */
        VIRTUAL: 'VIRTUAL',
        /** @member {String} */
        PROMOTED: 'PROMOTED',
        /** @member {String} */
        DIRECT: 'DIRECT'
    },
    /**
     * @enum {String}
     */
    CONTAINER_TYPE: {
        /** @member {String} */
        SPACE: 'SPACE',
        /** @member {String} */
        SOURCE: 'SOURCE',
        /** @member {String} */
        FOLDER: 'FOLDER',
        /** @member {String} */
        HOME: 'HOME'
    },
    /**
     * @enum {String}
     */
    JOB_STATUS: {
        /** @member {String} */
        NOT_SUBMITTED: 'NOT_SUBMITTED',
        /** @member {String} */
        STARTING: 'STARTING',
        /** @member {String} */
        RUNNING: 'RUNNING',
        /** @member {String} */
        COMPLETED: 'COMPLETED',
        /** @member {String} */
        CANCELED: 'CANCELED',
        /** @member {String} */
        FAILED: 'FAILED',
        /** @member {String} */
        CANCELLATION_REQUESTED: 'CANCELLATION_REQUESTED',
        /** @member {String} */
        ENQUEUED: 'ENQUEUED'
    },
    /**
     * @enum {String}
     */
    SOURCE_TYPE: {
        /** @member {String} */
        AMAZONELASTIC: 'Amazon Elasticsearch Service',
        /** @member {String} */
        REDSHIFT: 'Amazon Redshift',
        /** @member {String} */
        S3: 'Amazon S3',
        /** @member {String} */
        ADLS: 'Azure Data Lake Storage Gen1',
        /** @member {String} */
        AZURE_STORAGE: 'Azure Storage',
        /** @member {String} */
        ELASTIC: 'Elasticsearch',
        /** @member {String} */
        HDFS: 'HDFS',
        /** @member {String} */
        HIVE: 'Hive',
        /** @member {String} */
        MAPRFS: 'MapR-FS',
        /** @member {String} */
        MSSQL: 'Microsoft SQL Server',
        /** @member {String} */
        MONGO: 'MongoDB',
        /** @member {String} */
        MYSQL: 'MySQL',
        /** @member {String} */
        NAS: 'NAS',
        /** @member {String} */
        ORACLE: 'Oracle',
        /** @member {String} */
        POSTGRES: 'PostgreSQL'
    }
};

/**
 * @class
 * @extends Error
 */
class SDKError extends Error {
    constructor(message) {
        super();
        this.name = 'SDKError';
        this.message = message;
    }
}

/**
 * @class
 * @extends Error
 */
class SDKValidationError extends SDKError {
    constructor(message) {
        super(message);
        this.name = 'SDKValidationError';
    }
}

/**
 * SDKConfig
 * @typedef {Object} SDKConfig
 * @property {String} [host=http://localhost]  host сервера
 * @property {Boolean} [replaceFilterValues=true]   нужно ли заменять константы в значениях фильтров
 * @property {String} [authToken=null]      auth token
 * @property {Boolean} [admin=false]        admin mode
 */
/**
 * IQuerySchema
 * @typedef {Object} IQuerySchema
 * @property {Array} $from
 * @property {Object} $fields
 * @property {Array} $metrics
 * @property {Array} $dimensions
 * @property {Array} $filters
 * @property {Array} $sort
 */
/**
 * DremioResult
 * @typedef {Object} DremioResult
 * @property {Number} rowCount    кол-во записей
 * @property {Array} schema       схема данных
 * @property {Array} rows         данные
 */
/**
 * DremioRootEntity
 * @typedef {Object} DremioRootEntity
 * @property {String} id
 * @property {Array} path
 * @property {String} type
 * @property {String} containerType
 */
/**
 * DremioContainerInfo
 * @typedef {Object} DremioContainerInfo
 * @property {String} id
 * @property {Array} path
 * @property {String} name
 * @property {String} entityType
 * @property {DremioContainerChildInfo[]} children
 */
/**
 * DremioContainerChildInfo
 * @typedef {Object} DremioContainerChildInfo
 * @property {String} id
 * @property {Array} path
 * @property {String} type
 * @property {String} datasetType
 * @property {String} containerType
 */
/**
 * DremioEntityInfo
 * @typedef {Object} DremioEntityInfo
 * @property {String} id
 * @property {Array} path
 * @property {String} type
 * @property {String} entityType
 * @property {Array} fields
 */
/**
 * @typedef {Object} DremioPermissions
 * @property {String} employeeId
 * @property {Array<Object>} permissions
 */
/**
 * @typedef {Object} DremioPermissionInfo
 * @property {String} name
 */
/**
 * @typedef {Promise} PromiseCancelable
 * @property {Function} cancel
 */
/**
 * @class
 */
class SDK {
    /**
     * Constructor
     * @param {SDKConfig} [config=null]   config
     */
    constructor(config = {}) {
        let defaults = {
            host: 'http://localhost',
            replaceFilterValues: true,
            authToken: null,
            admin: false
        };
        /**
         * @property {Function}
         */
        this.beforeRequest = null;
        /**
         * @property {SDKConfig}
         */
        this.config = Object.assign({}, defaults, config);
        this.axios = axios.create();
        this._axiosResolveHandler = ({ data }) => {
            if (data.error) {
                throw new SDKError(data.error);
            } else {
                return data;
            }
        };
        this._axiosRejectHandler = e => {
            e.isCancel = axios.isCancel(e);
            // not 2xx + { error:'' }
            if (e.response && e.response.data.error) {
                throw new SDKError(e.response.data.error);
            } else {
                throw e;
            }
        };
        this._requests = [];
    }
    /**
     * Возвращает список прав текущего пользователя (если задан authToken {@link SDK#config} и он валиден)
     * @return {PromiseCancelable.<DremioPermissions, Error>}
     */
    getPermissions() {
        let s = this._cancelSource();
        let p = this._hook(this.beforeRequest)
            .then(() =>
                this.axios.get(
                    `${this.config.host}/api/v1/permissions`,
                    this._getRequestConfig({ cancelToken: s.token })
                )
            )
            .then(this._axiosResolveHandler, this._axiosRejectHandler);
        return this._cancelable(p, s);
    }
    /**
     * Возвращает информацию о правах
     * @return {PromiseCancelable.<DremioPermissionInfo[], Error>}
     */
    getPermissionsInfo() {
        let s = this._cancelSource();
        let p = this._hook(this.beforeRequest)
            .then(() =>
                this.axios.get(
                    `${this.config.host}/api/v1/permissions-info`,
                    this._getRequestConfig({ cancelToken: s.token })
                )
            )
            .then(this._axiosResolveHandler, this._axiosRejectHandler);
        return this._cancelable(p, s);
    }
    /**
     * Возрвщает root сущности dremio
     * @return {PromiseCancelable.<DremioRootEntity[], Error>}
     */
    getRootEntities() {
        let s = this._cancelSource();
        let p = this._hook(this.beforeRequest)
            .then(() =>
                this.axios.get(
                    `${this.config.host}/api/v1/catalog`,
                    this._getRequestConfig({ cancelToken: s.token })
                )
            )
            .then(this._axiosResolveHandler, this._axiosRejectHandler);
        return this._cancelable(p, s);
    }
    /**
     * Возвращает сущность dremio.CatalogEntity по path
     * @param {Array} [path=null]    путь
     * @return {PromiseCancelable.<DremioContainerInfo | DremioEntityInfo, Error>}
     */
    getEntityByPath(path) {
        let s = this._cancelSource();
        let r = { path };
        let p = this._hook(this.beforeRequest)
            .then(() =>
                this.axios.post(
                    `${this.config.host}/api/v1/catalog/by-path`,
                    r,
                    this._getRequestConfig({ cancelToken: s.token })
                )
            )
            .then(this._axiosResolveHandler, this._axiosRejectHandler);
        return this._cancelable(p, s);
    }
    /**
     * Выполняет запрос
     * @param {IQuerySchema} query      query
     * @param {Number} [offset=0]       offset
     * @param {Number} [limit=10]       limit
     * @param {Boolean} [debug=false]   debug
     * @return {PromiseCancelable.<DremioResult, Error>}
     */
    getData(query, offset = 0, limit = 10, debug = false) {
        if (this.config.replaceFilterValues) {
            query = Query.queryReplaceAllFilterValues(Query.clone(query));
        }
        let s = this._cancelSource();
        let r = {
            query,
            limit,
            offset
        };
        if (debug) {
            r.debug = debug;
        }
        let p = this._hook(this.beforeRequest)
            .then(() =>
                this.axios.post(
                    `${this.config.host}/api/v1/data`,
                    r,
                    this._getRequestConfig({ cancelToken: s.token })
                )
            )
            .then(this._axiosResolveHandler, this._axiosRejectHandler);
        return this._cancelable(p, s);
    }
    /**
     * Отменяет все активные запросы
     */
    cancelActiveRequests() {
        while (this._requests.length) {
            let p = this._requests.pop();
            p.cancel();
        }
    }
    /**
     * @private
     * @param {Object} config
     * @return {Object}
     */
    _getRequestConfig(config) {
        let configAddons = {
            headers: this._getRequestHeaders()
        };
        return { ...config, ...configAddons };
    }
    /**
     * @private
     * @return {Object}
     */
    _getRequestHeaders() {
        let { authToken, admin } = this.config;
        let headers = {};
        if (authToken) {
            headers['token'] = authToken;
        }
        if (admin) {
            headers['x-dremio-admin'] = 1;
        }
        return headers;
    }
    /**
     * @private
     * @param {Function} func
     * @return {Promise}
     */
    _hook(func) {
        return func ? Promise.resolve(func()) : Promise.resolve();
    }
    /**
     * @private
     * @param {Promise} promise
     * @param {CancelTokenSource} source
     * @return {PromiseCancelable}
     */
    _cancelable(promise, source) {
        let remove = () => this._removeRequest(p);
        let p = promise.finally(remove);
        p.cancel = () => {
            source.cancel('cancel');
            remove();
        };
        this._addRequest(p);
        return p;
    }
    /**
     * @private
     * @return {CancelTokenSource}
     */
    _cancelSource() {
        return axios.CancelToken.source();
    }
    /**
     * @private
     * @param {PromiseCancelable} promise
     */
    _addRequest(promise) {
        this._requests.push(promise);
    }
    /**
     * @private
     * @param {PromiseCancelable} promise
     */
    _removeRequest(promise) {
        this._requests = this._requests.filter(p => p !== promise);
    }
}

/**
 * @class
 */
class Query {
    /**
     * @typedef {Object} QueryOptions
     * @property {IQuerySchema} query       query
     * @property {Object} dimensionList     dimensionList
     */
    /**
     * Constructor
     * @param {QueryOptions} options
     */
    constructor({ query, dimensionList }) {
        this.query = query;
        this.dimensionList = dimensionList;
        this.states = [];
        this.enableDimensions(Object.keys(dimensionList));
    }
    /**
     * Активирует dimension из списка dimensionList
     * @param {Array} names     массив названий dimension
     */
    enableDimensions(names) {
        let arr = [];
        names.forEach(name => {
            let fields = this.dimensionList[name];
            if (!fields) {
                return;
            }
            arr.push({
                name,
                index: 0,
                fields,
                filters: []
            });
        });
        this.states = arr;
    }
    /**
     * Формирует query с текущим state всех активных dimension
     * @return {IQuerySchema}     query
     */
    buildQuery() {
        let query = Query.clone(this.query);
        this.states.forEach(({ name, index, fields, filters }) => {
            let field = fields[index];
            if (!field) {
                return;
            }
            let dimension = Query.createDimension({ name, field });
            Query.queryInsertUpdateDimension(query, dimension);
            filters.forEach(filter => {
                Query.queryInsertUpdateFilter(query, filter);
            });
        });
        return query;
    }
    /**
     * Проверяет, сущ. ли state для dimensionName
     * @param {String} dimensionName    название dimension
     * @return {Boolean}
     */
    dimensionStateExists(dimensionName) {
        return this.states.find(({ name }) => name == dimensionName) != null;
    }
    /**
     * Возвращает true, если dimension state первый
     * @param {String} dimensionName    название dimension
     * @return {Boolean}
     */
    dimensionStateIsFirst(dimensionName) {
        let state = this.states.find(({ name }) => name == dimensionName);
        return state ? state.index == 0 : false;
    }
    /**
     * Возвращает true, если dimension state последний
     * @param {String} dimensionName    название dimension
     * @return {Boolean}
     */
    dimensionStateIsLast(dimensionName) {
        let state = this.states.find(({ name }) => name == dimensionName);
        return state ? state.index == state.fields.length - 1 : false;
    }
    /**
     * Сдвигает тек. dimension state на шаг вперед
     * @param {String} dimensionName    название dimension
     * @param {Array} filterValue      значения filter
     * @param {String} filterType       тип filter {@link Query#FILTER_TYPE}
     * @return {Boolean}                true если удалось; false нет
     */
    dimensionStateGoNext(dimensionName, filterValue, filterType) {
        let state = this.states.find(({ name }) => dimensionName == name);
        if (!state || state.index >= state.fields.length - 1) {
            return false;
        }
        // add filter
        let filter = Query.createFilter({
            name: state.fields[state.index],
            type: filterType,
            value: filterValue
        });
        state.filters.push(filter);
        state.index++;
    }
    /**
     * Сдвигает тек. dimension state на шаг назад
     * @param {String} dimensionName    название dimension
     * @return {Boolean}                true если удалось; false нет
     */
    dimensionStateGoPrev(dimensionName) {
        let state = this.states.find(({ name }) => dimensionName == name);
        if (!state || state.index <= 0) {
            return false;
        }
        // remove filter
        state.filters.pop();
        state.index--;
        return true;
    }
    /**
     * Клонирует json-like obj
     * @param {Object} obj
     * @return {Object}
     */
    static clone(obj) {
        try {
            return JSON.parse(JSON.stringify(obj));
        } catch (e) {
            return Object.assign({}, obj);
        }
    }
    /**
     * Создает/валидирует query
     * @param {IQuerySchema} [query=null]    query для валидации
     */
    static createQuery(query = null) {
        if (query) {
            if (!Query.validateQuery(query)) {
                throw new SDKValidationError('query schema invalid');
            }
            if (!Object.keys(query[Query.KEY.FIELDS])) {
                throw new SDKValidationError(`query ${Query.KEY.FIELDS} schema invalid`);
            }
            if (!Query.validateFrom(query)) {
                throw new SDKValidationError(`query ${Query.KEY.FROM} schema invalid`);
            }
            query[Query.KEY.METRICS].forEach(el => {
                if (!Query.validateMetric(el)) {
                    throw new SDKValidationError(`query ${Query.KEY.METRICS} schema invalid`);
                }
            });
            query[Query.KEY.DIMENSIONS].forEach(el => {
                if (!Query.validateDimension(el)) {
                    throw new SDKValidationError(`query ${Query.KEY.DIMENSIONS} schema invalid`);
                }
            });
            query[Query.KEY.FILTERS].forEach(el => {
                if (!Query.validateFilter(el)) {
                    throw new SDKValidationError(`query ${Query.KEY.FILTERS} schema invalid`);
                }
            });
            query[Query.KEY.SORT].forEach(el => {
                if (!Query.validateSort(el)) {
                    throw new SDKValidationError(`query ${Query.KEY.SORT} schema invalid`);
                }
            });
        } else {
            query = {
                [Query.KEY.FIELDS]: {},
                [Query.KEY.FROM]: [],
                [Query.KEY.METRICS]: [],
                [Query.KEY.DIMENSIONS]: [],
                [Query.KEY.FILTERS]: [],
                [Query.KEY.SORT]: []
            };
        }
        return query;
    }
    /**
     * @typedef {Object} MetricOptions
     * @param {String} name?        название metric
     * @param {String} type?        тип metric {@link Query#METRIC_TYPE}
     * @param {String} field?       название field
     */
    /**
     * Создает/мутирует metric
     * @param {MetricOptions} options   опции
     * @param {Object} [metric=null]           metric для мутации
     */
    static createMetric({ name, type, field }, metric = null) {
        if (metric) {
            // @TODO validate ~ throw new SDKValidationError()
            if (name) {
                ObjUtils.replaceKey(metric, ObjUtils.firstKey(metric), name);
            }
            if (type) {
                let n = ObjUtils.firstKey(metric);
                ObjUtils.replaceKey(metric[n], ObjUtils.firstKey(metric[n]), type);
            }
            if (field) {
                let n = ObjUtils.firstKey(metric);
                let t = ObjUtils.firstKey(metric[n]);
                metric[n][t] = field;
            }
        } else {
            metric = { [name]: { [type]: field } };
        }
        return metric;
    }
    /**
     * @typedef {Object} DimensionOptions
     * @param {String} name?        название metric
     * @param {String} field?       название field
     */
    /**
     * Создает/мутирует dimension
     * @param {DimensionOptions} options    опции
     * @param {Object} [dimension=null]            dimension для мутации
     */
    static createDimension({ name, field }, dimension = null) {
        if (dimension) {
            // @TODO validate ~ throw new SDKValidationError()
            if (name) {
                ObjUtils.replaceKey(dimension, ObjUtils.firstKey(dimension), name);
            }
            if (field) {
                let n = ObjUtils.firstKey(dimension);
                dimension[n] = field;
            }
        } else {
            dimension = { [name]: field };
        }
        return dimension;
    }
    /**
     * @typedef {Object} FilterOptions
     * @param {String} name?        название metric/dimension/field
     * @param {String} type?        тип filter {@link Query#FILTER_TYPE}
     * @param {Array} value?       массив значений
     */
    /**
     * Создает/мутирует filter
     * @param {FilterOptions} options    опции
     * @param {Object} [filter=null]            filter для мутации
     */
    static createFilter({ name, type, value }, filter = null) {
        if (filter) {
            // @TODO validate ~ throw new SDKValidationError()
            if (name) {
                ObjUtils.replaceKey(filter, ObjUtils.firstKey(filter), name);
            }
            if (type) {
                let n = ObjUtils.firstKey(filter);
                ObjUtils.replaceKey(filter[n], ObjUtils.firstKey(filter[n]), type);
            }
            if (value) {
                let n = ObjUtils.firstKey(filter);
                let t = ObjUtils.firstKey(filter[n]);
                filter[n][t] = value;
            }
        } else {
            filter = { [name]: { [type]: value } };
        }
        return filter;
    }
    /**
     * @typedef {Object} SortOptions
     * @param {String} name?       название metric/dimension
     * @param {String} type?       тип sort {@link Query#SORT_TYPE}
     */
    /**
     * Создает/мутирует sort
     * @param {SortOptions} options     опции
     * @param {Object} [sort=null]             sort для мутации
     */
    static createSort({ name, type }, sort = null) {
        if (sort) {
            // @TODO validate ~ throw new SDKValidationError()
            if (name) {
                ObjUtils.replaceKey(sort, ObjUtils.firstKey(sort), name);
            }
            if (type) {
                let n = ObjUtils.firstKey(sort);
                sort[n] = type;
            }
        } else {
            sort = { [name]: type };
        }
        return sort;
    }
    /**
     * Возвращает имя metric
     * @param {Object} metric
     * @return {String}
     */
    static getMetricName(metric) {
        return ObjUtils.firstKey(metric);
    }
    /**
     * Возвращает тип metric
     * @param {Object} metric
     * @return {String}
     */
    static getMetricType(metric) {
        let n = ObjUtils.firstKey(metric);
        return ObjUtils.firstKey(metric[n]);
    }
    /**
     * Возвращает field metric
     * @param {Object} metric
     * @return {String}
     */
    static getMetricField(metric) {
        let n = ObjUtils.firstKey(metric);
        let t = ObjUtils.firstKey(metric[n]);
        return metric[n][t];
    }
    /**
     * Возвращает имя dimension
     * @param {Object} dimension
     * @return {String}
     */
    static getDimensionName(dimension) {
        return ObjUtils.firstKey(dimension);
    }
    /**
     * Возвращает field dimension
     * @param {Object} dimension
     * @return {String}
     */
    static getDimensionField(dimension) {
        let n = ObjUtils.firstKey(dimension);
        return dimension[n];
    }
    /**
     * Возвращает имя filter
     * @param {Object} filter
     * @return {String}
     */
    static getFilterName(filter) {
        return ObjUtils.firstKey(filter);
    }
    /**
     * Возвращает тип filter
     * @param {Object} filter
     * @return {String}
     */
    static getFilterType(filter) {
        let n = ObjUtils.firstKey(filter);
        return ObjUtils.firstKey(filter[n]);
    }
    /**
     * Возвращает value filter
     * @param {Object} filter
     * @return {String}
     */
    static getFilterValue(filter) {
        let n = ObjUtils.firstKey(filter);
        let t = ObjUtils.firstKey(filter[n]);
        return filter[n][t];
    }
    /**
     * Возвращает имя sort
     * @param {Object} sort
     * @return {String}
     */
    static getSortName(sort) {
        return ObjUtils.firstKey(sort);
    }
    /**
     * Возвращает тип sort
     * @param {Object} sort
     * @return {String}
     */
    static getSortType(sort) {
        let n = ObjUtils.firstKey(sort);
        return sort[n];
    }
    /**
     * Добавляет/заменяет filter в query
     * @param {IQuerySchema} query      query
     * @param {Object} filter           filter
     * @return {IQuerySchema}
     */
    static queryInsertUpdateFilter(query, filter) {
        let arr = query[Query.KEY.FILTERS];
        let filterName = Query.getFilterName(filter);
        let index = arr.findIndex(el => Query.getFilterName(el) == filterName);
        if (index >= 0) {
            arr.splice(index, 1, filter);
        } else {
            arr.push(filter);
        }
        return query;
    }
    /**
     * Удаляет filter из query
     * @param {IQuerySchema} query      query
     * @param {String} name             название metric/dimension
     * @return {IQuerySchema}
     */
    static queryRemoveFilter(query, name) {
        query[Query.KEY.FILTERS] = query[Query.KEY.FILTERS].filter(
            el => Query.getFilterName(el) != name
        );
        return query;
    }
    /**
     * Удаляет все filter из query
     * @param {IQuerySchema} query    query
     * @return {IQuerySchema}
     */
    static queryRemoveAllFilters(query) {
        query[Query.KEY.FILTERS] = [];
        return query;
    }
    /**
     * Заменяет Query.FILTER_VALUE value всех фильтров
     * @param {IQuerySchema} query
     * @return {IQuerySchema}
     */
    static queryReplaceAllFilterValues(query) {
        query[Query.KEY.FILTERS].forEach(el => {
            let value = Query.getFilterValue(el).map(val => {
                for (let key in Query.FILTER_VALUE) {
                    let handler = Query.FILTER_VALUE_HANDLER[key];
                    if (Query.FILTER_VALUE[key] === val && handler) {
                        return handler();
                    }
                }
                return val;
            });
            Query.createFilter({ value }, el);
        });
        return query;
    }
    /**
     * Возвращает field name dimension
     * @param {IQuerySchema} query      query
     * @param {String} name             dimension name
     * @return {String}                 field name или null если dimension нет
     */
    static queryGetDimensionField(query, name) {
        let dimension = query[Query.KEY.DIMENSIONS].find(el => Query.getDimensionName(el) == name);
        return dimension ? dimension[name] : null;
    }
    /**
     * Добавляет/заменяет dimension в query
     * @param {IQuerySchema} query      query
     * @param {Object} dimension        dimension
     * @return {IQuerySchema}
     */
    static queryInsertUpdateDimension(query, dimension) {
        let arr = query[Query.KEY.DIMENSIONS];
        let dimensionName = Query.getDimensionName(dimension);
        let index = arr.findIndex(el => Query.getDimensionName(el) == dimensionName);
        if (index >= 0) {
            arr.splice(index, 1, dimension);
        } else {
            arr.push(dimension);
        }
        return query;
    }
    /**
     * Возвращает массив имен field
     * @param {IQuerySchema} query      query
     * @return {Array}                  массив имен field
     */
    static queryFieldNames(query) {
        return Object.keys(query[Query.KEY.FIELDS]);
    }
    /**
     * Возвращает массив имен metric в query
     * @param {IQuerySchema} query      query
     * @return {Array}                  массив имен metric
     */
    static queryMetricNames(query) {
        return query[Query.KEY.METRICS]
            .map(el => Query.getMetricName(el))
            .filter((v, i, arr) => arr.indexOf(v) === i);
    }
    /**
     * Возвращает массив имен dimension в query
     * @param {IQuerySchema} query      query
     * @return {Array}                  массив имен dimension
     */
    static queryDimensionNames(query) {
        return query[Query.KEY.DIMENSIONS]
            .map(el => Query.getDimensionName(el))
            .filter((v, i, arr) => arr.indexOf(v) === i);
    }
    /**
     * Валидирует query
     * @param {IQuerySchema} query
     * @return {Boolean}
     */
    static validateQuery(query) {
        let keys = Object.values(Query.KEY);
        let found = keys.reduce((acc, val) => (acc + query[val] ? 1 : 0), 0);
        return keys.length != found;
    }
    /**
     * Валидирует from
     * @param {IQuerySchema} query
     * @return {Boolean}
     */
    static validateFrom(query) {
        let from = query[Query.KEY.FROM];
        return Array.isArray(from) && from.length > 0;
    }
    /**
     * Валидирует metric
     * @param {Object} metric
     * @return {Boolean}
     */
    static validateMetric(metric) {
        if (!Query._validateObj(metric)) {
            return false;
        }
        let name = ObjUtils.firstKey(metric);
        let def = metric[name];
        if (!def || !Query._validateObj(def)) {
            return false;
        }
        let type = ObjUtils.firstKey(def);
        if (Object.values(Query.METRIC_TYPE).indexOf(type) < 0) {
            return false;
        }
        return typeof metric[name][type] == 'string' && metric[name][type].length > 0;
    }
    /**
     * Валидирует dimension
     * @param {Object} dimension
     * @return {Boolean}
     */
    static validateDimension(dimension) {
        if (!Query._validateObj(dimension)) {
            return false;
        }
        let name = ObjUtils.firstKey(dimension);
        return typeof dimension[name] == 'string' && dimension[name].length > 0;
    }
    /**
     * Валидирует filter
     * @param {Object} filter
     * @return {Boolean}
     */
    static validateFilter(filter) {
        if (!Query._validateObj(filter)) {
            return false;
        }
        let name = ObjUtils.firstKey(filter);
        let def = filter[name];
        if (!def || !Query._validateObj(def)) {
            return false;
        }
        let type = ObjUtils.firstKey(def);
        if (Object.values(Query.FILTER_TYPE).indexOf(type) < 0) {
            return false;
        }
        let values = filter[name][type];
        if (!Array.isArray(values) || !Query.FILTER_TYPE_VALUE_VALIDATION[type](values)) {
            return false;
        }
        return true;
    }
    /**
     * Валидирует sort
     * @param {Object} sort
     * @return {Boolean}
     */
    static validateSort(sort) {
        if (!Query._validateObj(sort)) {
            return false;
        }
        let name = ObjUtils.firstKey(sort);
        if (Object.values(Query.SORT_TYPE).indexOf(sort[name]) < 0) {
            return false;
        }
        return true;
    }
    /**
     * @private Валидирует metric/dimension/filter/sort like obj
     * @param {Object} obj
     * @return {Boolean}
     */
    static _validateObj(obj) {
        if (typeof obj !== 'object') {
            return false;
        }
        let k = ObjUtils.firstKey(obj);
        if (!k) {
            return false;
        }
        return true;
    }
}
Query.FORMAT = {
    DATE: 'YYYY-MM-DD',
    TIME: 'HH:mm:ss',
    TIMESTAMP: 'YYYY-MM-DD HH:mm:ss'
};
/**
 * @static
 * @enum {String}
 */
Query.KEY = {
    /** @member {String} */
    FIELDS: '$fields',
    /** @member {String} */
    FROM: '$from',
    /** @member {String} */
    METRICS: '$metrics',
    /** @member {String} */
    DIMENSIONS: '$dimensions',
    /** @member {String} */
    FILTERS: '$filters',
    /** @member {String} */
    SORT: '$sort'
};
/**
 * @static
 * @enum {String}
 */
Query.SORT_TYPE = {
    /** @member {String} */
    ASC: 'asc',
    /** @member {String} */
    DESC: 'desc'
};
/**
 * @static
 * @enum {String}
 */
Query.METRIC_TYPE = {
    /**
     * no aggregation
     * @member {String}
     */
    VALUE: '$v',
    /**
     * sql expression
     * @member {String}
     */
    EXPRESSION: '$exp',
    /**
     * equivalent of sql 'avg()'
     * @member {String}
     */
    AVG: '$avg',
    /**
     * equivalent of sql 'count()'
     * @member {String}
     */
    COUNT: '$count',
    /**
     * equivalent of sql 'max()'
     * @member {String}
     */
    MAX: '$max',
    /**
     * equivalent of sql 'min()'
     * @member {String}
     */
    MIN: '$min',
    /**
     * equivalent of sql 'sum()'
     * @member {String}
     */
    SUM: '$sum',
    /**
     * equivalent of mysql 'group_concat()'
     * @member {String}
     */
    GROUP_CONCAT: '$gc',
    /**
     * equivalent of mysql 'group_concat(distinct)'
     * @member {String}
     */
    GROUP_CONCAT_UNIQ: '$gcu'
};
/**
 * @static
 * @enum {String}
 */
Query.FILTER_TYPE = {
    /**
     * equivalent of sql '='
     * @member {String}
     */
    EQ: '$eq',
    /**
     * equivalent of sql '!='
     * @member {String}
     */
    EQ_NOT: '$eqn',
    /**
     * equivalent of sql '<'
     * @member {String}
     */
    LESS: '$lt',
    /**
     * equivalent of sql '<='
     * @member {String}
     */
    LESS_EQ: '$ltq',
    /**
     * equivalent of sql '>'
     * @member {String}
     */
    GREATER: '$gt',
    /**
     * equivalent of sql '>='
     * @member {String}
     */
    GREATER_EQ: '$gtq',
    /**
     * equivalent of sql 'in()'
     * @member {String}
     */
    IN: '$in',
    /**
     * equivalent of sql 'not in()'
     * @member {String}
     */
    IN_NOT: '$inn',
    /**
     * equivalent of sql 'between()'
     * @member {String}
     */
    BETWEEN: '$btw',
    /**
     * equivalent of sql 'not between()'
     * @member {String}
     */
    BETWEEN_NOT: '$btwn',
    /**
     * equivalent of sql 'like()'
     * @member {String}
     */
    LIKE: '$lk',
    /**
     * equivalent of sql 'max()'
     * @member {String}
     */
    MAX: '$max',
    /**
     * equivalent of sql 'min()'
     * @member {String}
     */
    MIN: '$min'
};
/**
 * @static
 * @enum {String}
 */
Query.FILTER_VALUE = {
    /** @member {String} */
    YEAR_ROLLING: '#year-rolling#',
    /** @member {String} */
    YEAR_START: '#year-start#',
    /** @member {String} */
    YEAR_END: '#year-end#',
    /** @member {String} */
    YEAR_PREV_START: '#year-prev-start#',
    /** @member {String} */
    YEAR_PREV_END: '#year-prev-end#',

    /** @member {String} */
    QUARTER_START: '#quarter-start#',
    /** @member {String} */
    QUARTER_END: '#quarter-end#',
    /** @member {String} */
    QUARTER_PY_START: '#quarter-py-start#',
    /** @member {String} */
    QUARTER_PY_END: '#quarter-py-end#',
    /** @member {String} */
    QUARTER_PREV_START: '#quarter-prev-start#',
    /** @member {String} */
    QUARTER_PREV_END: '#quarter-prev-end#',

    /** @member {String} */
    MONTH_ROLLING: '#month-rolling#',
    /** @member {String} */
    MONTH_START: '#month-start#',
    /** @member {String} */
    MONTH_END: '#month-end#',
    /** @member {String} */
    MONTH_PY_START: '#month-py-start#',
    /** @member {String} */
    MONTH_PY_END: '#month-py-end#',
    /** @member {String} */
    MONTH_PREV_START: '#month-prev-start#',
    /** @member {String} */
    MONTH_PREV_END: '#month-prev-end#',

    /** @member {String} */
    WEEK_ROLLING: '#week-rolling#',
    /** @member {String} */
    WEEK_START: '#week-start#',
    /** @member {String} */
    WEEK_END: '#week-end#',
    /** @member {String} */
    WEEK_PY_START: '#week-py-start#',
    /** @member {String} */
    WEEK_PY_END: '#week-py-end#',
    /** @member {String} */
    WEEK_PREV_START: '#week-prev-start#',
    /** @member {String} */
    WEEK_PREV_END: '#week-prev-end#',

    /** @member {String} */
    TODAY: '#today#',
    /** @member {String} */
    YESTERDAY: '#yesterday#'
};
/**
 * @static
 * @enum {String}
 */
Query.FILTER_VALUE_HANDLER = {
    /** @member {Function} */
    YEAR_ROLLING: () => dayjs().add(-365, 'd').format(Query.FORMAT.DATE),
    /** @member {Function} */
    YEAR_START: () => dayjs().startOf('year').format(Query.FORMAT.DATE),
    /** @member {Function} */
    YEAR_END: () => dayjs().endOf('year').format(Query.FORMAT.DATE),
    /** @member {Function} */
    YEAR_PREV_START: () => dayjs().add(-1, 'y').startOf('year').format(Query.FORMAT.DATE),
    /** @member {Function} */
    YEAR_PREV_END: () => dayjs().add(-1, 'y').endOf('year').format(Query.FORMAT.DATE),

    /** @member {Function} */
    QUARTER_START: () => dayjs().startOf('Q').format(Query.FORMAT.DATE),
    /** @member {Function} */
    QUARTER_END: () => dayjs().endOf('Q').format(Query.FORMAT.DATE),
    /** @member {Function} */
    QUARTER_PY_START: () => dayjs().add(-1, 'y').startOf('Q').format(Query.FORMAT.DATE),
    /** @member {Function} */
    QUARTER_PY_END: () => dayjs().add(-1, 'y').endOf('Q').format(Query.FORMAT.DATE),
    /** @member {Function} */
    QUARTER_PREV_START: () => dayjs().add(-1, 'Q').startOf('Q').format(Query.FORMAT.DATE),
    /** @member {Function} */
    QUARTER_PREV_END: () => dayjs().add(-1, 'Q').endOf('Q').format(Query.FORMAT.DATE),

    /** @member {Function} */
    MONTH_ROLLING: () => dayjs().add(-30, 'd').format(Query.FORMAT.DATE),
    /** @member {Function} */
    MONTH_START: () => dayjs().startOf('month').format(Query.FORMAT.DATE),
    /** @member {Function} */
    MONTH_END: () => dayjs().endOf('month').format(Query.FORMAT.DATE),
    /** @member {Function} */
    MONTH_PY_START: () => dayjs().add(-1, 'y').startOf('month').format(Query.FORMAT.DATE),
    /** @member {Function} */
    MONTH_PY_END: () => dayjs().add(-1, 'y').endOf('month').format(Query.FORMAT.DATE),
    /** @member {Function} */
    MONTH_PREV_START: () => dayjs().add(-1, 'M').startOf('month').format(Query.FORMAT.DATE),
    /** @member {Function} */
    MONTH_PREV_END: () => dayjs().add(-1, 'M').endOf('month').format(Query.FORMAT.DATE),

    /** @member {Function} */
    WEEK_ROLLING: () => dayjs().add(-7, 'd').format(Query.FORMAT.DATE),
    /** @member {Function} */
    WEEK_START: () => dayjs().startOf('isoWeek').format(Query.FORMAT.DATE),
    /** @member {Function} */
    WEEK_END: () => dayjs().endOf('isoWeek').format(Query.FORMAT.DATE),
    /** @member {Function} */
    WEEK_PY_START: () => dayjs().add(-1, 'y').startOf('isoWeek').format(Query.FORMAT.DATE),
    /** @member {Function} */
    WEEK_PY_END: () => dayjs().add(-1, 'y').endOf('isoWeek').format(Query.FORMAT.DATE),
    /** @member {Function} */
    WEEK_PREV_START: () =>
        dayjs().startOf('isoWeek').add(-1, 'd').startOf('isoWeek').format(Query.FORMAT.DATE),
    /** @member {Function} */
    WEEK_PREV_END: () =>
        dayjs().startOf('isoWeek').add(-1, 'd').endOf('isoWeek').format(Query.FORMAT.DATE),

    /** @member {Function} */
    TODAY: () => dayjs().format(Query.FORMAT.DATE),
    /** @member {Function} */
    YESTERDAY: () => dayjs().add(-1, 'd').format(Query.FORMAT.DATE)
};
Query.FILTER_TYPE_VALUE_VALIDATION = {
    [Query.FILTER_TYPE.EQ]: v => v.length == 1,
    [Query.FILTER_TYPE.EQ_NOT]: v => v.length == 1,
    [Query.FILTER_TYPE.LESS]: v => v.length == 1,
    [Query.FILTER_TYPE.LESS_EQ]: v => v.length == 1,
    [Query.FILTER_TYPE.GREATER]: v => v.length == 1,
    [Query.FILTER_TYPE.GREATER_EQ]: v => v.length == 1,
    [Query.FILTER_TYPE.IN]: v => v.length > 0,
    [Query.FILTER_TYPE.IN_NOT]: v => v.length > 0,
    [Query.FILTER_TYPE.BETWEEN]: v => v.length == 2,
    [Query.FILTER_TYPE.BETWEEN_NOT]: v => v.length == 2,
    [Query.FILTER_TYPE.LIKE]: v => v.length > 0,
    [Query.FILTER_TYPE.MAX]: v => v.length == 1,
    [Query.FILTER_TYPE.MIN]: v => v.length == 1
};

/**
 * @namespace
 */
let Errors = {
    /** @property {SDKError} */
    SDKError,
    /** @property {SDKValidationError} */
    SDKValidationError
};
export { SDK, Query, Dremio, Errors };