json-sql-builder
Version:
SQLBuilder to translate JSON dataformat like mongo to SQL
1,245 lines (1,135 loc) • 56.4 kB
JavaScript
'use strict';
const _ = require('lodash');
const SQLQuery = require('./sqlquery');
const pgFormat = require('pg-format');
function loadLanguageModule(language){
try {
return require('./' + language);
} catch(e) {
return undefined;
}
}
const loadHelpers = {
ansi: loadLanguageModule('ansi'),
mysql: loadLanguageModule('mysql'),
postgreSQL: loadLanguageModule('postgreSQL'),
sqlite: loadLanguageModule('sqlite')
};
/**
* @before
* # Build Queries
*
* With the NPM package `json-sql-builder` you can build each query you need to run on your database.
*
* <div class="sub-title">
* Detailed documentation of all available methods and options
* </div>
*
* @namespace SQLBuilder
* @summary Main Api to build queries.
* @hide true
*/
class SQLBuilder {
/**
* @summary Creates a new instance of the SQLBuilder.
*
* @param {String} language
* Specifies the language. If theres was no parameter provided only the ANSI Standards will be loaded.
* - mysql
* - postgreSQL
*
* @return {SQLBuilder} New instance of the SQLBuilder
*/
constructor(language) {
this.mainOperator = null;
this.quoteChar = '`';
this.wildcardChar = '%';
this._syntax = {};
this._helpers = {};
// new helpers table since v2.0.0
this._helpers2 = {};
this._recursions = 0;
this.supportedSQLDialects = {
ansi: true,
mysql: true,
postgreSQL: true,
oracle: true,
mssql: true,
sqlite: true,
maria: true
}
this._initBuilder();
this.sqlDialect = language;
// always load ANSI SQL standards
loadHelpers.ansi(this);
// use a specific language or only standard?
if (language){
// check if the specific helper-module for the language is installed
// if yes -> run it, otherwise error
if (loadHelpers[language]) {
loadHelpers[language](this);
} else {
throw new Error('Language extension \'' + language + '\' is not available.');
}
}
}
_initBuilder(){
this.mainOperator = null;
this._recursions = 0;
this._sql = '';
this._values = [];
this._helperChain = [];
this._logicalJoiner = [];
this._stripMostOuterParentheses = true;
}
/**
* @summary Builds the given query and returns an new SQLQuery object.
* @memberof SQLBuilder
*
* @param {Object} query Specifies the JSON query-object.
* @param {String} [identifier] **Optional.** Specifies the identifier detected before.
* @param {String} [syntax] **Optional.** Specifies the Syntax that the SQLBuilder has to use for this query.
* @param {String} [stripMostOuterParentheses] **Optional.** Specifies whether the most outer parentheses should be stripped before final return or not. Default is **true**
* @param {String} [joinWith] **Optional.** Specifies the String to concat the results of each operator, helper after processing them. Default is ' '.
* @return {SQLQuery}
*/
build(query, identifier, syntax, stripMostOuterParentheses, joinWith){
var result = '';
// if the build was started more than once with different queries
// we have to reset the _sql, _values and _helperChain
if (this._recursions == 0) {
this._initBuilder();
}
this._recursions++;
// set the global - stripping outer parentheses
if (typeof stripMostOuterParentheses !== typeof undefined) {
this._stripMostOuterParentheses = stripMostOuterParentheses;
}
if (!joinWith) joinWith = ' ';
// check if we have a syntax
if (syntax){
var items = syntax.match(/(<\$\w+>)|(\[\$\w+\])/g);
items = items.map(function(item) {
return {
name: item.replace('[', '').replace(']', '').replace('<', '').replace('>', ''),
required: item.startsWith('<'),
};
});
// iterate like the specific syntax
var syntaxResults;
syntaxResults = items.map((helper) => {
// check if the helper is registered and the query has this helper as property
if (this._helpers[helper.name] && query[helper.name]) {
return this.callHelper(helper.name, query[helper.name], query, identifier);
} else if (helper.required && !query[helper.name]) {
throw new Error('Required expression missing: ' + helper.name + '.');
} else if (query[helper.name] && !this._helpers[helper.name]) {
throw new Error('Unknown expression/operator detected: ' + helper.name + '.');
} else {
return '';
}
});
// concat each valid result with a starting ' '
_.forEach(syntaxResults, function(value){
if (value && value.length > 0) {
result += (result.length > 0 ? joinWith : '') + value;
}
});
} else {
var results = [];
// there is no syntax, so we iterate each item as it is
for (var key in query){
// check if we know the helper -> each helper starts with $
if (key.startsWith('$') && this._helpers[key]) {
results.push(this.callHelper(key, query[key], query, identifier));
} else {
// there was no helper detected or the key did not startwith '$'
// check if it didnt start with $, so we have an identifier as key
if (!key.startsWith('$')) {
// identifier detected -> example "first_name: 'John'" or "first_name:{$eq:'John'}"
if (_.isPlainObject(query[key])){
// "first_name:{$eq:'John'}"
results.push(this.build(query[key], key));
} else if (_.isString(query[key]) || _.isNumber(query[key]) || _.isBoolean(query[key])){
// "first_name: 'John'"
results.push(this.quote(key) + ' = ' + this.addValue(query[key]));
} else {
throw new Error('Unknown expression/operator detected: ' + key);
}
} else {
// unknown helper
throw new Error('Unknown expression/operator detected -> ' + key);
}
}
}
result = results.join(joinWith);
}
this._recursions--;
if (this._recursions == 0) {
if (this._stripMostOuterParentheses){
// we are finished and remove the outer most parentheses
var result = result.startsWith('(') && result.endsWith(')') ? result.substring(1, result.length - 1) : result;
}
this._sql = result;
return {
sql: result,
values: this._values
}//new SQLQuery(result, this._values);
}
return result;
}
/**
* @summary Quotes the given identifier with the quote-character defined for the specific SQL language dialect.
* @memberof SQLBuilder
*
* @after
* # Quote Identifiers
*
* If you are creating your own helpers and operators you have to quote the generated identifiers.
* For this you can use the standard method that will do the job for you.
*
* If you are passing only one identifier to the method you will receive the the identifier as quoted string.
* On passing two identifiers you will receive the quoted identifiers joined with a dot '.' like `table.column`.
*
* In exception to this a column-identifier with the value ` * ` or `ALL` will returned as unquoted string.
* Also all variable identifiers that starts with ` @ ` will be leave unquoted.
*
* @param {String} column Specifies the main identifier to quote. Normally it will be a column or an alias name.
* @param {String} [table] Optional. Specifies the table-identifier.
*
* @return {String} Quoted identifier like `table`.`column`
*/
quote(column, table){
if (!_.isString(column)) {
throw new Error('Using quoted identifiers - arguments provided must always be type of String, but got arg1="' + typeof column + '"')
}
if (table && !_.isString(table)) {
throw new Error('Using quoted identifiers - arguments provided must always be type of String, but got arg2="' + typeof column + '"')
}
// do not quote identifiers starts with '@'
// SELECT `first_name` INTO @firstname FROM `people`
if (column.startsWith('@')){
return column;
}
// maybe the column includes the table name or the column arg is tebl schema and table
// like <table>.<column> or <schema>.<table>
// so we have to replace the "." and quote it correctly before and after the quote
column = column.split('.').join(this.quoteChar + '.' + this.quoteChar);
if (table){
return this.quoteChar + table + this.quoteChar + '.' + (column === '*' || column === 'ALL' ? column : this.quoteChar + column + this.quoteChar);
} else {
return (column === '*' || column === 'ALL' ? column : this.quoteChar + column + this.quoteChar);
}
}
/**
* @summary Specifies the placeholder function for the ANSI SQL Standard. This function can be overwrite by any SQL dialect loaded on instancing the builder.
* @memberof SQLBuilder
*
* @return {String} placeholder
*/
placeholder(){
return '?';
}
/**
* @summary Specifies the 'AS' clause for a table or column or expression.
* @memberof SQLBuilder
*
* @param {String} [identifier] Optional. Specifies the identifier.
* @return {String} ' AS <quoted-identifier>' or empty String '' depends on a valid identifier given by argument
*/
aliasIdent(identifier) {
return (identifier ? ' AS ' + this.quote(identifier) : '');
}
/**
* @summary Adds the given value to the current value stack and returns the language specific placeholder as string.
* @memberof SQLBuilder
*
* @param {Primitive} val Specifies the value to add.
*/
addValue(val){
// postgreSQL does not support parameterized values for create statements like CREATE TABLE
if (this.sqlDialect == 'postgreSQL' && this.isCurrent('$create')) {
if (_.isNumber(val)) {
return val;
} else if (_.isString(val)) {
if (val.startsWith('~~')){
return this.quote(val.substring(2));
}
return pgFormat('%L', val);
} else if (_.isBoolean(val)) {
return val ? 'TRUE' : 'FALSE';
} else {
return val;
}
}
// check a shotcut for identifiers
if (_.isString(val) && val.startsWith('~~')) {
return this.quote(val.substring(2));
}
this._values.push(val);
return this.placeholder();
}
/**
* @summary Calls the given Helper by name. The Helper will be executed in the context of the current SQLBuilder instance.
* @memberof SQLBuilder
*
* @param {String} name Specifies the name of the Helper / Operator.
* @param {Object} query Specifies the query-object that should be translated to sql by the specified Helper.
* @param {Object} [outerQuery] Optional. Specifies the outer object from the given query.
* @param {String} [identifier] Optional. Specifies the current available identifier.
*
* @return {String} Returns the translated SQL code from the given query.
*/
callHelper(name, query, outerQuery, identifier){
this._helperChain.push(name);
var result = this._helpers[name].fn.call(this, query, outerQuery, identifier);
this._helperChain.pop();
return result;
}
/**
* @summary Checks if the given Helper or Operator is on the current Path. If is currently in use the function returns true, otherwise false.
* @memberof SQLBuilder
*
* @param {String} name Specifies the name of the Helper or Operator
* @return {Boolean}
*/
isCurrent(name) {
for (var i=0, max=this._helperChain.length; i<max; i++){
if (this._helperChain[i] == name) {
return true;
}
}
return false;
}
/**
* @after
*
* # Register a new Syntax
*
* If you are creating a new Helper, Operator for the SQLBuilder you may need a Syntax for the build-method.
*
* By using a pre-defined Syntax you achive that the build method will output the
* expected SQL code and detect optional and required helper/operators. On top of this the order of the
* object properties inside the query does not matter.
*
* ## Required helpers/operators
* To define a helper, operator as required you have to use pointed brackets ` < ... > `.
*
* ## Optional helpers/operators
* To define an optional helper, operator you have to use square brackets ` [ ... ] `.
*
* **Example `$select` - Syntax**
* ```javascript
* // ANSI-SELECT Statement Syntax
* sqlBuilder.registerSyntax('$select', `SELECT [$distinct] [$all]
* <$columns>
* { FROM [$table] | [$from] }
* { WHERE [$where] }
* { GROUP BY [$groupBy]
* { HAVING [$having] }
* }
* { ORDER BY { [$sort] | [$orderBy] } }`);
*
* ```
* > **Remarks**
*
* > At this time only the optional and required helpers will be detected. Everything around this will be ignored.
* >
* > In a future version we will use the whole syntactical meanings including the key-words the curly brackets an so on.
* > So If you create a new Syntax please provide a Syntax with all the stuff seen above.
*
* @summary Register a new Syntax to use later on the build process.
* @memberof SQLBuilder
*
* @param {String} name Specifies the name of the Syntax.
* @param {String} syntax Specifies the Syntax. See the example below.
*/
registerSyntax(name, syntax){
if (_.isString(syntax)) {
// check if a syntax with this name already exists
if (this._syntax[name]) {
throw new Error('Can\'t register new Syntax \'' + name + '\'. A Syntax with this name already exists.');
}
this._syntax[name] = syntax;
} else if (_.isPlainObject(syntax)) {
this._registerHelperBySyntax(name, syntax);
} else {
throw new Error('Can\'t register new Syntax \'' + name + '\'. The Syntax must be described as String or Object. Please refer the docs.');
}
}
_registerHelperBySyntax(name, helperDefinition) {
this._helpers2[name] = helperDefinition;
let checkQueryType = function(query, allowedTypes, eachItemOfParentType) {
let validType;
_.forEach(allowedTypes, (value, key) => {
let validateType = 'is' + key.replace('Object', 'PlainObject');
if (_[validateType](query)) validType = key;
});
if (!validType) {
if (eachItemOfParentType){
throw new Error('Using Helper ' + name + ' must be type of ' + eachItemOfParentType + '->' + Object.keys(allowedTypes).join(', ') + ' but got "' + typeof query + '" with value \'' + (_.isPlainObject(query) ? JSON.stringify(query):query) + '\'');
} else {
throw new Error('Using Helper ' + name + ' must be type of ' + Object.keys(allowedTypes).join(', '));
}
}
return validType;
}
// Build the given query and return the result as object with all values
//
// Let's say we have a query: { $exression: '~~first_name', $delimiter: ', ' }
// and a given syntax like: "string_agg(<$expression>, <$delimiter>) [AS <identifier>]"
// The Result after the build has processed is:
// {
// $expression: '"first_name"',
// $delimiter: '$1'
// }
//
let _buildQuery = (query, registeredHelpers, identifier) => {
var results = {};
// check if we got an helper, that is not registered
// we have to report this as Error
_.forEach(query, (value, name) => {
// no identifiers allowed on query
if (!name.startsWith('$')) {
throw new Error ('Execute Query ' + JSON.stringify(query) + ' Identifier with Name "' + name + '" detected. Please check your query.');
}
if (!registeredHelpers[name]){
throw new Error ('Execute Query ' + JSON.stringify(query) + ' the Helper with the Name "' + name + '" is not permitted by Syntax.');
}
});
_.forEach(registeredHelpers, (queryOrValue, helperOrIdentifier) => {
// $select: {...} => $select operator/helper
// my_column: { ... } => Identifier
let isIdentifier = !helperOrIdentifier.startsWith('$');
if (!isIdentifier) {
// check if helper is available on query
if (!query[helperOrIdentifier]) return;
// check if the helper is available in the registeredHelpers
if (registeredHelpers[helperOrIdentifier]) {
// okay, the helper exists and we can execute him
results[helperOrIdentifier] = this.callHelper(helperOrIdentifier, query[helperOrIdentifier], query /*outer Query*/);
} else {
// helper does not exists in the registered Helpers
// given from the syntax declaration
throw new Error ('Execute Query ' + JSON.stringify(query) + ' the helper or operator with the Name "' + helperOrIdentifier + '" is not permitted by Syntax.');
}
} else {
// Identifier!
// so we have to execute the inner query of the identifier and pass
// the identifier to the next level
//this.build(queryOrValue, helperOrIdentifier);
}
});
/*_.forEach(query, (queryOrValue, helperOrIdentifier) => {
// $select: {...} => $select operator/helper
// my_column: { ... } => Identifier
let isIdentifier = !helperOrIdentifier.startsWith('$');
if (!isIdentifier) {
// check if the helper is available in the registeredHelpers
if (registeredHelpers[helperOrIdentifier]) {
// okay, the helper exists and we can execute him
results[helperOrIdentifier] = this.callHelper(helperOrIdentifier, queryOrValue, query);
} else {
// helper does not exists in the registered Helpers
// given from the syntax declaration
throw new Error ('Execute Query ' + JSON.stringify(query) + ' the helper or operator with the Name "' + helperOrIdentifier + '" is not permitted by Syntax.');
}
} else {
// Identifier!
// so we have to execute the inner query of the identifier and pass
// the identifier to the next level
this.build(queryOrValue, helperOrIdentifier);
}
});*/
return results;
}
let _getHelperName = function(helper) {
return (
helper.replace('[', '')
.replace(']', '')
.replace('<', '')
.replace('>', '')
.replace('[ ', '')
.replace(' ]', '')
.replace('< ', '')
.replace(' >', '')
);
};
let _getHelperNameByToken = function(token, registeredHelpers) {
var map = Object.keys(registeredHelpers);
for (var i=0, max=map.length; i<max; i++) {
if (registeredHelpers[map[i]].token == token){
return map[i];
}
}
}
let _removeHelperByToken = function(token, registeredHelpers) {
var map = Object.keys(registeredHelpers);
for (var i=0, max=map.length; i<max; i++) {
if (registeredHelpers[map[i]].token == token){
delete registeredHelpers[map[i]];
}
}
}
let _isHelperRequired = function (helper) {
return helper.startsWith('<');
}
// translate the given syntax and return a
// translated object with a cleaned up Syntax
// and all registered helpers and operators
let _translate = (syntax) => {
// prepare the result for each type defined
let uniqueHelperID = 0;
let results = {
cleanedSyntax: '',
registeredHelpers: {},
joinedWith: null
};
// first of all get a possible joiner
// the notation of this is [ <joiner>... ]
let joiner = syntax.match(/\[ (.*)\.\.\.\ \]/g);
results.joinedWith = joiner && joiner.length > 0 ? joiner[0].substring(2, joiner[0].length - 5) : null;
if (results.joinedWith) {
// remove the joiner
syntax = syntax.replace(/\[ (.*)\.\.\.\ \]/g, '');
}
// get all required and optional helpers and operators
// defined by "<...>" and "[...]"
var helpers = syntax.match(/(<\$\w+>)|(\[\$\w+\])/g);
_.forEach(helpers, function(helper){
let helperName = _getHelperName(helper);
if (!results.registeredHelpers[helperName]) {
let uid = ++uniqueHelperID;
results.registeredHelpers[helperName] = {
id: uid,
// a unique token as replacement for the current helper
// so later it will be easier to remove all remaining
// white-spaces between the helpers
token: '>->->' + uid + '<-<-<',
definition: helper, // store original helper definition for a later replacement
required: _isHelperRequired(helper),
supportedBy: {
// list of all rdbms that support the current helper
}
}
}
// replace the original helper defined with a unique ID by it's token
syntax = syntax.replace(helper, results.registeredHelpers[helperName].token);
});
// cleanup of all tabs, newlines and carrige return's
syntax = syntax.replace(/(\t|\n|\r)/g, '');
// remove all white-spaces between the tokens
syntax = syntax.replace(/<-<-<\s+>->->/g, '<-<-<>->->');
// remove every "or" defined with "|" between two tokens
// example: { ORDER BY [$sort] | [$orderBy] }
syntax = syntax.replace(/<-<-< \| >->->/g, '<-<-<>->->');
// get all items located in curly braces like "{ FROM >->->1<-<-< }"
// and register the subsyntax
var curlyItems = syntax.match(/\{([^}]+)\}/g);
_.forEach(curlyItems, function(curlyItem) {
// get all optional and required operators and helpers
var tokens = curlyItem.match(/(>->->[0-9]+<-<-<)/g);
_.forEach(tokens, function(token) {
let name = _getHelperNameByToken(token, results.registeredHelpers);
// ignore sub-syntax for {* ... *}
// example: {* INNER | LEFT OUTER | RIGHT OUTER JOIN [$joins] *}
// This is only for a better understanding of the total syntax,
// but the INNER JOIN, etc. will defined in the helper '$innerJoin'
if (! (curlyItem.startsWith('{*') && curlyItem.endsWith('*}'))) {
results.registeredHelpers[name].subSyntax = curlyItem.substring(1, curlyItem.length - 1); // remove outer curly braces
}
// replace the curly braces optional helper with the
// 'real'Helper to get a very clean syntax
// curlyItem = "{ FROM [ $from ] }"
// item = "[ $ from ]"
syntax = syntax.split(curlyItem).join(token);
});
});
// Example-Syntax after main cleanup:
// SELECT >->->1<-<-<-->(mssql) >->->2<-<-< >->->3<-<-<-->(mysql) >->->4<-<-<>->->5<-<-<>->->6<-<-<>->->7<-<-<>->->8<-<-<>->->9<-<-<>->->10<-<-<>->->11<-<-<>->->13<-<-<-->(mysql,postsgreSQL)>->->14<-<-<-->(mysql,postsgreSQL
// Now check for language specific helpers and operaotrs
// and remove them if they are not equal to the current
// sql dialect
var dialectHelpers = syntax.match(/(>->->[0-9]+<-<-<)-->\([\w,]+\)/g)
_.forEach(dialectHelpers, (helper) => {
let token = helper.substring(0, helper.indexOf('-->('));
if (helper.indexOf(this.sqlDialect) == -1) {
_removeHelperByToken(token, results.registeredHelpers);
syntax = syntax.replace(helper, '');
} else {
// remove the dialect-info "-->(mysql,postgreSQL)"
let dialectInfo = helper.substring(helper.indexOf('-->('), helper.length);
syntax = syntax.replace(dialectInfo, '');
}
});
// remove every "or" defined with "|" between two tokens
// example: { ORDER BY [$sort] | [$orderBy] }
syntax = syntax.replace(/<-<-< \| >->->/g, '<-<-<>->->');
// remove all white-spaces between the tokens
syntax = syntax.replace(/<-<-<\s+>->->/g, '<-<-<>->->');
// remove all white-spaces if there are more then one
// and replace with ' '
syntax = syntax.replace(/\s+/g, ' ');
results.cleanedSyntax = syntax;
return results;
}
/* translate the given syntax in a more clean way like the example:
* syntax: {
allowedTypes: {
Object: `SELECT [$distinct] [$all] <$columns>
{ FROM [$from] }
{* INNER | LEFT OUTER | RIGHT OUTER JOIN [$joins] *}
{ WHERE [$where] }
{ GROUP BY [$groupBy] }
{ HAVING [$having] }
{ ORDER BY [$sort] | [$orderBy] }`,
},
translated: {
Object: {
cleanedSyntax: 'SELECT [$distinct] [$all] <$columns> [$from] [$joins] [$where] [$groupBy] [$having] [$sort] [$orderBy]'
registeredHelpers: {
$distinct: { id: 1, token: '>->->1<-<-<', required: false },
$all: { id: 2, token: '>->->2<-<-<', required: false },
$columns: { id: 3, token: '>->->3<-<-<', required: true },
$from: { id: 4, token: '>->->4<-<-<', required: false, subSyntax: 'FROM [$from]'},
$joins: { id: 5, token: '>->->5<-<-<', required: false },
$where: { id: 6, token: '>->->6<-<-<', required: false, subSyntax: 'WHERE >->->6<-<-<'},
$sort: { id: 7, token: '>->->7<-<-<', required: false, subSyntax: 'GROUP BY >->->7<-<-<>->->8<-<-<'}
$groupBy: { id: 8, token: '>->->8<-<-<', required: false, subSyntax: 'GROUP BY >->->7<-<-<>->->8<-<-<'}
}
}
},
belongsTo: {
Any: true
},
dependsOn: null
},*/
let _translateSyntax = (definition) => {
let results = {};
_.forEach(definition.allowedTypes, (typeDef, type) => {
if (!typeDef.syntax && typeDef.eachItemOf) {
// we have an Object or Array type with a
// objected syntax-declaration like:
//
// sqlBuilder.registerSyntax('$from', {
/* description: 'Specifies the `FROM` clause for the `SELECT` Statement.',
supportedBy: {
mysql: 'https://dev.mysql.com/doc/refman/5.7/en/select.html',
postgreSQL: 'https://www.postgresql.org/docs/9.5/static/sql-select.html'
},
definition: {
allowedTypes: {
------> Object: {
eachItemOf: {
Boolean: {
syntax: {
true: '<key-ident> [ , ... ]',
false: ''
}
},
String: { syntax: { '<value> AS <key>' [, ...] } },
Object: { syntax: { '<value> AS <key>' [, ...] } },
}
},
String: '<value>',
},
belongsTo: {
$select: true
},
dependsOn: {
$select: true
}
},
*/
_.forEach(typeDef.eachItemOf, (typeDef, itemType) => {
// inject the translated SyntaxObject to the type-definition for String, Number, Object
// For type Boolean we have the possibility to define a syntax object with two
// different syntax-style depend on the bool value true or false
if (_.isPlainObject(typeDef.syntax)) {
// syntax is an Object, so we have a value-based Syntax like:
/* allowedTypes: {
Object: {
eachItemOf: {
Boolean: {
syntax: {
true: '<key-ident>[ , ... ]',
false: ''
}
},
*/
// so iterate each value and translate it's syntax
definition.allowedTypes[type].eachItemOf[itemType].translated = {};
_.forEach(typeDef.syntax, (valueBasedSyntax, key)=>{
definition.allowedTypes[type].eachItemOf[itemType].translated[key] = _translate(valueBasedSyntax);
});
} else {
// we have subItem-based Type-Syntax like:
/* allowedTypes: {
Object: {
eachItemOf: {
String: { syntax: '<key-ident> AS <value-ident>[ , ... ]' },
Object: { syntax: '<value> AS <key-ident>[ , ... ]' },
},
*/
definition.allowedTypes[type].eachItemOf[itemType].translated = _translate(typeDef.syntax);
}
});
} else {
// we have a simple Type-Based syntax like:
/* definition: {
allowedTypes: {
Object: {
syntax: `SELECT [$distinct] [$all] <$columns>
{ FROM [$from]}
{* INNER | LEFT OUTER | RIGHT OUTER JOIN [$joins] *}
{ WHERE [$where] }
{ GROUP BY [$groupBy] }
{ HAVING [$having] }
{ ORDER BY [$sort] | [$orderBy] }`
},
String: {
syntax: {
ALL: 'my value-based syntax '
}
}
}
*/
// check for a value-based Syntax
if (_.isPlainObject(definition.allowedTypes[type].syntax)) {
// so iterate each value and translate it's syntax
definition.allowedTypes[type].translated = {};
_.forEach(definition.allowedTypes[type].syntax, (valueBasedSyntax, key)=>{
definition.allowedTypes[type].translated[key] = _translate(valueBasedSyntax);
});
} else {
// inject the translated SyntaxObject to the type-definition
definition.allowedTypes[type].translated = _translate(typeDef.syntax);
}
}
});
return results;
}
let _aliasIdent = (identifier) => {
return (identifier ? 'AS ' + this.quote(identifier) : '');
}
/**** Internal
* Replacing the rplv's (replacment-values) in the
* given Syntax string with real values
*
* Pass the rplv as Object like:
* {
* key: "any key value" // means the key or property of an object
* value: "any value" // Specifies the value of any Primitiv or the value of and object with it's vaue as primitiv
* identifier: "string" // Specifies a detected ientifier from the previous build-process
* result: "string" // Specifies the SQL-result of the build-process
* }
*
* @param syntax {String} Specifies the Syntax a string to replace wit current values
* @param rplv {Object} Specifies the values to use on replace
* @param [rplv.key] {Primitiv} Value to replace on <key>, <key-param> or <key-ident>
* @param [rplv.value] {Primitiv} Value to replace on <value>, <value-param> or <value-ident>
* @param [rplv.identifier] {String} Value to replace on <identifier>
* @param [rplv.result] {String} Value to replace on <result> given from the internal build-process
*
* @return {String} replaced Syntax-String
*/
let _replaceSyntaxWithValues = (syntax, rplv) => {
// change undefined to a null value
// for the database undefined means "NULL"
rplv.key = typeof rplv.key === typeof undefined ? null : rplv.key;
rplv.value = typeof rplv.value === typeof undefined ? null : rplv.value;
rplv.identifier = typeof rplv.identifier === typeof undefined ? null : rplv.identifier;
//if (typeof rplv.key !== typeof undefined) {
if (syntax.indexOf('<key>') > -1) {
syntax = syntax.replace('<key>', rplv.key);
}
if (syntax.indexOf('<key-param>') > -1) {
syntax = syntax.replace('<key-param>', this.addValue(rplv.key));
}
if (syntax.indexOf('<key-ident>') > -1) {
syntax = syntax.replace('<key-ident>', this.quote(rplv.key));
}
//}
//if (typeof rplv.value !== typeof undefined) {
if (syntax.indexOf('<value>') > -1) {
syntax = syntax.replace('<value>', rplv.value);
}
if (syntax.indexOf('<value-param>') > -1) {
syntax = syntax.replace('<value-param>', this.addValue(rplv.value));
}
if (syntax.indexOf('<value-ident>') > -1){
syntax = syntax.replace('<value-ident>', this.quote(rplv.value));
}
//}
//if (typeof rplv.identifier !== typeof undefined){
if (syntax.indexOf('<identifier>') > -1){
syntax = syntax.replace('<identifier>', this.quote(rplv.identifier));
}
//}
return syntax;
}
// calling the linkerHook to change the query or make magic things
// The Linker will call the nested operators linkerHook before
// the current query will be performed, against the the beforeExecute hook
// that will be called for the current query.
// the changed query will be returned as deep-clone
let _linker = (query, outerQuery) => {
let _query = _.cloneDeep(query);
if (_.isPlainObject(_query)){
// checking each helper, operator in this query
// if there is a linkHoo defined for and call it
// to give a chance to change the query before anything
// will do with it
_.forEach(_query, (value, helperName) => {
if (helperName.startsWith('$') && this._helpers2[helperName]) {
let hooks = this._helpers2[helperName].hooks;
if (hooks && _.isFunction(hooks.link)) {
hooks.link.call(this, _query, outerQuery);
}
}
});
}
return _query;
}
let _helper = (query, outerQuery, identifier) => {
query = _linker(query, outerQuery);
// checking allowed query-types
// and throw an Error if we got another type as allowed by helperDefinition
var queryType = checkQueryType(query, helperDefinition.definition.allowedTypes),
currentAllowedType = helperDefinition.definition.allowedTypes[queryType],
currentSyntax = currentAllowedType.translated && currentAllowedType.translated.cleanedSyntax,
joinedWith = currentAllowedType.translated && currentAllowedType.translated.joinedWith;
// check for a value-based-syntax for "normal" use of Types
// that means not ! eachItemOf-Type-declaration
if (_.isPlainObject(helperDefinition.definition.allowedTypes[queryType].syntax)) {
// it's a value based syntax
// just overwrite the syntax with the value-based
currentSyntax = currentAllowedType.translated[query] && currentAllowedType.translated[query].cleanedSyntax;
}
// check for before-hook and call if available
if (helperDefinition.hooks && _.isFunction(helperDefinition.hooks.beforeExecute)){
query = helperDefinition.hooks.beforeExecute.call(this, query, {
queryType: queryType,
sqlDialect: this.sqlDialect,
outerQuery: outerQuery,
identifier: identifier
});
}
// execute the syntax of the given type
switch (queryType) {
case 'Number':
case 'Boolean':
case 'String':
currentSyntax = _replaceSyntaxWithValues(currentSyntax, {
value: query
});
break;
case 'Object':
case 'Array':
// check if we got a valid syntax or
// if there is a definition for eachItemOf
if (!currentSyntax) {
if (!currentAllowedType.eachItemOf) {
throw new Error ('Execute query: ' + JSON.stringify(query) + ' There is no "syntax" property nor "eachItemOf" to work with.')
}
let resultsOnIterate = '';
// okay we have to iterate as told by property eachItemOf
_.forEach(query, (value, key) => {
let itemType = checkQueryType(value, currentAllowedType.eachItemOf, queryType);
let translated = currentAllowedType.eachItemOf[itemType].translated;
let iteratingSyntax,
joinedWith;
// check for cleanedSyntax to decide if we have a eachItemOf Type-base Syntax
// or we have a value base Syntax
if (!translated.cleanedSyntax) {
// we have a value-based Syntax --> get the current value and
// let's hope we have a syntax for that value
if (!translated[value]) {
throw new Error (`The value '${value}' using Type '${queryType}->${itemType}' on Helper or Operator '${name}' is not allowed. Possible values are '${Object.keys(translated)}'.`);
}
iteratingSyntax = translated[value].cleanedSyntax;
joinedWith = translated[value].joinedWith || '';
} else {
iteratingSyntax = translated.cleanedSyntax;
joinedWith = translated.joinedWith || '';
}
if (itemType == 'Boolean' || itemType == 'String' || itemType == 'Number') {
resultsOnIterate += (resultsOnIterate == '' ? '' : joinedWith);
resultsOnIterate += _replaceSyntaxWithValues(iteratingSyntax, {
key: key,
value: value
});
} else if (itemType == 'Function') {
let functionResult = value.call(this);
resultsOnIterate += (resultsOnIterate == '' ? '' : joinedWith);
resultsOnIterate += _replaceSyntaxWithValues(iteratingSyntax, {
key: key,
value: functionResult
});
} else if (itemType == 'Object') {
if (Object.keys(translated.registeredHelpers).length == 0) {
// okay, here we've got an Object from it's parent -> Object or Array
// which has no Syntax-helpers like $select or $distinct...
// it's a plain syntax with <value> <key-ident>[ , ... ]
// so we iterate the object and see what comes next :-)
// it can be anything! maybe { first_name: { $gt: 18 } }
let buildUnknown = (query, joinedWith) => {
let results = [];
_.forEach(query, (innerQuery, key) => {
// check if we have an identifier or operator/helper
if (key.startsWith('$')) {
// key is an helper/operater
// callHelper(name, query, outerQuery, identifier){
let result = this.callHelper(/*name*/key, /*query*/innerQuery, /*outerquery*/query /*, identifier*/);
results.push(result);
} else {
// key is an identifier
// if the value of the identifier is a primitiv (String, Number, Boolean)
// then we are using temp $eq
if (_.isString(innerQuery) || _.isNumber(innerQuery) || _.isBoolean(innerQuery)) {
results.push(this.quote(key) + ' = ' + this.addValue(innerQuery));
} else if (_.isPlainObject(innerQuery)) {
let bu = buildUnknown(innerQuery, '++');
results.push(this.quote(key) + ' ' + bu);
} else if (_.isArray(query)) {
throw new Error('Type Array inside unknown query detected!');
} else {
throw new Error('Unknown Type inside unknown query detected!');
}
}
});
return results.join(joinedWith);
}
// after getting the results, just put them
// into the syntax-template for iterating
let retval = buildUnknown(value, joinedWith);
resultsOnIterate += (resultsOnIterate == '' ? '' : joinedWith);
resultsOnIterate += _replaceSyntaxWithValues(iteratingSyntax, {
key: key,
value: retval
});
/*let buildResult = this.build(value, key);
resultsOnIterate += (resultsOnIterate == '' ? '' : joinedWith);
resultsOnIterate += _replaceSyntaxWithValues(iteratingSyntax, {
key: key,
value: buildResult
});*/
} else {
let buildResult = _buildQuery(value, translated.registeredHelpers, iteratingSyntax.indexOf('[AS <identifier>]') > -1 ? undefined:identifier);
// after building each part of the query, we have
// to replace the placeholders in the syntax with the
// current result of the buildQuery func
_.forEach(buildResult, (result, helperName) => {
let helper = translated.registeredHelpers[helperName];
// do we have a subSyntax declared in curly braces?
if (helper.subSyntax) {
let rplSubSyntax = helper.subSyntax.replace(helper.token, result);
currentSyntax = currentSyntax.replace(helper.token, rplSubSyntax);
} else {
currentSyntax = currentSyntax.replace(helper.token, result)
}
});
resultsOnIterate += (resultsOnIterate == '' ? '':translated.joinedWith) +
iteratingSyntax.replace('<value>', this.addValue(value));
}
} else if (itemType == 'Array') {
throw new Error(`Type Array is not supported for ${queryType}.eachItemOf`);
} else {
throw new Error(`Unknown itemType '${itemType}' on iterating current Object using Object.eachItemOf`);
}
});
currentSyntax = resultsOnIterate;
} else {
if (queryType == 'Object') {
// no iteration, just build and see what's happening :-)
if (Object.keys(currentAllowedType.translated.registeredHelpers).length == 0) {
let buildResult = this.build(query/*value*/);
currentSyntax = _replaceSyntaxWithValues(currentSyntax, {
value: buildResult
});
} else {
let buildResult = _buildQuery(query, currentAllowedType.translated.registeredHelpers, currentSyntax.indexOf('[AS <identifier>]') > -1 ? undefined:identifier);
// after building each part of the query, we have
// to replace the placeholders in the syntax with the
// current result of the buildQuery func
_.forEach(buildResult, (result, helperName) => {
if (result == '') return;
if (result.startsWith('-->Accepted->Return:')) {
result = result.substring('-->Accepted->Return:'.length);
}
let helper = currentAllowedType.translated.registeredHelpers[helperName];
// do we have a subSyntax declared in curly braces?
if (helper.subSyntax) {
let rplSubSyntax = helper.subSyntax.replace(helper.token, result);
currentSyntax = currentSyntax.replace(helper.token, rplSubSyntax);
} else {
currentSyntax = currentSyntax.replace(helper.token, result)
}
});
}
} else if (queryType == 'Array') {
let resultsOnIterate = '';
let iteratingSyntax = currentSyntax;
_.forEach(query, (value, key) => {
resultsOnIterate += (resultsOnIterate == '' ? '' : joinedWith);
resultsOnIterate += _replaceSyntaxWithValues(iteratingSyntax, {
key: key,
value: value
});
});
currentSyntax = resultsOnIterate;
}
}
break;
default:
throw new Error(`Unknown queryType '${queryType}' detected!`);
}
// replace all remaining helpers and operators with ''
// because not all of the optional helpers may be used
currentSyntax = currentSyntax.replace(/(>->->[0-9]+<-<-<)/g, '');
// check for AS clause with identifier and replace it with the
// identifier if there is some
currentSyntax = currentSyntax.replace('[AS <identifier>]', _aliasIdent(identifier));
if (helperDefinition.hooks && _.isFunction(helperDefinition.hooks.afterExecute)){
currentSyntax = helperDefinition.hooks.afterExecute.call(this, currentSyntax);
}
return currentSyntax;
};
// check if all neccessary data is defined for this helper
if (!_.isString(helperDefinition.description)) {
throw new Error(`Register helper '${name}'. Please support a short description.`);
}
if (!_.isPlainObject(helperDefinition.supportedBy)) {
throw new Error(`Register helper '${name}'. Make sure you have defined the "supportedBy" property as Object.`);
}
// check if supported sql-system is known by json-sql-builder
_.forEach(helperDefinition.supportedBy, (officialDocs, sqlDialect) => {
if (!(sqlDialect in this.supportedSQLDialects)) {
throw new Error(`Register helper '${name}'. Please check the "supportedBy" property. The language dialect "${sqlDialect}" is currently unknown.`);
}
// check for current doc's
if (!_.isString(officialDocs) || !officialDocs.startsWith('http')) {
throw new Error(`Register helper '${name}'. Please make sure you have linked the official docs for this operator or helper.`);
}
});
// check for syntax
if (!_.isPlainObject(helperDefinition.definition) || !_.isPlainObject(helperDefinition.definition.allowedTypes)){
throw new Error(`Register helper '${name}'. Please make sure you have a definition section for this operator or helper.`);
}
if (!_.isPlainObject(helperDefinition.examples)) {
throw new Error(`Register helper '${name}'. Please provide tests and examples.`);
}
// checking valid, supported types
var supportedTypes = ['Object', 'String', 'Number', 'Boolean', 'Array', 'Function'];
_.forEach(helperDefinition.definition.allowedTypes, (typeDef, type)=>{
if (supportedTypes.indexOf(type) == -1){
throw new Error(`Register helper '${name}'. The Type "${type}" in definition.allowedTypes is currently not supported.`);
}
if (!_.isPlainObject(typeDef)) {
throw new Error(`Register helper '${name}'. The allowedTypes defined for Type "${type}" must be an Object with { syntax: '...' | eachItemOf: { ... } }`);
}
if (typeDef.syntax && typeDef.eachItemOf) {
throw new Error(`Register helper '${name}'. The allowedTypes defined for Type "${type}" can only defined with { syntax: '...' | eachItemOf: { ... } } but NOT both of them.`);
}
// checking eachItemOf declaration
if (typeDef.eachItemOf) {
// check tests and examples for each sub-type declaration
_.forEach(typeDef.eachItemOf, (itemTypeDef, itemType) =>{
if (supportedTypes.indexOf(itemType) == -1){
throw new Error(`Register helper '${name}'. The Type "${itemType}" in definition.allowedTypes.${type}->eachItemOf is currently not supported.`);
}
if (itemTypeDef.syntax && !_.isString(itemTypeDef.syntax) && !_.isPlainObject(itemTypeDef.syntax)) {
throw new Error(`Register helper '${name}'. The syntax defined for Type "${type}.eachItemOf.${itemType}" can either be a String or Object.`);
}
if (!_.isPlainObject(helperDefinition.examples[type])) {
throw new Error(`Register helper '${name}'. Please provide Tests and Examples for Type "${type}.eachItemOf.${itemType}" in section examples.`);
}
if (!_.isPlainObject(helperDefinition.examples[type].eachItemOf)) {
throw new Error(`Register helper '${name}'. Please provide Tests and Examples for Type "${type}.eachItemOf.${itemType}" in section examples.`);
}
if (!_.isPlainObject(helperDefinition.examples[type].eachItemOf[itemType])) {
throw new Error(`Register helper '${name}'. Please provide Tests and Examples for Type "${type}.eachItemOf.${itemType}" in section examples.`);
}
if (_.isString(itemTypeDef.syntax)) {
// checking tests and examples for each allowed type
if (!_.isPlainObject(helperDefinition.examples[type].eachItemOf[itemType].basicUsage)) {
throw new Error(`Register helper '${name}'. Please provide a basic Test and Example for Type "${type}.eachItemOf.${itemType}"`);
}
if (!_.isPlainObject(helperDefinition.examples[type].eachItemOf[itemType].basicUsage.test)
|| !_.isPlainObject(helperDefinition.examples[type].eachItemOf[itemType].basicUsage.expectedResult)) {
throw new Error(`Register helper '${name}'. The basic Test and Example fails on definition for Type "${type}.eachItemOf.${itemType}". Please make sure you provide "test" and "expectedResult" for property "basicUsage".`);
}
} else {
// syntax as object like
/* definition: {
allowedTypes: {
Object: {
eachItemOf: {
Boolean: {
syntax: {
true: '<key-ident>[ , ... ]',
false: ''
}
},
}
}
}*/
// sub-syntaxing only allowed for
// Boolean, Number and String
if (!(itemType == 'Boolean' || itemType == 'String' || itemType == 'Number')) {
throw new Error(`Register helper '${name}'. The definition for Type "${type}.eachItemOf.${itemType}.syntax" fails. Sub-Syntaxing is only allowed for Type Boolean, String or Number.`);
}
// at least one item must be in there
if (Object.keys(itemTypeDef.syntax).length == 0) {
throw new Error(`Register helper '${name}'. The definition for Type "${type}.eachItemOf.${itemType}.syntax" is an empty Object. Please provide at least one Item with a String-value representing the Syntax.`);
}
// check that all items be type of string and include the current Syntax for the value (=key)
// AND !!! each value must have it's own Test and Example
_.forEach(itemTypeDef.syntax, (value, key)=>{
// check that value is the syntax and for that it must be a string
if (!_.isString(value)) {
throw new Error(`Register helper '${name}'. The syntax defined for Type "${type}.eachItemOf.${itemType}.syntax->${key}" must be a string representing the Syntax for this value.`);
}
// check the key, that it's value expect the itemType, so if we have a Boolean it key's
// can only be true or false. Or if we have a Number, the value ca only be a number-value
if (itemType == 'Boolean' && !(key=='true' || 'false')) {
throw new Error(`Register helper '${name}'. The value '${key}' given for Type "${type}.eachItemOf.${itemType}" is not allowed. Only 'true' or 'false' are valid.`);
}
if (itemType == 'Number' && isNaN(key)) {
throw new Error(`Register helper '${name}'. The value '${key}' given for Type "${type}.eachItemOf.${itemType}" is not allowed. Only numeric values are allowed.`);
}
// check Tests
if (!_.isPlainObject(helperDefinition.examples[type].eachItemOf[itemType][key])) {
throw new Error(`Register helper '${name}'. Please provide Tests and Examples for Type "${type}.eachItemOf.${itemType}->${key}" in section examples.`);
}
if (!_.isPlainObject(helperDefinition.examples[type].eachItemOf[itemType][key].basicUsage)) {
throw new Error(`Register helper '${name}'. Please provide a basic Test and Example for Type "${type}.eachItemOf.${itemType}->${key}"`);
}
if (!_.isPlainObject(helperDefinition.examples[type].eachItemOf[itemType][key].basicUsage.test) ||
!_.isPlainObject(helperDefinition.examples[type].eachItemOf[itemType][key].basicUsage.expectedResult)) {
throw new Error(`Register helper '${name}'. The basic Test and Example fails on definition for Type "${type}.eachItemOf.${itemType}->${key}". Please make sure you provide "test" and "expectedResult" for property "basicUsage".`);
}
});
}
});
} // eachItemOf
else {
// checking tests and examples for each allowed type
if (!_.isPlainObject(helperDefinition.examples[type])) {
throw new Error(`Register helper '${name}'. Please provide Tests and Examples for Type "${type}" in section examples.`);
}
// check if we have a value-based syntax
if (_.isPlainObject(helperDefinition.definition.allowedTypes[type].syntax)) {
// okay, it's value-based
// so iterate each value and check the examples for each value
_.forEach(helperDefinition.definition.allowedTypes[type].syntax, (valueBasedSyntax, value) =>{
if (!_.isPlainObject(helperDefinition.examples[type][value].basicUsage)) {
throw new Error(`Register helper '${name}'. Please provide a basic Test and Example for Type "${type}" in section examples.${type}.basicUsage.`);
}
if (!_.isPlainObject(helperDefinition.examples[type][value].basicUsage.test) || !_.isPlainObject(helperDefinition.examples[type][value].basicUsage.expectedResult)) {
throw new Error(`Register helper '${name}'. The basic Test and Example fails on definition for Type "${type}". Please make sure you have provide "test" and "expectedResult" for property "basicUsage".`);
}
})
} else {
if (!_.isPlainObject(helperDefinition.examples[type].basicUsage)) {
throw new Error(`Register helper '${name}'. Please provide a basic Test and Example for Type "${type}" in se