diff --git a/client/modules/home/components/dashboards/presentation_mode/chart_viewer.jsx b/client/modules/home/components/dashboards/presentation_mode/chart_viewer.jsx index 55786eb..234f5b3 100644 --- a/client/modules/home/components/dashboards/presentation_mode/chart_viewer.jsx +++ b/client/modules/home/components/dashboards/presentation_mode/chart_viewer.jsx @@ -17,6 +17,7 @@ const ChartViewer = ({ dashboard, charts, meteorMethodCall, routeTo, setChartVie prevArrow: , nextArrow: , }; + return (
{ } const { viewObject: { data, chartType, dataTimeStamp, pivot }, queryObject } = chart; const needRedraw = !dataTimeStamp || Date.now() - dataTimeStamp < 1000; + if (chartType) { return ( { - if (err) cb({ error: true }); + if (err) { + cb({ error: true }); + } else if (LocalState.get('QUASAR_REQUEST_ID') === answerId || isSingleRequest) { const processedData = dataProcessing.process(data, fields); cb({ data: processedData }); @@ -48,20 +51,41 @@ export default { } ); }, - determineDefaultFields({ Notificator }, fields, chartType, pivot) { - let result; - if (pivot && pivot.model) { - const measuresId = pivot.model.values; + + // todo !!! + // this function must be refactored in th future + determineDefaultFields({ Notificator, LocalState }, fields, chartType, pivot) { + const determineFieldsBySavedConstructors = (fieldsArr, constructors) => + fieldsArr.map(field => { + if (constructors.dimensions.includes(field.id)) { + field.constructorType = 'dimensions'; + } else if (constructors.measures.includes(field.id)) { + field.constructorType = 'measures'; + } + return field; + }); + const determineFieldsByModel = (fieldsArr, pivotModel) => { + const measuresId = pivotModel.values; let dimensionsCounter = 0; - fields.forEach(f => { - if (!~measuresId.indexOf(f.id)) { + return fieldsArr.map(field => { + if (!~measuresId.indexOf(field.id)) { dimensionsCounter++; - if (dimensionsCounter < 3) f.constructorType = 'dimensions'; + if (dimensionsCounter < 3) field.constructorType = 'dimensions'; } else { - f.constructorType = 'measures'; + field.constructorType = 'measures'; } + return field; }); - result = fields; + }; + const viewObj = LocalState.get('VIEW_OBJECT'); + let result; + + if (viewObj.fieldsConstructors) { + return determineFieldsBySavedConstructors(fields, viewObj.fieldsConstructors); + } + + if (pivot && pivot.model) { + result = determineFieldsByModel(fields, pivot.model); } else { const getNeededfields = () => { const fieldsArray = []; @@ -102,13 +126,14 @@ export default { }); if (neededFields.filter(f => f.id).length) result = fields; } + return result; }, getNewChartModelFields(context, { chartType, fields }, { fieldId, isChecked, label }) { const checkedFieldsNumber = fields.filter(f => f.constructorType === label).length; const newFields = fields.map(f => _.extend(_.clone(f), - f.id === fieldId ? { constructorType: isChecked ? label : null } : {}) + f.id === fieldId ? { constructorType: isChecked ? label : null } : {}) ); const maxCheckedFields = (() => { let res = chartTypeRules[chartType][label]; diff --git a/client/modules/workplace/actions/workplace_state.js b/client/modules/workplace/actions/workplace_state.js index eae7a6a..f2ec70e 100644 --- a/client/modules/workplace/actions/workplace_state.js +++ b/client/modules/workplace/actions/workplace_state.js @@ -1,14 +1,23 @@ import { _ } from 'meteor/underscore'; import { pivotCellsLimit } from '/lib/constants'; -import SQLParser from '/lib/sql_parser'; +import { parseToQueryObject } from '/lib/sql_parser'; export default { setSQLQuery({ LocalState }, query) { const fields = LocalState.get('COLLECTION_FIELDS'); const oldQueryObj = LocalState.get('SQL_QUERY_OBJECT'); - const queryObj = SQLParser.parseToQueryObject(query, fields); + const fieldsConstructors = getFieldsConstructorsType(oldQueryObj.fields); + + if (fieldsConstructors) { + const viewObj = LocalState.get('VIEW_OBJECT'); + viewObj.fieldsConstructors = fieldsConstructors; + LocalState.set('VIEW_OBJECT', viewObj); + } + + const queryObj = parseToQueryObject(query, fields); queryObj.from = oldQueryObj.from; queryObj.on = oldQueryObj.on; + LocalState.set('SQL_QUERY_OBJECT', queryObj); }, @@ -109,3 +118,25 @@ function resetData(LocalState) { LocalState.set('VIEW_OBJECT', viewObj); } } + +const isPropEmptyArray = (prop, obj) => !obj[prop].length; + +const getFieldsConstructor = fields => fields.reduce( + (constructor, { constructorType }, index) => { + if (constructorType) { + constructor[constructorType].push(index); + } + return constructor; + }, { + measures: [], + dimensions: [], + } +); + +function getFieldsConstructorsType(fields) { + const constructor = getFieldsConstructor(fields); + const isDimensions = !isPropEmptyArray('dimensions', constructor); + const isMeasures = !isPropEmptyArray('measures', constructor); + + return (isDimensions || isMeasures) ? constructor : false; +} diff --git a/client/modules/workplace/components/preview/data_visualisation.jsx b/client/modules/workplace/components/preview/data_visualisation.jsx index ae4203e..8507f44 100644 --- a/client/modules/workplace/components/preview/data_visualisation.jsx +++ b/client/modules/workplace/components/preview/data_visualisation.jsx @@ -22,6 +22,7 @@ class DataVisualisation extends React.Component { updateSQLQueryObj } = this.props; const { chartType } = viewObject; const tableHeight = `${window.innerHeight - 74}px`; + return (
{tableType === 'simple' && diff --git a/client/modules/workplace/components/preview/preview.jsx b/client/modules/workplace/components/preview/preview.jsx index b706080..13c7c42 100644 --- a/client/modules/workplace/components/preview/preview.jsx +++ b/client/modules/workplace/components/preview/preview.jsx @@ -44,6 +44,7 @@ class Preview extends React.Component { const { savedQuery, queryObject, viewObject, tableType } = this.props; const { isQueryChanged, isLive } = this.state; const isFields = !_.isEmpty(queryObject.fields); + return (
{ const { chartType, query: savedQuery } = viewObject; parseCollectionFieldNames(queryObject.collectionFields); setSQLQueryObj(queryObject, tableType); + LocalState.set('VIEW_OBJECT', viewObject); onData(null, { isSavedChart: true, tableType, chartType, savedQuery }); } diff --git a/lib/sql_parser.js b/lib/sql_parser.js deleted file mode 100644 index 986b7fd..0000000 --- a/lib/sql_parser.js +++ /dev/null @@ -1,263 +0,0 @@ -import { _ } from 'meteor/underscore'; - -const cutOff = - /(\sgroup by\s|\sorder by\s|\swhere\s|\shaving\s|\slimit\s|\soffset\s)+[\w([\])"'`.,/=<>\s]+$/i; - -export default { - parse(query) { - const objectSQL = { - select: getSelect(query), - from: getFrom(query), - having: getHaving(query), - where: getWhere(query), - limit: getLimit(query), - offset: getOffset(query), - join: getJoin(query), - }; - objectSQL.groupBy = getGroupBy(query, objectSQL.select); - objectSQL.orderBy = getOrderBy(query, objectSQL.select); - return objectSQL; - }, - parseToQueryObject(rawQuery, fields) { - const query = this.parse(rawQuery); - const queryObject = {}; - const having = getHavingObj(query); - - queryObject.pagination = getPagination(query); - queryObject.fields = getFields(query, having); - - query.groupBy && query.groupBy.forEach(fieldIndex => { - queryObject.fields[fieldIndex].grouping = true; - }); - query.orderBy && query.orderBy.forEach(field => { - queryObject.fields[field.index].sort = field.type; - }); - - queryObject.collectionFields = getCollectionFields(query, fields); - - queryObject.join = query.join; - - return queryObject; - }, -}; - -function getSelect(query) { - let select = query.match(/^\s*select\s+([\w([\])"'`.,/\s]+)from\s/i); - if (!select) return { error: 'Wrong syntax: "SELECT FROM " expected' }; - select = select[1]; - const asNames = select.match(/\s+as\s+\w+/ig); - let names = []; - let expressions = []; - if (asNames) { - names = asNames.map(item => item.replace(/(^\s+as\s|\s)+/ig, '')); - expressions = [select.slice(0, select.indexOf(asNames[0]))]; - for (let i = 0; i < asNames.length - 1; i++) { - expressions.push(select.slice(select.indexOf(asNames[i]) + asNames[i].length, - select.indexOf(asNames[i + 1]))); - } - } else { - expressions = select.match(/\w+\.\w+/ig); - if (expressions) { - names = expressions.map(exp => exp.match(/\w+\.(\w+)/i)[1]); - } else { - return { error: 'Wrong fields!' }; - } - } - return names.map((name, i) => ({ - name: names[i], - expression: expressions[i].replace(/(^,|\s)/g, ''), - })); -} - -const getParamName = (queryPartial) => queryPartial.match(/\s+as\s+(\w+)/i); -const parseQueryPartial = (partial, paramName) => - partial.slice(0, paramName.index).match(/(\w+)\/(\w+)/i); - -function getFrom(query) { - const from = query.match(/\sfrom\s+([\w([\])"'`.,/=<>\s]+$)/i)[1].replace(cutOff, ''); - const name = getParamName(from); - const path = parseQueryPartial(from, name); - return { db: path[1], collection: path[2], name: name[1] }; -} - -function getJoin(query) { - const from = query.match(/\sjoin\s+([\w([\])"'`.,/=<>\s]+$)/i)[1].replace(cutOff, ''); - const name = getParamName(from); - const path = parseQueryPartial(from, name); - return path[2]; -} - -function getGroupBy(query, select) { - let groupBy = query.match(/\sgroup by\s+([\w([\])"'`.,/=<>\s]+$)/i); - if (groupBy) { - groupBy = groupBy[1].replace(cutOff, '') - .replace(/\s/g, ''); - return select.reduce((previous, field, i) => { - if (~groupBy.indexOf(field.expression)) previous.push(i); - return previous; - }, []); - } - return null; -} - -function getOrderBy(query, select) { - let orderBy = query.match(/\sorder by\s+([\w([\])"'`.,/=<>\s]+$)/i); - if (orderBy) { - orderBy = orderBy[1].replace(cutOff, ''); - return select.reduce((previous, field, index) => { - if (~orderBy.indexOf(field.name)) { - const obj = { index }; - const regex = new RegExp(`${field.name}\\s+(asc|desc)(\\s|,|$)`, 'i'); - const match = orderBy.match(regex); - if (match) obj.type = match[1].toLowerCase(); - previous.push(obj); - } - return previous; - }, []); - } - return null; -} - -function getHaving(query) { - let having = query.match(/\shaving\s+([\w([\])"'`.,/=<>\s]+$)/i); - if (having) { - having = having[1].replace(cutOff, ''); - return parseFiltering(having); - } - return null; -} - -function getWhere(query) { - let where = query.match(/\swhere\s+([\w([\])"'`.,/=<>\s]+$)/i); - if (where) { - where = where[1].replace(cutOff, ''); - return parseFiltering(where); - } - return null; -} - -function getLimit(query) { - const limit = query.match(/\slimit\s+(\d+)/i); - if (limit) return +limit[1]; - return null; -} - -function getOffset(query) { - const offset = query.match(/\soffset\s+(\d+)/i); - if (offset) return +offset[1]; - return null; -} - -function parseFiltering(str) { - const filters = []; - const andOr = str.match(/\s+(and|or)\s+/gi); - if (andOr) { - let rest = str.slice(); - andOr.forEach(item => { - const part = rest.slice(0, rest.indexOf(item)); - filters.push(part); - rest = rest.slice(rest.indexOf(item) + item.length); - }); - filters.push(rest); - } else { - filters.push(str); - } - return filters.map((filter, i) => { - const obj = { operator: filter.match(/(=|<>|<|>|\slike\s)/i)[1] }; - const regex = new RegExp(`${obj.operator}([\\w([\\])"'\`.,/\\s]+)`, 'i'); - obj.exp2 = filter.match(regex)[1]; - obj.exp1 = filter.replace(obj.operator, '').replace(obj.exp2, '').replace(/\s/g, ''); - obj.operator = obj.operator.replace(/\s/g, ''); - obj.exp2 = obj.exp2.replace(/^\s+|\s+$/g, ''); - if (andOr && i < andOr.length) obj.join = andOr[i].replace(/\s/g, ''); - return obj; - }); -} - -function getPagination(query) { - return { - limit: query.limit, - page: query.skip ? query.skip / query.limit : 1, - }; -} - -function getHavingObj(query) { - const having = {}; - if (query.having) { - query.having.forEach(item => { - if (!having[item.exp1]) having[item.exp1] = []; - having[item.exp1].push({ - value: item.exp2, - joinOperator: item.join, - operator: item.operator, - }); - }); - } - return having; -} - -function getFields(query, having) { - return query.select.map((field, i) => ( - { - id: i, - name: field.name, - expression: field.expression, - grouping: field.date ? field.date : false, - sort: false, - filters: having[field.expression], - } - )); -} - -function getCollectionFields(query, fields) { - const collectionFields = {}; - if (query.where) { - query.where.forEach(item => { - const expression = item.exp1; - - if (!collectionFields[expression]) { - collectionFields[expression] = { - filters: [], - name: _.last(expression.split('.')), - constructorType: 'filters', - type: getType(expression, fields[0]), - expression, - }; - collectionFields[expression].filters = []; - } - - collectionFields[expression].filters.push({ - value: item.exp2, - joinOperator: item.join, - operator: item.operator, - }); - }); - } - return collectionFields; -} - -function getType(expression, fields) { - let currentValue = fields; - const path = expression.split('.'); - path.forEach(pathItem => { - currentValue = currentValue[pathItem]; - }); - return getItemType(currentValue); -} - -function getItemType(value) { - if (_.isNumber(value)) return 'number'; - if (_.isDate(value)) return 'date'; - if (isDate(value)) return 'date'; - if (_.isString(value)) return 'string'; - if (_.isBoolean(value)) return 'boolean'; - if (_.isArray(value)) return 'array'; - if (_.isObject(value)) return 'object'; - return null; -} - -function isDate(value) { - const dateObj = new Date(value); - return dateObj.toString() !== 'Invalid Date' && - dateObj.toISOString().replace('.000', '') === value; -} diff --git a/lib/sql_parser/index.js b/lib/sql_parser/index.js new file mode 100644 index 0000000..ae2315f --- /dev/null +++ b/lib/sql_parser/index.js @@ -0,0 +1 @@ +export * from './sql_parser'; diff --git a/lib/sql_parser/parser_utils/get_collection_fields.js b/lib/sql_parser/parser_utils/get_collection_fields.js new file mode 100644 index 0000000..ee57293 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_collection_fields.js @@ -0,0 +1,32 @@ +import { _ } from 'meteor/underscore'; +import { getType } from '../utils'; + +const getCollectionFields = (query, fields) => { + const collectionFields = {}; + if (query.where) { + // todo refactor + query.where.forEach(item => { + const expression = item.exp1; + + if (!collectionFields[expression]) { + collectionFields[expression] = { + filters: [], + name: _.last(expression.split('.')), + constructorType: 'filters', + type: getType(expression, fields[0]), + expression, + }; + collectionFields[expression].filters = []; + } + + collectionFields[expression].filters.push({ + value: item.exp2, + joinOperator: item.join, + operator: item.operator, + }); + }); + } + return collectionFields; +}; + +export { getCollectionFields }; diff --git a/lib/sql_parser/parser_utils/get_fields.js b/lib/sql_parser/parser_utils/get_fields.js new file mode 100644 index 0000000..1fec375 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_fields.js @@ -0,0 +1,14 @@ +const getFields = (query, having) => { + return query.select.map((field, i) => ( + { + id: i, + name: field.name, + expression: field.expression, + grouping: field.date ? field.date : false, + sort: false, + filters: having[field.expression], + } + )); +}; + +export { getFields }; diff --git a/lib/sql_parser/parser_utils/get_from.js b/lib/sql_parser/parser_utils/get_from.js new file mode 100644 index 0000000..86b654b --- /dev/null +++ b/lib/sql_parser/parser_utils/get_from.js @@ -0,0 +1,13 @@ +import { cutOff, getParamName, parseQueryPartial } from '../utils'; + + +const getFrom = query => { + const matchRes = query.match(/\sfrom\s+([\w([\])"'`.,/=<>\s]+$)/im); + if (!matchRes) return null; + const from = matchRes[1].replace(cutOff, ''); + const name = getParamName(from); + const path = parseQueryPartial(from, name); + return { db: path[1], collection: path[2], name: name[1] }; +}; + +export { getFrom }; diff --git a/lib/sql_parser/parser_utils/get_group_by.js b/lib/sql_parser/parser_utils/get_group_by.js new file mode 100644 index 0000000..4bf8f1d --- /dev/null +++ b/lib/sql_parser/parser_utils/get_group_by.js @@ -0,0 +1,18 @@ +import { cutOff } from '../utils'; + +const getGroupBy = (query, select) => { + let groupBy = query.match(/^\s*group by\s+([\w([\])"'`*:&;.,/\s]+)/im); + if (groupBy) { + // todo refactor + groupBy = groupBy[1].replace(cutOff, '') + .replace(/\s/g, ''); + + return select.reduce((previous, field, i) => { + if (~groupBy.indexOf(field.expression)) previous.push(i); + return previous; + }, []); + } + return null; +}; + +export { getGroupBy }; diff --git a/lib/sql_parser/parser_utils/get_having.js b/lib/sql_parser/parser_utils/get_having.js new file mode 100644 index 0000000..30c1bb1 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_having.js @@ -0,0 +1,74 @@ +import { replaceCuttOff } from '../utils'; + + +const parseFiltersToArray = (filters, andOrConditionsList) => { + let tmpFiltersLine = filters.slice(); + const filtersArray = andOrConditionsList.map(conditionItem => { + const linePart = tmpFiltersLine.slice(0, tmpFiltersLine.indexOf(conditionItem)); + tmpFiltersLine = + tmpFiltersLine.slice(tmpFiltersLine.indexOf(conditionItem) + conditionItem.length); + return linePart; + }); + filtersArray.push(tmpFiltersLine); + return filtersArray; +}; + +const divideFiltersByLogicalConditions = (str, andOrConditions) => { + if (andOrConditions) { + return parseFiltersToArray(str, andOrConditions); + } + return [str]; +}; + +const parseHavingConditions = (haveingString) => { + const andOr = haveingString.match(/\s+(and|or)\s+/gi); + const filters = divideFiltersByLogicalConditions(haveingString, andOr); + + return filters.map((filter, i) => { + const obj = { operator: filter.match(/(=|<>|<|>|\slike\s)/i)[1] }; + const regex = new RegExp(`${obj.operator}([\\w([\\])"'\`*:&;%@.,-/\\s]+)`, 'i'); + obj.exp2 = filter.match(regex)[1]; + obj.exp1 = filter.replace(obj.operator, '').replace(obj.exp2, '').replace(/\s/g, ''); + obj.operator = obj.operator.replace(/\s/g, ''); + obj.exp2 = obj.exp2.replace(/^\s+|\s+$/g, ''); + if (andOr && i < andOr.length) obj.join = andOr[i].replace(/\s/g, ''); + return obj; + }); +}; + + +const getHaving = query => { + // todo refactor + const getWordIndex = (line, word, fromPosition = 0) => { + let result = line.indexOf(word, fromPosition); + if (result === -1) { + result = line.indexOf(word.toUpperCase(), fromPosition); + } + return result; + }; + const isCountExist = (queryToCheck) => queryToCheck.match(/COUNT[(]/gi); + const removebracesWithRegex = (line) => line.replace(/[()]/g, ''); + const removeBraces = (queryPart) => { + let result = ''; + if (isCountExist(queryPart)) { + const countWordINdex = getWordIndex(queryPart, 'count('); + const afterCountIndex = getWordIndex(queryPart, ')', countWordINdex); + result += removebracesWithRegex(queryPart.substr(0, countWordINdex)); + result += queryPart.substring(countWordINdex, afterCountIndex + 1); + result += removeBraces(queryPart.substring(afterCountIndex + 1)); + } else { + result += removebracesWithRegex(queryPart); + } + return result; + }; + const having = query.match(/^\s*having\s+([\w([\])"'`*:&;.,-<>%@=\/\s]+)limit\s/im) || []; + if (!having.length) return null; + + return having + .filter((el, i) => i === 1) + .map(replaceCuttOff) + .map(removeBraces) + .map(parseHavingConditions)[0]; +}; + +export { getHaving }; diff --git a/lib/sql_parser/parser_utils/get_having_object.js b/lib/sql_parser/parser_utils/get_having_object.js new file mode 100644 index 0000000..f477417 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_having_object.js @@ -0,0 +1,17 @@ +const getHavingObj = query => { + const having = {}; + if (query.having) { + // todo refactor + query.having.forEach(item => { + if (!having[item.exp1]) having[item.exp1] = []; + having[item.exp1].push({ + value: item.exp2, + joinOperator: item.join, + operator: item.operator, + }); + }); + } + return having; +}; + +export { getHavingObj }; diff --git a/lib/sql_parser/parser_utils/get_join.js b/lib/sql_parser/parser_utils/get_join.js new file mode 100644 index 0000000..4154b29 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_join.js @@ -0,0 +1,13 @@ +import { cutOff, getParamName, parseQueryPartial } from '../utils'; + +const getJoin = query => { + const matchRes = query.match(/\sjoin\s+([\w([\])"'`.,/=<>\s]+$)/im); + if (!matchRes) return null; + // todo reafactor + const from = matchRes[1].replace(cutOff, ''); + const name = getParamName(from); + const path = parseQueryPartial(from, name); + return path[2]; +}; + +export { getJoin }; diff --git a/lib/sql_parser/parser_utils/get_limit.js b/lib/sql_parser/parser_utils/get_limit.js new file mode 100644 index 0000000..6479f03 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_limit.js @@ -0,0 +1,7 @@ +const getLimit = query => { + const limit = query.match(/\slimit\s+(\d+)/i); + if (limit) return +limit[1]; + return null; +}; + +export { getLimit }; diff --git a/lib/sql_parser/parser_utils/get_offset.js b/lib/sql_parser/parser_utils/get_offset.js new file mode 100644 index 0000000..80a7c12 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_offset.js @@ -0,0 +1,7 @@ +const getOffset = query => { + const offset = query.match(/\soffset\s+(\d+)/i); + if (offset) return +offset[1]; + return null; +}; + +export { getOffset }; diff --git a/lib/sql_parser/parser_utils/get_order_by.js b/lib/sql_parser/parser_utils/get_order_by.js new file mode 100644 index 0000000..17b4cc2 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_order_by.js @@ -0,0 +1,22 @@ +import { cutOff } from '../utils'; + +const getOrderBy = (query, select) => { + let orderBy = query.match(/\sorder by\s+([\w([\])"'`.,/=<>\s]+$)/i); + if (orderBy) { + // todo reactor + orderBy = orderBy[1].replace(cutOff, ''); + return select.reduce((previous, field, index) => { + if (~orderBy.indexOf(field.name)) { + const obj = { index }; + const regex = new RegExp(`${field.name}\\s+(asc|desc)(\\s|,|$)`, 'i'); + const match = orderBy.match(regex); + if (match) obj.type = match[1].toLowerCase(); + previous.push(obj); + } + return previous; + }, []); + } + return null; +}; + +export { getOrderBy }; diff --git a/lib/sql_parser/parser_utils/get_pagination.js b/lib/sql_parser/parser_utils/get_pagination.js new file mode 100644 index 0000000..1d9ca58 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_pagination.js @@ -0,0 +1,8 @@ +const getPagination = query => { + return { + limit: query.limit, + page: query.skip ? query.skip / query.limit : 1, + }; +}; + +export { getPagination }; diff --git a/lib/sql_parser/parser_utils/get_select.js b/lib/sql_parser/parser_utils/get_select.js new file mode 100644 index 0000000..8961e55 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_select.js @@ -0,0 +1,29 @@ +const getSelect = query => { + let select = query.match(/^\s*select\s+([\w([\])"'`*:&;.,/\s]+)from\s/i); + if (!select) return { error: 'Wrong syntax: "SELECT FROM " expected' }; + select = select[1]; + const asNames = select.match(/\s+as\s+\w+/ig); + let names = []; + let expressions = []; + if (asNames) { + names = asNames.map(item => item.replace(/(^\s+as\s|\s)+/ig, '')); + expressions = [select.slice(0, select.indexOf(asNames[0]))]; + for (let i = 0; i < asNames.length - 1; i++) { + expressions.push(select.slice(select.indexOf(asNames[i]) + asNames[i].length, + select.indexOf(asNames[i + 1]))); + } + } else { + expressions = select.match(/\w+\.\w+/ig); + if (expressions) { + names = expressions.map(exp => exp.match(/\w+\.(\w+)/i)[1]); + } else { + return { error: 'Wrong fields!' }; + } + } + return names.map((name, i) => ({ + name: names[i], + expression: expressions[i].replace(/(^,|\s)/g, ''), + })); +}; + +export { getSelect }; diff --git a/lib/sql_parser/parser_utils/get_where.js b/lib/sql_parser/parser_utils/get_where.js new file mode 100644 index 0000000..97c1cb1 --- /dev/null +++ b/lib/sql_parser/parser_utils/get_where.js @@ -0,0 +1,44 @@ +import { cutOff } from '../utils'; + +const parseFiltering = str => { + const filters = []; + const andOr = str.match(/\s+(and|or)\s+/gi); + if (andOr) { + let rest = str.slice(); + andOr.forEach(item => { + const part = rest.slice(0, rest.indexOf(item)); + filters.push(part); + rest = rest.slice(rest.indexOf(item) + item.length); + }); + filters.push(rest); + } else { + filters.push(str); + } + return filters.map((filter, i) => { + const obj = { operator: filter.match(/(=|<>|<|>|\slike\s)/i)[1] }; + const regex = new RegExp(`${obj.operator}([\\w([\\])"'\`.,/\\s]+)`, 'i'); + obj.exp2 = filter.match(regex)[1]; + obj.exp1 = filter.replace(obj.operator, '').replace(obj.exp2, '').replace(/\s/g, ''); + obj.operator = obj.operator.replace(/\s/g, ''); + obj.exp2 = obj.exp2.replace(/^\s+|\s+$/g, ''); + if (andOr && i < andOr.length) obj.join = andOr[i].replace(/\s/g, ''); + return obj; + }); +}; + +const getWhere = query => { + let where = query.match(/\swhere\s+([\w([\])"'`.,/=<>\s]+$)/im); + if (where) { + // todo refactor + where = where[1].replace(cutOff, ''); + const parsed = parseFiltering(where); + // todo refactor + parsed.forEach((item) => { + item.exp2 = item.exp2.replace(/^["]/i, '').replace(/["]$/i, ''); + }); + return parsed; + } + return null; +}; + +export { getWhere }; diff --git a/lib/sql_parser/parser_utils/index.js b/lib/sql_parser/parser_utils/index.js new file mode 100644 index 0000000..8be33de --- /dev/null +++ b/lib/sql_parser/parser_utils/index.js @@ -0,0 +1,13 @@ +export * from './get_collection_fields'; +export * from './get_fields'; +export * from './get_from'; +export * from './get_group_by'; +export * from './get_having'; +export * from './get_having_object'; +export * from './get_join'; +export * from './get_limit'; +export * from './get_offset'; +export * from './get_order_by'; +export * from './get_pagination'; +export * from './get_select'; +export * from './get_where'; diff --git a/lib/sql_parser/sql_parser.js b/lib/sql_parser/sql_parser.js new file mode 100644 index 0000000..50bba37 --- /dev/null +++ b/lib/sql_parser/sql_parser.js @@ -0,0 +1,55 @@ +import { + getSelect, + getFrom, + getHaving, + getWhere, + getLimit, + getOffset, + getJoin, + getGroupBy, + getOrderBy, + getHavingObj, + getPagination, + getFields, + getCollectionFields, +} from './parser_utils'; + +const parse = (query) => { + const objectSQL = { + select: getSelect(query), + from: getFrom(query), + having: getHaving(query), + where: getWhere(query), + limit: getLimit(query), + offset: getOffset(query), + join: getJoin(query), + }; + objectSQL.groupBy = getGroupBy(query, objectSQL.select); + objectSQL.orderBy = getOrderBy(query, objectSQL.select); + + return objectSQL; +}; + +const parseToQueryObject = (rawQuery, fields) => { + const query = parse(rawQuery); + const queryObject = {}; + const having = getHavingObj(query); + + queryObject.pagination = getPagination(query); + queryObject.fields = getFields(query, having); + + query.groupBy && query.groupBy.forEach(fieldIndex => { + queryObject.fields[fieldIndex].grouping = true; + }); + query.orderBy && query.orderBy.forEach(field => { + queryObject.fields[field.index].sort = field.type; + }); + + queryObject.collectionFields = getCollectionFields(query, fields); + + queryObject.join = query.join; + + return queryObject; +}; + +export { parseToQueryObject }; diff --git a/lib/sql_parser/utils.js b/lib/sql_parser/utils.js new file mode 100644 index 0000000..e051213 --- /dev/null +++ b/lib/sql_parser/utils.js @@ -0,0 +1,46 @@ +import { _ } from 'meteor/underscore'; + +const cutOff = + /(\sgroup by\s|\sorder by\s|\swhere\s|\shaving\s|\slimit\s|\soffset\s)+[\w([\])"'`.,/=<>\s]+$/i; + +const replaceCuttOff = string => string.replace(cutOff, ''); + +const getParamName = (queryPartial) => queryPartial.match(/\s+as\s+(\w+)/i); + +const parseQueryPartial = (partial, paramName) => + partial.slice(0, paramName.index).match(/(\w+)\/(\w+)/i); + +const isDate = value => { + const dateObj = new Date(value); + return dateObj.toString() !== 'Invalid Date' && + dateObj.toISOString().replace('.000', '') === value; +}; + +const getItemType = value => { + if (_.isNumber(value)) return 'number'; + if (_.isDate(value)) return 'date'; + if (isDate(value)) return 'date'; + if (_.isString(value)) return 'string'; + if (_.isBoolean(value)) return 'boolean'; + if (_.isArray(value)) return 'array'; + if (_.isObject(value)) return 'object'; + return null; +}; + +const getType = (expression, fields) => { + let currentValue = fields; + const path = expression.split('.'); + // todo refactor + path.forEach(pathItem => { + currentValue = currentValue[pathItem]; + }); + return getItemType(currentValue); +}; + +export { + cutOff, + replaceCuttOff, + getParamName, + parseQueryPartial, + getType, +};