UNPKG

@blueshit/squel

Version:

squel without browser supports

1,806 lines (1,482 loc) 76.1 kB
// append to string if non-empty function _pad(str, pad) { return str.length ? str + pad : str; } // get whether object is a plain object function _isPlainObject(obj) { return obj && obj.constructor.prototype === Object.prototype; } // clone given item function _clone(src) { if (!src) { return src; } if (typeof src.clone === "function") { return src.clone(); } else if (_isPlainObject(src) || Array.isArray(src)) { let ret = new src.constructor(); Object.getOwnPropertyNames(src).forEach(function(key) { if (typeof src[key] !== "function") { ret[key] = _clone(src[key]); } }); return ret; } else { return JSON.parse(JSON.stringify(src)); } } /** * Register a value type handler * * Note: this will override any existing handler registered for this value type. */ function registerValueHandler(handlers, type, handler) { let typeofType = typeof type; if (typeofType !== "function" && typeofType !== "string") { throw new Error("type must be a class constructor or string"); } if (typeof handler !== "function") { throw new Error("handler must be a function"); } for (let typeHandler of handlers) { if (typeHandler.type === type) { typeHandler.handler = handler; return; } } handlers.push({ type: type, handler: handler }); } /** * Get value type handler for given type */ function getValueHandler(value, localHandlers, globalHandlers) { return _getValueHandler(value, localHandlers) || _getValueHandler(value, globalHandlers); } function _getValueHandler(value, handlers) { for (let i = 0; i < handlers.length; i++) { const typeHandler = handlers[i]; // if type is a string then use `typeof` or else use `instanceof` if ( typeof value === typeHandler.type || (typeof typeHandler.type !== "string" && value instanceof typeHandler.type) ) { return typeHandler.handler; } } } /** * Build base squel classes and methods */ function _buildSquel(flavour = null) { let cls = { // Get whether obj is a query builder isSquelBuilder: function(obj) { return obj && !!obj._toParamString; } }; // Get whether nesting should be applied for given item const _shouldApplyNesting = function(obj) { return !cls.isSquelBuilder(obj) || !obj.options.rawNesting; }; // default query builder options cls.DefaultQueryBuilderOptions = { // If true then table names will be rendered inside quotes. The quote character used is configurable via the nameQuoteCharacter option. autoQuoteTableNames: false, // If true then field names will rendered inside quotes. The quote character used is configurable via the nameQuoteCharacter option. autoQuoteFieldNames: false, // If true then alias names will rendered inside quotes. The quote character used is configurable via the `tableAliasQuoteCharacter` and `fieldAliasQuoteCharacter` options. autoQuoteAliasNames: true, // If true then table alias names will rendered after AS keyword. useAsForTableAliasNames: false, // The quote character used for when quoting table and field names nameQuoteCharacter: "`", // The quote character used for when quoting table alias names tableAliasQuoteCharacter: "`", // The quote character used for when quoting table alias names fieldAliasQuoteCharacter: '"', // Custom value handlers where key is the value type and the value is the handler function valueHandlers: [], // Character used to represent a parameter value parameterCharacter: "?", // Numbered parameters returned from toParam() as $1, $2, etc. numberedParameters: false, // Numbered parameters prefix character(s) numberedParametersPrefix: "$", // Numbered parameters start at this number. numberedParametersStartAt: 1, // If true then replaces all single quotes within strings. The replacement string used is configurable via the `singleQuoteReplacement` option. replaceSingleQuotes: false, // The string to replace single quotes with in query strings singleQuoteReplacement: "''", // String used to join individual blocks in a query when it's stringified separator: " ", // Function for formatting string values prior to insertion into query string stringFormatter: null, // Whether to prevent the addition of brackets () when nesting this query builder's output rawNesting: false }; // Global custom value handlers for all instances of builder cls.globalValueHandlers = []; /* # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- # Custom value types # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- */ // Register a new value handler cls.registerValueHandler = function(type, handler) { registerValueHandler(cls.globalValueHandlers, type, handler); }; /* # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- # Base classes # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- */ // Base class for cloneable builders cls.Cloneable = class { /** * Clone this builder */ clone() { let newInstance = new this.constructor(); return Object.assign(newInstance, _clone(Object.assign({}, this))); } }; // Base class for all builders cls.BaseBuilder = class extends cls.Cloneable { /** * Constructor. * this.param {Object} options Overriding one or more of `cls.DefaultQueryBuilderOptions`. */ constructor(options) { super(); let defaults = JSON.parse(JSON.stringify(cls.DefaultQueryBuilderOptions)); // for function values, etc we need to manually copy ["stringFormatter"].forEach(p => { defaults[p] = cls.DefaultQueryBuilderOptions[p]; }); this.options = Object.assign({}, defaults, options); } /** * Register a custom value handler for this builder instance. * * Note: this will override any globally registered handler for this value type. */ registerValueHandler(type, handler) { registerValueHandler(this.options.valueHandlers, type, handler); return this; } /** * Sanitize given expression. */ _sanitizeExpression(expr) { // If it's not a base builder instance if (!cls.isSquelBuilder(expr)) { // It must then be a string if (typeof expr !== "string") { throw new Error("expression must be a string or builder instance"); } } return expr; } /** * Sanitize the given name. * * The 'type' parameter is used to construct a meaningful error message in case validation fails. */ _sanitizeName(value, type) { if (typeof value !== "string") { throw new Error(`${type} must be a string`); } return value; } _sanitizeField(item) { if (!cls.isSquelBuilder(item)) { item = this._sanitizeName(item, "field name"); } return item; } _sanitizeBaseBuilder(item) { if (cls.isSquelBuilder(item)) { return item; } throw new Error("must be a builder instance"); } _sanitizeTable(item) { if (typeof item !== "string") { try { item = this._sanitizeBaseBuilder(item); } catch (e) { throw new Error("table name must be a string or a builder"); } } else { item = this._sanitizeName(item, "table"); } return item; } _sanitizeTableAlias(item) { return this._sanitizeName(item, "table alias"); } _sanitizeFieldAlias(item) { return this._sanitizeName(item, "field alias"); } // Sanitize the given limit/offset value. _sanitizeLimitOffset(value) { value = parseInt(value); if (0 > value || isNaN(value)) { throw new Error("limit/offset must be >= 0"); } return value; } // Santize the given field value _sanitizeValue(item) { let itemType = typeof item; if (null === item) { // null is allowed } else if ("string" === itemType || "number" === itemType || "boolean" === itemType) { // primitives are allowed } else if (cls.isSquelBuilder(item)) { // Builders allowed } else { let typeIsValid = !!getValueHandler(item, this.options.valueHandlers, cls.globalValueHandlers); if (!typeIsValid) { throw new Error( "field value must be a string, number, boolean, null or one of the registered custom value types" ); } } return item; } // Escape a string value, e.g. escape quotes and other characters within it. _escapeValue(value) { return this.options.replaceSingleQuotes && value ? value.replace(/\'/g, this.options.singleQuoteReplacement) : value; } _formatTableName(item) { if (this.options.autoQuoteTableNames) { const quoteChar = this.options.nameQuoteCharacter; item = `${quoteChar}${item}${quoteChar}`; } return item; } _formatFieldAlias(item) { if (this.options.autoQuoteAliasNames) { let quoteChar = this.options.fieldAliasQuoteCharacter; item = `${quoteChar}${item}${quoteChar}`; } return item; } _formatTableAlias(item) { if (this.options.autoQuoteAliasNames) { let quoteChar = this.options.tableAliasQuoteCharacter; item = `${quoteChar}${item}${quoteChar}`; } return this.options.useAsForTableAliasNames ? `AS ${item}` : item; } _formatFieldName(item, formattingOptions = {}) { if (this.options.autoQuoteFieldNames) { let quoteChar = this.options.nameQuoteCharacter; if (formattingOptions.ignorePeriodsForFieldNameQuotes) { // a.b.c -> `a.b.c` item = `${quoteChar}${item}${quoteChar}`; } else { // a.b.c -> `a`.`b`.`c` item = item .split(".") .map(function(v) { // treat '*' as special case (#79) return "*" === v ? v : `${quoteChar}${v}${quoteChar}`; }) .join("."); } } return item; } // Format the given custom value _formatCustomValue(value, asParam, formattingOptions) { // user defined custom handlers takes precedence let customHandler = getValueHandler(value, this.options.valueHandlers, cls.globalValueHandlers); // use the custom handler if available if (customHandler) { value = customHandler(value, asParam, formattingOptions); // custom value handler can instruct caller not to process returned value if (value && value.rawNesting) { return { formatted: true, rawNesting: true, value: value.value }; } } return { formatted: !!customHandler, value: value }; } /** * Format given value for inclusion into parameter values array. */ _formatValueForParamArray(value, formattingOptions = {}) { if (Array.isArray(value)) { return value.map(v => { return this._formatValueForParamArray(v, formattingOptions); }); } else { return this._formatCustomValue(value, true, formattingOptions).value; } } /** * Format the given field value for inclusion into the query string */ _formatValueForQueryString(initialValue, formattingOptions = {}) { // maybe we have a cusotm value handler let { rawNesting, formatted, value } = this._formatCustomValue(initialValue, false, formattingOptions); // if formatting took place then return it directly if (formatted) { if (rawNesting) { return value; } else { return this._applyNestingFormatting(value, _shouldApplyNesting(initialValue)); } } // if it's an array then format each element separately if (Array.isArray(value)) { value = value.map(v => { return this._formatValueForQueryString(v); }); value = this._applyNestingFormatting(value.join(", "), _shouldApplyNesting(value)); } else { let typeofValue = typeof value; if (null === value) { value = "NULL"; } else if (typeofValue === "boolean") { value = value ? "TRUE" : "FALSE"; } else if (cls.isSquelBuilder(value)) { value = this._applyNestingFormatting(value.toString(), _shouldApplyNesting(value)); } else if (typeofValue !== "number") { // if it's a string and we have custom string formatting turned on then use that if ("string" === typeofValue && this.options.stringFormatter) { return this.options.stringFormatter(value); } if (formattingOptions.dontQuote) { value = `${value}`; } else { let escapedValue = this._escapeValue(value); value = `'${escapedValue}'`; } } } return value; } _applyNestingFormatting(str, nesting = true) { if (str && typeof str === "string" && nesting && !this.options.rawNesting) { // apply brackets if they're not already existing let alreadyHasBrackets = "(" === str.charAt(0) && ")" === str.charAt(str.length - 1); if (alreadyHasBrackets) { // check that it's the form "((x)..(y))" rather than "(x)..(y)" let idx = 0, open = 1; while (str.length - 1 > ++idx) { const c = str.charAt(idx); if ("(" === c) { open++; } else if (")" === c) { open--; if (1 > open) { alreadyHasBrackets = false; break; } } } } if (!alreadyHasBrackets) { str = `(${str})`; } } return str; } /** * Build given string and its corresponding parameter values into * output. * * @param {String} str * @param {Array} values * @param {Object} [options] Additional options. * @param {Boolean} [options.buildParameterized] Whether to build paramterized string. Default is false. * @param {Boolean} [options.nested] Whether this expression is nested within another. * @param {Boolean} [options.formattingOptions] Formatting options for values in query string. * @return {Object} */ _buildString(str, values, options = {}) { let { nested, buildParameterized, formattingOptions } = options; values = values || []; str = str || ""; let formattedStr = "", curValue = -1, formattedValues = []; const paramChar = this.options.parameterCharacter; let idx = 0; while (str.length > idx) { // param char? if (str.substr(idx, paramChar.length) === paramChar) { let value = values[++curValue]; if (buildParameterized) { if (cls.isSquelBuilder(value)) { let ret = value._toParamString({ buildParameterized: buildParameterized, nested: true }); formattedStr += ret.text; ret.values.forEach(value => formattedValues.push(value)); } else { value = this._formatValueForParamArray(value, formattingOptions); if (Array.isArray(value)) { // Array(6) -> "(??, ??, ??, ??, ??, ??)" let tmpStr = value .map(function() { return paramChar; }) .join(", "); formattedStr += `(${tmpStr})`; value.forEach(val => formattedValues.push(val)); } else { formattedStr += paramChar; formattedValues.push(value); } } } else { formattedStr += this._formatValueForQueryString(value, formattingOptions); } idx += paramChar.length; } else { formattedStr += str.charAt(idx); idx++; } } return { text: this._applyNestingFormatting(formattedStr, !!nested), values: formattedValues }; } /** * Build all given strings and their corresponding parameter values into * output. * * @param {Array} strings * @param {Array} strValues array of value arrays corresponding to each string. * @param {Object} [options] Additional options. * @param {Boolean} [options.buildParameterized] Whether to build paramterized string. Default is false. * @param {Boolean} [options.nested] Whether this expression is nested within another. * @return {Object} */ _buildManyStrings(strings, strValues, options = {}) { let totalStr = [], totalValues = []; for (let idx = 0; strings.length > idx; ++idx) { let inputString = strings[idx], inputValues = strValues[idx]; let { text, values } = this._buildString(inputString, inputValues, { buildParameterized: options.buildParameterized, nested: false }); totalStr.push(text); values.forEach(value => totalValues.push(value)); } totalStr = totalStr.join(this.options.separator); return { text: totalStr.length ? this._applyNestingFormatting(totalStr, !!options.nested) : "", values: totalValues }; } /** * Get parameterized representation of this instance. * * @param {Object} [options] Options. * @param {Boolean} [options.buildParameterized] Whether to build paramterized string. Default is false. * @param {Boolean} [options.nested] Whether this expression is nested within another. * @return {Object} */ _toParamString(options) { throw new Error("Not yet implemented"); } /** * Get the expression string. * @return {String} */ toString(options = {}) { return this._toParamString(options).text; } /** * Get the parameterized expression string. * @return {Object} */ toParam(options = {}) { return this._toParamString(Object.assign({}, options, { buildParameterized: true })); } }; /* # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- # cls.Expressions # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- */ /** * An SQL expression builder. * * SQL expressions are used in WHERE and ON clauses to filter data by various criteria. * * Expressions can be nested. Nested expression contains can themselves * contain nested expressions. When rendered a nested expression will be * fully contained within brackets. * * All the build methods in this object return the object instance for chained method calling purposes. */ cls.Expression = class extends cls.BaseBuilder { // Initialise the expression. constructor(options) { super(options); this._nodes = []; } // Combine the current expression with the given expression using the intersection operator (AND). and(expr, ...params) { expr = this._sanitizeExpression(expr); this._nodes.push({ type: "AND", expr: expr, para: params }); return this; } // Combine the current expression with the given expression using the union operator (OR). or(expr, ...params) { expr = this._sanitizeExpression(expr); this._nodes.push({ type: "OR", expr: expr, para: params }); return this; } _toParamString(options = {}) { let totalStr = [], totalValues = []; for (let node of this._nodes) { let { type, expr, para } = node; let { text, values } = cls.isSquelBuilder(expr) ? expr._toParamString({ buildParameterized: options.buildParameterized, nested: true }) : this._buildString(expr, para, { buildParameterized: options.buildParameterized }); if (totalStr.length) { totalStr.push(type); } totalStr.push(text); values.forEach(value => totalValues.push(value)); } totalStr = totalStr.join(" "); return { text: this._applyNestingFormatting(totalStr, !!options.nested), values: totalValues }; } }; /* # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- # cls.Case # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- */ /** * An SQL CASE expression builder. * * SQL cases are used to select proper values based on specific criteria. */ cls.Case = class extends cls.BaseBuilder { constructor(fieldName, options = {}) { super(options); if (_isPlainObject(fieldName)) { options = fieldName; fieldName = null; } if (fieldName) { this._fieldName = this._sanitizeField(fieldName); } this.options = Object.assign({}, cls.DefaultQueryBuilderOptions, options); this._cases = []; this._elseValue = null; } when(expression, ...values) { this._cases.unshift({ expression: expression, values: values || [] }); return this; } then(result) { if (this._cases.length == 0) { throw new Error("when() needs to be called first"); } this._cases[0].result = result; return this; } else(elseValue) { this._elseValue = elseValue; return this; } _toParamString(options = {}) { let totalStr = "", totalValues = []; for (let { expression, values, result } of this._cases) { totalStr = _pad(totalStr, " "); let ret = this._buildString(expression, values, { buildParameterized: options.buildParameterized, nested: true }); totalStr += `WHEN ${ret.text} THEN ${this._formatValueForQueryString(result)}`; ret.values.forEach(value => totalValues.push(value)); } if (totalStr.length) { totalStr += ` ELSE ${this._formatValueForQueryString(this._elseValue)} END`; if (this._fieldName) { totalStr = `${this._fieldName} ${totalStr}`; } totalStr = `CASE ${totalStr}`; } else { totalStr = this._formatValueForQueryString(this._elseValue); } return { text: totalStr, values: totalValues }; } }; /* # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- # Building blocks # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- */ /* # A building block represents a single build-step within a query building process. # # Query builders consist of one or more building blocks which get run in a particular order. Building blocks can # optionally specify methods to expose through the query builder interface. They can access all the input data for # the query builder and manipulate it as necessary, as well as append to the final query string output. # # If you wish to customize how queries get built or add proprietary query phrases and content then it is recommended # that you do so using one or more custom building blocks. # # Original idea posted in https://github.com/hiddentao/export/issues/10#issuecomment-15016427 */ cls.Block = class extends cls.BaseBuilder { constructor(options) { super(options); } /** # Get input methods to expose within the query builder. # # By default all methods except the following get returned: # methods prefixed with _ # constructor and toString() # # @return Object key -> function pairs */ exposedMethods() { let ret = {}; let obj = this; while (obj) { Object.getOwnPropertyNames(obj).forEach(function(prop) { if ( "constructor" !== prop && typeof obj[prop] === "function" && prop.charAt(0) !== "_" && !cls.Block.prototype[prop] ) { ret[prop] = obj[prop]; } }); obj = Object.getPrototypeOf(obj); } return ret; } }; // A fixed string which always gets output cls.StringBlock = class extends cls.Block { constructor(options, str) { super(options); this._str = str; } _toParamString(options = {}) { return { text: this._str, values: [] }; } }; // A function string block cls.FunctionBlock = class extends cls.Block { constructor(options) { super(options); this._strings = []; this._values = []; } function(str, ...values) { this._strings.push(str); this._values.push(values); } _toParamString(options = {}) { return this._buildManyStrings(this._strings, this._values, options); } }; // value handler for FunctionValueBlock objects cls.registerValueHandler(cls.FunctionBlock, function(value, asParam = false) { return asParam ? value.toParam() : value.toString(); }); /* # Table specifier base class */ cls.AbstractTableBlock = class extends cls.Block { /** * @param {Boolean} [options.singleTable] If true then only allow one table spec. * @param {String} [options.prefix] String prefix for output. */ constructor(options, prefix) { super(options); this._tables = []; } /** # Update given table. # # An alias may also be specified for the table. # # Concrete subclasses should provide a method which calls this */ _table(table, alias = null) { alias = alias ? this._sanitizeTableAlias(alias) : alias; table = this._sanitizeTable(table); if (this.options.singleTable) { this._tables = []; } this._tables.push({ table: table, alias: alias }); } // get whether a table has been set _hasTable() { return 0 < this._tables.length; } /** * @override */ _toParamString(options = {}) { let totalStr = "", totalValues = []; if (this._hasTable()) { // retrieve the parameterised queries for (let { table, alias } of this._tables) { totalStr = _pad(totalStr, ", "); let tableStr; if (cls.isSquelBuilder(table)) { let { text, values } = table._toParamString({ buildParameterized: options.buildParameterized, nested: true }); tableStr = text; values.forEach(value => totalValues.push(value)); } else { tableStr = this._formatTableName(table); } if (alias) { tableStr += ` ${this._formatTableAlias(alias)}`; } totalStr += tableStr; } if (this.options.prefix) { totalStr = `${this.options.prefix} ${totalStr}`; } } return { text: totalStr, values: totalValues }; } }; // target table for DELETE queries, DELETE <??> FROM cls.TargetTableBlock = class extends cls.AbstractTableBlock { target(table) { this._table(table); } }; // Update Table cls.UpdateTableBlock = class extends cls.AbstractTableBlock { table(table, alias = null) { this._table(table, alias); } _toParamString(options = {}) { if (!this._hasTable()) { throw new Error("table() needs to be called"); } return super._toParamString(options); } }; // FROM table cls.FromTableBlock = class extends cls.AbstractTableBlock { constructor(options) { super( Object.assign({}, options, { prefix: "FROM" }) ); } from(table, alias = null) { this._table(table, alias); } }; // INTO table cls.IntoTableBlock = class extends cls.AbstractTableBlock { constructor(options) { super( Object.assign({}, options, { prefix: "INTO", singleTable: true }) ); } into(table) { this._table(table); } _toParamString(options = {}) { if (!this._hasTable()) { throw new Error("into() needs to be called"); } return super._toParamString(options); } }; // (SELECT) Get field cls.GetFieldBlock = class extends cls.Block { constructor(options) { super(options); this._fields = []; } /** # Add the given fields to the final result set. # # The parameter is an Object containing field names (or database functions) as the keys and aliases for the fields # as the values. If the value for a key is null then no alias is set for that field. # # Internally this method simply calls the field() method of this block to add each individual field. # # options.ignorePeriodsForFieldNameQuotes - whether to ignore period (.) when automatically quoting the field name */ fields(_fields, options = {}) { if (Array.isArray(_fields)) { for (let field of _fields) { this.field(field, null, options); } } else { for (let field in _fields) { let alias = _fields[field]; this.field(field, alias, options); } } } /** # Add the given field to the final result set. # # The 'field' parameter does not necessarily have to be a fieldname. It can use database functions too, # e.g. DATE_FORMAT(a.started, "%H") # # An alias may also be specified for this field. # # options.ignorePeriodsForFieldNameQuotes - whether to ignore period (.) when automatically quoting the field name */ field(field, alias = null, options = {}) { alias = alias ? this._sanitizeFieldAlias(alias) : alias; field = this._sanitizeField(field); // if field-alias combo already present then don't add let existingField = this._fields.filter(f => { return f.name === field && f.alias === alias; }); if (existingField.length) { return this; } this._fields.push({ name: field, alias: alias, options: options }); } _toParamString(options = {}) { let { queryBuilder, buildParameterized } = options; let totalStr = "", totalValues = []; for (let field of this._fields) { totalStr = _pad(totalStr, ", "); let { name, alias, options } = field; if (typeof name === "string") { totalStr += this._formatFieldName(name, options); } else { let ret = name._toParamString({ nested: true, buildParameterized: buildParameterized }); totalStr += ret.text; ret.values.forEach(value => totalValues.push(value)); } if (alias) { totalStr += ` AS ${this._formatFieldAlias(alias)}`; } } if (!totalStr.length) { // if select query and a table is set then all fields wanted let fromTableBlock = queryBuilder && queryBuilder.getBlock(cls.FromTableBlock); if (fromTableBlock && fromTableBlock._hasTable()) { totalStr = "*"; } } return { text: totalStr, values: totalValues }; } }; // Base class for setting fields to values (used for INSERT and UPDATE queries) cls.AbstractSetFieldBlock = class extends cls.Block { constructor(options) { super(options); this._reset(); } _reset() { this._fields = []; this._values = [[]]; this._valueOptions = [[]]; } // Update the given field with the given value. // This will override any previously set value for the given field. _set(field, value, valueOptions = {}) { if (this._values.length > 1) { throw new Error("Cannot set multiple rows of fields this way."); } if (typeof value !== "undefined") { value = this._sanitizeValue(value); } field = this._sanitizeField(field); // Explicity overwrite existing fields let index = this._fields.indexOf(field); // if field not defined before if (-1 === index) { this._fields.push(field); index = this._fields.length - 1; } this._values[0][index] = value; this._valueOptions[0][index] = valueOptions; } // Insert fields based on the key/value pairs in the given object _setFields(fields, valueOptions = {}) { if (typeof fields !== "object") { throw new Error("Expected an object but got " + typeof fields); } for (let field in fields) { this._set(field, fields[field], valueOptions); } } // Insert multiple rows for the given fields. Accepts an array of objects. // This will override all previously set values for every field. _setFieldsRows(fieldsRows, valueOptions = {}) { if (!Array.isArray(fieldsRows)) { throw new Error("Expected an array of objects but got " + typeof fieldsRows); } // Reset the objects stored fields and values this._reset(); // for each row for (let i = 0; fieldsRows.length > i; ++i) { let fieldRow = fieldsRows[i]; // for each field for (let field in fieldRow) { let value = fieldRow[field]; field = this._sanitizeField(field); value = this._sanitizeValue(value); let index = this._fields.indexOf(field); if (0 < i && -1 === index) { throw new Error("All fields in subsequent rows must match the fields in the first row"); } // Add field only if it hasn't been added before if (-1 === index) { this._fields.push(field); index = this._fields.length - 1; } // The first value added needs to add the array if (!Array.isArray(this._values[i])) { this._values[i] = []; this._valueOptions[i] = []; } this._values[i][index] = value; this._valueOptions[i][index] = valueOptions; } } } }; // (UPDATE) SET field=value cls.SetFieldBlock = class extends cls.AbstractSetFieldBlock { set(field, value, options) { this._set(field, value, options); } setFields(fields, valueOptions) { this._setFields(fields, valueOptions); } _toParamString(options = {}) { let { buildParameterized } = options; if (0 >= this._fields.length) { throw new Error("set() needs to be called"); } let totalStr = "", totalValues = []; for (let i = 0; i < this._fields.length; ++i) { totalStr = _pad(totalStr, ", "); let field = this._formatFieldName(this._fields[i]); let value = this._values[0][i]; // e.g. field can be an expression such as `count = count + 1` if (0 > field.indexOf("=")) { field = `${field} = ${this.options.parameterCharacter}`; } let ret = this._buildString(field, [value], { buildParameterized: buildParameterized, formattingOptions: this._valueOptions[0][i] }); totalStr += ret.text; ret.values.forEach(value => totalValues.push(value)); } return { text: `SET ${totalStr}`, values: totalValues }; } }; // (INSERT INTO) ... field ... value cls.InsertFieldValueBlock = class extends cls.AbstractSetFieldBlock { set(field, value, options = {}) { this._set(field, value, options); } setFields(fields, valueOptions) { this._setFields(fields, valueOptions); } setFieldsRows(fieldsRows, valueOptions) { this._setFieldsRows(fieldsRows, valueOptions); } _toParamString(options = {}) { let { buildParameterized } = options; let fieldString = this._fields.map(f => this._formatFieldName(f)).join(", "); let valueStrings = [], totalValues = []; for (let i = 0; i < this._values.length; ++i) { valueStrings[i] = ""; for (let j = 0; j < this._values[i].length; ++j) { let ret = this._buildString(this.options.parameterCharacter, [this._values[i][j]], { buildParameterized: buildParameterized, formattingOptions: this._valueOptions[i][j] }); ret.values.forEach(value => totalValues.push(value)); valueStrings[i] = _pad(valueStrings[i], ", "); valueStrings[i] += ret.text; } } return { text: fieldString.length ? `(${fieldString}) VALUES (${valueStrings.join("), (")})` : "", values: totalValues }; } }; // (INSERT INTO) ... field ... (SELECT ... FROM ...) cls.InsertFieldsFromQueryBlock = class extends cls.Block { constructor(options) { super(options); this._fields = []; this._query = null; } fromQuery(fields, selectQuery) { this._fields = fields.map(v => { return this._sanitizeField(v); }); this._query = this._sanitizeBaseBuilder(selectQuery); } _toParamString(options = {}) { let totalStr = "", totalValues = []; if (this._fields.length && this._query) { let { text, values } = this._query._toParamString({ buildParameterized: options.buildParameterized, nested: true }); totalStr = `(${this._fields.join(", ")}) ${this._applyNestingFormatting(text)}`; totalValues = values; } return { text: totalStr, values: totalValues }; } }; // DISTINCT cls.DistinctBlock = class extends cls.Block { // Add the DISTINCT keyword to the query. distinct() { this._useDistinct = true; } _toParamString() { return { text: this._useDistinct ? "DISTINCT" : "", values: [] }; } }; // GROUP BY cls.GroupByBlock = class extends cls.Block { constructor(options) { super(options); this._groups = []; } // Add a GROUP BY transformation for the given field. group(field) { this._groups.push(this._sanitizeField(field)); } _toParamString(options = {}) { return { text: this._groups.length ? `GROUP BY ${this._groups.join(", ")}` : "", values: [] }; } }; cls.AbstractVerbSingleValueBlock = class extends cls.Block { /** * @param options.verb The prefix verb string. */ constructor(options) { super(options); this._value = null; } _setValue(value) { this._value = null !== value ? this._sanitizeLimitOffset(value) : value; } _toParamString(options = {}) { const expr = null !== this._value ? `${this.options.verb} ${this.options.parameterCharacter}` : ""; const values = null !== this._value ? [this._value] : []; return this._buildString(expr, values, options); } }; // OFFSET x cls.OffsetBlock = class extends cls.AbstractVerbSingleValueBlock { constructor(options) { super( Object.assign({}, options, { verb: "OFFSET" }) ); } /** # Set the OFFSET transformation. # # Call this will override the previously set offset for this query. Also note that Passing 0 for 'max' will remove # the offset. */ offset(start) { this._setValue(start); } }; // LIMIT cls.LimitBlock = class extends cls.AbstractVerbSingleValueBlock { constructor(options) { super( Object.assign({}, options, { verb: "LIMIT" }) ); } /** # Set the LIMIT transformation. # # Call this will override the previously set limit for this query. Also note that Passing `null` will remove # the limit. */ limit(limit) { this._setValue(limit); } }; // Abstract condition base class cls.AbstractConditionBlock = class extends cls.Block { /** * @param {String} options.verb The condition verb. */ constructor(options) { super(options); this._conditions = []; } /** # Add a condition. # # When the final query is constructed all the conditions are combined using the intersection (AND) operator. # # Concrete subclasses should provide a method which calls this */ _condition(condition, ...values) { condition = this._sanitizeExpression(condition); this._conditions.push({ expr: condition, values: values || [] }); } _toParamString(options = {}) { let totalStr = [], totalValues = []; for (let { expr, values } of this._conditions) { let ret = cls.isSquelBuilder(expr) ? expr._toParamString({ buildParameterized: options.buildParameterized }) : this._buildString(expr, values, { buildParameterized: options.buildParameterized }); if (ret.text.length) { totalStr.push(ret.text); } ret.values.forEach(value => totalValues.push(value)); } if (totalStr.length) { totalStr = totalStr.join(") AND ("); } return { text: totalStr.length ? `${this.options.verb} (${totalStr})` : "", values: totalValues }; } }; // WHERE cls.WhereBlock = class extends cls.AbstractConditionBlock { constructor(options) { super( Object.assign({}, options, { verb: "WHERE" }) ); } where(condition, ...values) { this._condition(condition, ...values); } }; // HAVING cls.HavingBlock = class extends cls.AbstractConditionBlock { constructor(options) { super( Object.assign({}, options, { verb: "HAVING" }) ); } having(condition, ...values) { this._condition(condition, ...values); } }; // ORDER BY cls.OrderByBlock = class extends cls.Block { constructor(options) { super(options); this._orders = []; } /** # Add an ORDER BY transformation for the given field in the given order. # # To specify descending order pass false for the 'dir' parameter. */ order(field, dir, ...values) { field = this._sanitizeField(field); if (!(typeof dir === "string")) { if (dir === undefined) { dir = "ASC"; // Default to asc } else if (dir !== null) { dir = dir ? "ASC" : "DESC"; // Convert truthy to asc } } this._orders.push({ field: field, dir: dir, values: values || [] }); } _toParamString(options = {}) { let totalStr = "", totalValues = []; for (let { field, dir, values } of this._orders) { totalStr = _pad(totalStr, ", "); let ret = this._buildString(field, values, { buildParameterized: options.buildParameterized }); (totalStr += ret.text), Array.isArray(ret.values) && ret.values.forEach(value => totalValues.push(value)); if (dir !== null) { totalStr += ` ${dir}`; } } return { text: totalStr.length ? `ORDER BY ${totalStr}` : "", values: totalValues }; } }; // JOIN cls.JoinBlock = class extends cls.Block { constructor(options) { super(options); this._joins = []; } /** # Add a JOIN with the given table. # # 'table' is the name of the table to join with. # # 'alias' is an optional alias for the table name. # # 'condition' is an optional condition (containing an SQL expression) for the JOIN. # # 'type' must be either one of INNER, OUTER, LEFT or RIGHT. Default is 'INNER'. # */ join(table, alias = null, condition = null, type = "INNER") { table = this._sanitizeTable(table, true); alias = alias ? this._sanitizeTableAlias(alias) : alias; condition = condition ? this._sanitizeExpression(condition) : condition; this._joins.push({ type: type, table: table, alias: alias, condition: condition }); } left_join(table, alias = null, condition = null) { this.join(table, alias, condition, "LEFT"); } right_join(table, alias = null, condition = null) { this.join(table, alias, condition, "RIGHT"); } outer_join(table, alias = null, condition = null) { this.join(table, alias, condition, "OUTER"); } left_outer_join(table, alias = null, condition = null) { this.join(table, alias, condition, "LEFT OUTER"); } full_join(table, alias = null, condition = null) { this.join(table, alias, condition, "FULL"); } cross_join(table, alias = null, condition = null) { this.join(table, alias, condition, "CROSS"); } _toParamString(options = {}) { let totalStr = "", totalValues = []; for (let { type, table, alias, condition } of this._joins) { totalStr = _pad(totalStr, this.options.separator); let tableStr; if (cls.isSquelBuilder(table)) { let ret = table._toParamString({ buildParameterized: options.buildParameterized, nested: true }); ret.values.forEach(value => totalValues.push(value)); tableStr = ret.text; } else { tableStr = this._formatTableName(table); } totalStr += `${type} JOIN ${tableStr}`; if (alias) { totalStr += ` ${this._formatTableAlias(alias)}`; } if (condition) { totalStr += " ON "; let ret; if (cls.isSquelBuilder(condition)) { ret = condition._toParamString({ buildParameterized: options.buildParameterized }); } else { ret = this._buildString(condition, [], { buildParameterized: options.buildParameterized }); } totalStr += this._applyNestingFormatting(ret.text); ret.values.forEach(value => totalValues.push(value)); } } return { text: totalStr, values: totalValues }; } }; // UNION cls.UnionBlock = class extends cls.Block { constructor(options) { super(options); this._unions = []; } /** # Add a UNION with the given table/query. # # 'table' is the name of the table or query to union with. # # 'type' must be either one of UNION or UNION ALL.... Default is 'UNION'. */ union(table, type = "UNION") { table = this._sanitizeTable(table); this._unions.push({ type: type, table: table }); } // Add a UNION ALL with the given table/query. union_all(table) { this.union(table, "UNION ALL"); } _toParamString(options = {}) { let totalStr = "", totalValues = []; for (let { type, table } of this._unions) { totalStr = _pad(totalStr, this.options.separator); let tableStr; if (table instanceof cls.BaseBuilder) { let ret = table._toParamString({ buildParameterized: options.buildParameterized, nested: true }); tableStr = ret.text; ret.values.forEach(value => totalValues.push(value)); } else { totalStr = this._formatTableName(table); } totalStr += `${type} ${tableStr}`; } return { text: totalStr, values: totalValues }; } }; /* # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- # Query builders # --------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------------------------------- */ /** # Query bui