diff --git a/.eslintrc.js b/.eslintrc.js index ef4990e..b62dce4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,26 +8,20 @@ module.exports = { es2021: true }, extends: [ + 'eslint:recommended', 'standard' ], globals: { RED: true, Promise: true }, + plugins: ['eslint-plugin-html', 'no-only-tests'], + parserOptions: { + ecmaVersion: 13 + }, rules: { indent: ['error', 4], - semi: ['error', 'always'], - quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }], - 'quote-props': ['error', 'as-needed', { unnecessary: false }] - // 'space-before-function-paren': ['never'], - // 'spaced-comment': ['never'] - - }, - plugins: [ - 'html' - ], - settings: { - 'html/html-extensions': ['.html'] + 'spaced-comment': ['error', 'always', { block: { balanced: true }, line: { markers: ['/'] } }], + 'no-only-tests/no-only-tests': 'error' } - -}; +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 310422b..a07a885 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: - node-version: [16, 18] + node-version: [16, 18, 20] env: SA_PASSWORD: "P@ssw0rdP@ssw0rd" @@ -66,5 +66,9 @@ jobs: cp ./test/_config.github.json ./test/config.json npm install + - name: Run ESLint + if: matrix.node-version == 20 + run: npm run lint + - name: npm test run: npm test diff --git a/package.json b/package.json index ddd7481..746b3f5 100644 --- a/package.json +++ b/package.json @@ -4,19 +4,18 @@ "description": "A node-red node to execute queries, stored procedures and bulk inserts in Microsoft SQL Server and Azure Databases SQL2000 ~ SQL2022", "main": "odbc.js", "dependencies": { - "mssql": "^10.0.0", + "mssql": "^10.0.4", "mustache": "^4.2.0" }, "devDependencies": { - "eslint": "^7.25.0", - "eslint-config-standard": "^16.0.2", - "eslint-plugin-html": "^6.1.2", - "eslint-plugin-import": "^2.22.1", + "eslint": "^8.47.0", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-html": "^7.1.0", + "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^4.3.1", - "mocha": "^9.2.0", - "node-red": "^1.0.3", - "node-red-node-test-helper": "^0.3.0", + "mocha": "^10.7.3", + "node-red": "^4.0.2", + "node-red-node-test-helper": "^0.3.4", "should": "^13.2.3" }, "node-red": { @@ -34,6 +33,8 @@ }, "scripts": { "test": "mocha \"test/**/*_spec.js\"", + "lint": "eslint test/**/*.js src/**/*.js src/**/*.html", + "lint:fix": "eslint test/**/*.js src/**/*.js src/**/*.html --fix", "release": "np" }, "repository": { @@ -54,8 +55,8 @@ "author": "Redconnect.io", "contributors": [ "Shao Yu-Lung (https://github.com/bestlong)", - "Olli Kasari <8488349+kasarol@users.noreply.github.com>", - "Steve McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> (https://github.com/Steve-Mcl)" + "Steve McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> (https://github.com/Steve-Mcl)", + "Olli Kasari <8488349+kasarol@users.noreply.github.com>" ], "license": "MIT", "homepage": "https://github.com/bestlong/node-red-contrib-mssql-plus", diff --git a/src/mssql.html b/src/mssql.html index 68b27b7..3c2fb08 100755 --- a/src/mssql.html +++ b/src/mssql.html @@ -220,21 +220,21 @@ }, label: function () { if (this.name) { - return this.name; + return this.name } - let n = 'MSSQL-CN'; + let n = 'MSSQL-CN' if (this.server) { - n = this.server; + n = this.server if (this.database) { - n += '.' + this.database; + n += '.' + this.database } } - return n; + return n }, oneditresize: function (size) { - $('div.form-row.mssql-plus-form-row > .form-tips').css('maxWidth', 'unset'); + $('div.form-row.mssql-plus-form-row > .form-tips').css('maxWidth', 'unset') } - }); + }) @@ -328,30 +328,30 @@

Shameless request for beta testers...

/* eslint-disable spaced-comment */ /* eslint-disable space-before-function-paren */ (function() { - let paramsHeight = 145; + let paramsHeight = 145 function resizeParameters() { - let paramsWidth = $('#node-input-params-container').width(); - const queryMode = $('#node-input-modeOpt').typedInput('type'); - const bulkMode = queryMode === 'bulk'; - const showItemOptsRow = ['bulk', 'msg', 'flow', 'env'].indexOf(queryMode) >= 0; + let paramsWidth = $('#node-input-params-container').width() + const queryMode = $('#node-input-modeOpt').typedInput('type') + const bulkMode = queryMode === 'bulk' + const showItemOptsRow = ['bulk', 'msg', 'flow', 'env'].indexOf(queryMode) >= 0 if (showItemOptsRow) { - $('#node-input-params-container .node-input-item-property-options').show(); + $('#node-input-params-container .node-input-item-property-options').show() } else { - $('#node-input-params-container .node-input-item-property-options').hide(); + $('#node-input-params-container .node-input-item-property-options').hide() } - paramsWidth -= (28 + 5 + 72 + 6 + 6 + 6 + 6 + 5 + 28);//remove outer padding, margins & input/output width - let p1 = Math.max(((paramsWidth / 3) - 20), 80); - let p2 = Math.max(p1 - 20, 80); - const p3 = Math.max(paramsWidth - (p1 + p2), 80); + paramsWidth -= (28 + 5 + 72 + 6 + 6 + 6 + 6 + 5 + 28)//remove outer padding, margins & input/output width + let p1 = Math.max(((paramsWidth / 3) - 20), 80) + let p2 = Math.max(p1 - 20, 80) + const p3 = Math.max(paramsWidth - (p1 + p2), 80) if (bulkMode) { - p1 += (p3 / 2); - p2 += (p3 / 2); + p1 += (p3 / 2) + p2 += (p3 / 2) } else { - $('#node-input-params-container .node-input-item-property-value').typedInput('width', Math.round(p3)); + $('#node-input-params-container .node-input-item-property-value').typedInput('width', Math.round(p3)) } - $('#node-input-params-container .node-input-item-property-name').typedInput('width', Math.round(p1)); - $('#node-input-params-container .node-input-item-property-type').typedInput('width', Math.round(p2)); + $('#node-input-params-container .node-input-item-property-name').typedInput('width', Math.round(p1)) + $('#node-input-params-container .node-input-item-property-type').typedInput('width', Math.round(p2)) } RED.nodes.registerType('MSSQL', { category: 'storage', @@ -410,25 +410,25 @@

Shameless request for beta testers...

outputs: 1, icon: 'db.png', label: function () { - return this.name || 'MSSQL-PLUS'; + return this.name || 'MSSQL-PLUS' }, labelStyle: function () { - return this.name ? 'node_label_italic' : ''; + return this.name ? 'node_label_italic' : '' }, oneditprepare: function () { if (this.parseMustache === undefined) { - this.parseMustache = true; - $('#node-input-parseMustache').prop('checked', true); + this.parseMustache = true + $('#node-input-parseMustache').prop('checked', true) } - const node = this; + const node = this //ensure node.query is a string otherwise editor wont initialise - if (typeof node.query !== 'string') node.query = ''; + if (typeof node.query !== 'string') node.query = '' //create the query editor this.editor = RED.editor.createEditor({ id: 'node-sql-query-editor', mode: 'ace/mode/sql', value: node.query // $("#node-input-query").val() - }); + }) //create typedInput widgets const modeField = $('#node-input-modeOpt').typedInput({ types: [ @@ -440,7 +440,7 @@

Shameless request for beta testers...

], typeField: $('#node-input-modeOptType'), default: 'query' - }); + }) const queryField = $('#node-input-queryOpt').typedInput({ types: [ { value: 'editor', label: this._('mssql.common.editor'), hasValue: false }, @@ -448,7 +448,7 @@

Shameless request for beta testers...

], typeField: $('#node-input-queryOptType'), default: 'editor' - }); + }) const paramsField = $('#node-input-paramsOpt').typedInput({ types: [ { value: 'none', label: 'None', hasValue: false }, @@ -457,86 +457,86 @@

Shameless request for beta testers...

], typeField: $('#node-input-paramsOptType'), default: 'none' - }); + }) // eslint-disable-next-line no-unused-vars const rowsField = $('#node-input-rows').typedInput({ types: ['msg', 'flow', 'global', 'json', 'jsonata'], typeField: $('#node-input-rowsType'), default: 'msg' - }); + }) // eslint-disable-next-line no-unused-vars const outField = $('#node-input-outField').typedInput({ types: [ { value: 'str', label: 'msg.' } ], default: 'str' - }); + }) //show/hide rows field depending on query type modeField.on('change', function() { - const queryMode = $(this).typedInput('type'); + const queryMode = $(this).typedInput('type') if (queryMode === 'bulk') { - $('#mssql-plus-params-label').text('Columns'); - $('#dialog-form > div.form-row.mssql-plus-form-row.bulk-data-input-row').show(); - $('#node-input-params-container .node-input-item-property-value').typedInput('hide'); - $('#node-input-params-container .node-input-item-property-options').show(); + $('#mssql-plus-params-label').text('Columns') + $('#dialog-form > div.form-row.mssql-plus-form-row.bulk-data-input-row').show() + $('#node-input-params-container .node-input-item-property-value').typedInput('hide') + $('#node-input-params-container .node-input-item-property-options').show() } else if (queryMode === 'query' || queryMode === 'execute') { - $('#mssql-plus-params-label').text('Parameters'); - $('#dialog-form > div.form-row.mssql-plus-form-row.bulk-data-input-row').hide(); - $('#node-input-params-container .node-input-item-property-value').typedInput('show'); - $('#node-input-params-container .node-input-item-property-options').hide(); + $('#mssql-plus-params-label').text('Parameters') + $('#dialog-form > div.form-row.mssql-plus-form-row.bulk-data-input-row').hide() + $('#node-input-params-container .node-input-item-property-value').typedInput('show') + $('#node-input-params-container .node-input-item-property-options').hide() } else { - $('#mssql-plus-params-label').text('Params/Cols'); - $('#dialog-form > div.form-row.mssql-plus-form-row.bulk-data-input-row').show(); - $('#node-input-params-container .node-input-item-property-value').typedInput('show'); - $('#node-input-params-container .node-input-item-property-options').show(); + $('#mssql-plus-params-label').text('Params/Cols') + $('#dialog-form > div.form-row.mssql-plus-form-row.bulk-data-input-row').show() + $('#node-input-params-container .node-input-item-property-value').typedInput('show') + $('#node-input-params-container .node-input-item-property-options').show() } - RED.tray.resize(); - }); + RED.tray.resize() + }) //show/hide editor depending on query source queryField.on('change', function() { - const type = $(this).typedInput('type'); + const type = $(this).typedInput('type') if (type === 'editor') { - $('.form-row.node-text-editor-row').removeClass('mssql-plus-hide-row'); + $('.form-row.node-text-editor-row').removeClass('mssql-plus-hide-row') } else { - $('.form-row.node-text-editor-row').addClass('mssql-plus-hide-row'); + $('.form-row.node-text-editor-row').addClass('mssql-plus-hide-row') } - RED.tray.resize(); + RED.tray.resize() if (type === 'editor') { - $('#node-query-expand').show(); + $('#node-query-expand').show() } else { - $('#node-query-expand').hide(); + $('#node-query-expand').hide() } - }); + }) //show/hide parameter list depending on queryMode paramsField.on('change', function() { - const type = $(this).typedInput('type'); + const type = $(this).typedInput('type') if (type === 'editor') { - $('.form-row.node-input-params-container-row').removeClass('mssql-plus-hide-row'); + $('.form-row.node-input-params-container-row').removeClass('mssql-plus-hide-row') } else { - $('.form-row.node-input-params-container-row').addClass('mssql-plus-hide-row'); + $('.form-row.node-input-params-container-row').addClass('mssql-plus-hide-row') } - RED.tray.resize(); - }); + RED.tray.resize() + }) $('#node-query-expand').click(function(e) { - e.preventDefault(); - const value = node.editor.getValue(); + e.preventDefault() + const value = node.editor.getValue() RED.editor.editJavaScript({ title: 'Edit SQL Query', - value: value, + value, width: 'Infinity', cursor: node.editor.getCursorPosition(), mode: 'ace/mode/sql', complete: function(v, cursor) { - node.editor.setValue(v, -1); - node.editor.gotoLine(cursor.row + 1, cursor.column, false); + node.editor.setValue(v, -1) + node.editor.gotoLine(cursor.row + 1, cursor.column, false) setTimeout(function() { - node.editor.focus(); - }, 300); + node.editor.focus() + }, 300) } - }); - }); + }) + }) // *** compatibility defaults *** //if node.query is empty, the user would probably have been sending the @@ -545,53 +545,53 @@

Shameless request for beta testers...

// if they have any other value, the user must have been in and operated the Done // button (thus accepting the .queryOpt setting - i.e. leave as is!) if (!node.query && typeof node.queryOpt === 'undefined' && typeof node.queryOptType === 'undefined') { - queryField.typedInput('type', 'msg'); - queryField.typedInput('value', 'payload'); + queryField.typedInput('type', 'msg') + queryField.typedInput('value', 'payload') } if ((!node.params || !node.params.length) && typeof node.paramsOpt === 'undefined' && typeof node.paramsOptType === 'undefined') { - paramsField.typedInput('type', 'msg'); - paramsField.typedInput('value', 'queryParams'); + paramsField.typedInput('type', 'msg') + paramsField.typedInput('value', 'queryParams') } - $('#node-input-returnType')[0].selectedIndex = this.returnType ? this.returnType : 0; - $('#node-input-throwErrors')[0].selectedIndex = this.throwErrors ? this.throwErrors : 0; + $('#node-input-returnType')[0].selectedIndex = this.returnType ? this.returnType : 0 + $('#node-input-throwErrors')[0].selectedIndex = this.throwErrors ? this.throwErrors : 0 //Parameters Editor... $('#node-input-params-container').css('min-height', '120px').css('min-width', '520px').editableList({ addItem: function (container, i, opt) { - const queryMode = modeField.typedInput('type'); - let rule = opt; let isNew = false; + const queryMode = modeField.typedInput('type') + let rule = opt; let isNew = false if (!Object.prototype.hasOwnProperty.call(rule, 'type') && !Object.prototype.hasOwnProperty.call(rule, 'name')) { - rule = { type: 'int', name: 'param' + (i + 1), value: 0, valueType: 'num', output: false }; - isNew = true; + rule = { type: 'int', name: 'param' + (i + 1), value: 0, valueType: 'num', output: false } + isNew = true } if (!rule.options || typeof rule.options !== 'object') { - rule.options = { nullable: true, primary: false, identity: false, readOnly: false }; + rule.options = { nullable: true, primary: false, identity: false, readOnly: false } } container.css({ overflow: 'hidden', whiteSpace: 'nowrap' - }); - const fragment = document.createDocumentFragment(); - const row1 = $('
', { style: 'display:flex;', class: 'mssql-plus-row' }).appendTo(fragment); - const row2 = $('
', { style: 'display:flex;', class: 'mssql-plus-row' }).appendTo(fragment); + }) + const fragment = document.createDocumentFragment() + const row1 = $('
', { style: 'display:flex;', class: 'mssql-plus-row' }).appendTo(fragment) + const row2 = $('
', { style: 'display:flex;', class: 'mssql-plus-row' }).appendTo(fragment) - const row1Div1 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row1); + const row1Div1 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row1) const inoutField = $('', { class: 'node-input-item-property-inout', type: 'text', width: '72px' }) .appendTo(row1Div1) - .typedInput({ types: [{ label: 'Input', value: 'input', hasValue: false }, { label: 'Output', value: 'output', hasValue: false }] }); + .typedInput({ types: [{ label: 'Input', value: 'input', hasValue: false }, { label: 'Output', value: 'output', hasValue: false }] }) - const row1Div2 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row1); + const row1Div2 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row1) const nameField = $('', { class: 'node-input-item-property-name', type: 'text', width: '112px' }) .appendTo(row1Div2) - .typedInput({ types: [{ label: 'Name', value: 'str' }] }); + .typedInput({ types: [{ label: 'Name', value: 'str' }] }) - const row1Div3 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row1); + const row1Div3 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row1) const typeField = $('', { class: 'node-input-item-property-type', type: 'text', width: '120px' }) .appendTo(row1Div3) - .typedInput({ types: [{ label: 'Type', value: 'str' }] }); + .typedInput({ types: [{ label: 'Type', value: 'str' }] }) - const row1Div4 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row1); + const row1Div4 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row1) const valueField = $('', { class: 'node-input-item-property-value', type: 'text', width: '140px' }) .appendTo(row1Div4) .typedInput( @@ -621,34 +621,34 @@

Shameless request for beta testers...

}], default: 'num' } - ); - valueField.typedInput(queryMode === 'bulk' ? 'hide' : 'show'); + ) + valueField.typedInput(queryMode === 'bulk' ? 'hide' : 'show') - const row2Div1 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row2); + const row2Div1 = $('
', { style: 'display:flex;', class: 'mssql-plus-row-item' }).appendTo(row2) const optionsField = $('
', { class: 'node-input-item-property-options', type: 'text', width: '100px' }) .data('role', 'controlgroup').data('type', 'horizontal') - .appendTo(row2Div1); + .appendTo(row2Div1) - optionsField.append($('')); - optionsField.append($('')); - optionsField.append($('')); - optionsField.append($('')); + optionsField.append($('')) + optionsField.append($('')) + optionsField.append($('')) + optionsField.append($('')) - const nullableOptionField = optionsField.find('.node-input-item-property-nullable'); - const primaryOptionField = optionsField.find('.node-input-item-property-primary'); - const identityOptionField = optionsField.find('.node-input-item-property-identity'); - const readOnlyOptionField = optionsField.find('.node-input-item-property-readOnly'); + const nullableOptionField = optionsField.find('.node-input-item-property-nullable') + const primaryOptionField = optionsField.find('.node-input-item-property-primary') + const identityOptionField = optionsField.find('.node-input-item-property-identity') + const readOnlyOptionField = optionsField.find('.node-input-item-property-readOnly') inoutField.on('change', function (o) { - const t = $(this).typedInput('type'); + const t = $(this).typedInput('type') if (t === 'output') { - valueField.typedInput('hide'); + valueField.typedInput('hide') } else { - valueField.typedInput('show'); + valueField.typedInput('show') } - }); + }) - const typeInput = row1Div3.find('input[type!=hidden]'); + const typeInput = row1Div3.find('input[type!=hidden]') const sqlTypes = [ 'VarChar', 'VarChar(?)', @@ -694,94 +694,94 @@

Shameless request for beta testers...

'Geography', 'Geometry', 'Variant' - ]; + ] $(typeInput).autocomplete({ source: sqlTypes - }); + }) - nameField.typedInput('type', 'str'); - nameField.typedInput('value', rule.name || ('param' + (i + 1))); + nameField.typedInput('type', 'str') + nameField.typedInput('value', rule.name || ('param' + (i + 1))) - typeField.typedInput('type', 'str'); - typeField.typedInput('value', rule.type || ''); + typeField.typedInput('type', 'str') + typeField.typedInput('value', rule.type || '') - valueField.typedInput('type', rule.valueType || 'num'); - valueField.typedInput('value', rule.value || 0); + valueField.typedInput('type', rule.valueType || 'num') + valueField.typedInput('value', rule.value || 0) - inoutField.typedInput('type', (rule.output === 'true' || rule.output === true) ? 'output' : 'input'); + inoutField.typedInput('type', (rule.output === 'true' || rule.output === true) ? 'output' : 'input') - nullableOptionField.prop('checked', rule.options.nullable); - primaryOptionField.prop('checked', rule.options.primary); - identityOptionField.prop('checked', rule.options.identity); - readOnlyOptionField.prop('checked', rule.options.readOnly); + nullableOptionField.prop('checked', rule.options.nullable) + primaryOptionField.prop('checked', rule.options.primary) + identityOptionField.prop('checked', rule.options.identity) + readOnlyOptionField.prop('checked', rule.options.readOnly) - container[0].appendChild(fragment); + container[0].appendChild(fragment) - if (isNew) resizeParameters();//cause newly added row to size correctly. + if (isNew) resizeParameters()//cause newly added row to size correctly. }, removable: true, sortable: true - }); + }) for (let i = 0; i < this.params.length; i++) { - const item = this.params[i]; - $('#node-input-params-container').editableList('addItem', item); + const item = this.params[i] + $('#node-input-params-container').editableList('addItem', item) } //try to maintain user set height for parameters window - const paramsContainer = $('#dialog-form > div.form-row.node-input-params-container-row > .red-ui-editableList > .red-ui-editableList-container'); - paramsContainer.css('resize', 'vertical'); - paramsContainer.css('min-height', 120); - paramsHeight = Math.max(120, paramsHeight); - paramsContainer.css('height', paramsHeight); - resizeParameters(); + const paramsContainer = $('#dialog-form > div.form-row.node-input-params-container-row > .red-ui-editableList > .red-ui-editableList-container') + paramsContainer.css('resize', 'vertical') + paramsContainer.css('min-height', 120) + paramsHeight = Math.max(120, paramsHeight) + paramsContainer.css('height', paramsHeight) + resizeParameters() }, oneditsave: function () { - const node = this; - node.params = []; - const paramsContainer = $('#dialog-form > div.form-row.node-input-params-container-row > .red-ui-editableList > .red-ui-editableList-container'); - paramsHeight = Math.max(120, paramsContainer.height()); - const items = $('#node-input-params-container').editableList('items'); + const node = this + node.params = [] + const paramsContainer = $('#dialog-form > div.form-row.node-input-params-container-row > .red-ui-editableList > .red-ui-editableList-container') + paramsHeight = Math.max(120, paramsContainer.height()) + const items = $('#node-input-params-container').editableList('items') items.each(function (i) { - const rule = $(this); - const r = {}; - r.output = rule.find('.node-input-item-property-inout').typedInput('type') === 'output'; - r.name = rule.find('.node-input-item-property-name').val() || 'param' + (i + 1); - r.type = rule.find('.node-input-item-property-type').val() || ''; + const rule = $(this) + const r = {} + r.output = rule.find('.node-input-item-property-inout').typedInput('type') === 'output' + r.name = rule.find('.node-input-item-property-name').val() || 'param' + (i + 1) + r.type = rule.find('.node-input-item-property-type').val() || '' if (r.output) { //exclude value } else { - r.valueType = rule.find('.node-input-item-property-value').typedInput('type') || 'num'; - r.value = rule.find('.node-input-item-property-value').typedInput('value') || 0; + r.valueType = rule.find('.node-input-item-property-value').typedInput('type') || 'num' + r.value = rule.find('.node-input-item-property-value').typedInput('value') || 0 } r.options = { nullable: rule.find('.node-input-item-property-nullable').prop('checked'), primary: rule.find('.node-input-item-property-primary').prop('checked'), identity: rule.find('.node-input-item-property-identity').prop('checked'), readOnly: rule.find('.node-input-item-property-readOnly').prop('checked') - }; - node.params.push(r); - }); + } + node.params.push(r) + }) - node.query = this.editor.getValue(); - this.editor.destroy(); + node.query = this.editor.getValue() + this.editor.destroy() }, oneditresize: function (size) { - const rows = $('#dialog-form>div:not(.node-text-editor-row)'); - let height = $('#dialog-form').height(); - let row; + const rows = $('#dialog-form>div:not(.node-text-editor-row)') + let height = $('#dialog-form').height() + let row for (let i = 0; i < rows.size(); i++) { - row = $(rows[i]); - if (row && row.is(':hidden')) continue; - height -= row.outerHeight(true); + row = $(rows[i]) + if (row && row.is(':hidden')) continue + height -= row.outerHeight(true) } - const editorRow = $('#dialog-form>div.node-text-editor-row'); - height -= (parseInt(editorRow.css('marginTop')) + parseInt(editorRow.css('marginBottom'))); - const h = Math.max(height, 60); - $('.node-text-editor').css('height', h + 'px'); - this.editor.resize(); - resizeParameters(); + const editorRow = $('#dialog-form>div.node-text-editor-row') + height -= (parseInt(editorRow.css('marginTop')) + parseInt(editorRow.css('marginBottom'))) + const h = Math.max(height, 60) + $('.node-text-editor').css('height', h + 'px') + this.editor.resize() + resizeParameters() } - }); -})(); + }) +})() diff --git a/src/mssql.js b/src/mssql.js index b40eaaf..b66b838 100755 --- a/src/mssql.js +++ b/src/mssql.js @@ -1,23 +1,21 @@ -/* eslint-disable spaced-comment */ -/* eslint-disable space-before-function-paren */ module.exports = function (RED) { - 'use strict'; - const mustache = require('mustache'); - const sql = require('mssql'); + 'use strict' + const mustache = require('mustache') + const sql = require('mssql') const UUID = (function () { - const crypto = require('crypto'); + const crypto = require('crypto') // isValid routine developed based on \tedious\lib\guid-parser.js // checking the guid in this node BEFORE calling pool.query() // prevents an exception that I cannot catch in tedious - const CHARCODEMAP = {}; - const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F'].map(d => d.charCodeAt(0)); + const CHARCODEMAP = {} + const hexDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F'].map(d => d.charCodeAt(0)) for (let i = 0; i < hexDigits.length; i++) { - const map = CHARCODEMAP[hexDigits[i]] = {}; + const map = CHARCODEMAP[hexDigits[i]] = {} for (let j = 0; j < hexDigits.length; j++) { - const hex = String.fromCharCode(hexDigits[i], hexDigits[j]); - const value = parseInt(hex, 16); - map[hexDigits[j]] = value; + const hex = String.fromCharCode(hexDigits[i], hexDigits[j]) + const value = parseInt(hex, 16) + map[hexDigits[j]] = value } } return { @@ -28,9 +26,9 @@ module.exports = function (RED) { */ isValid: function (guid) { try { - return Array.isArray([CHARCODEMAP[guid.charCodeAt(6)][guid.charCodeAt(7)], CHARCODEMAP[guid.charCodeAt(4)][guid.charCodeAt(5)], CHARCODEMAP[guid.charCodeAt(2)][guid.charCodeAt(3)], CHARCODEMAP[guid.charCodeAt(0)][guid.charCodeAt(1)], CHARCODEMAP[guid.charCodeAt(11)][guid.charCodeAt(12)], CHARCODEMAP[guid.charCodeAt(9)][guid.charCodeAt(10)], CHARCODEMAP[guid.charCodeAt(16)][guid.charCodeAt(17)], CHARCODEMAP[guid.charCodeAt(14)][guid.charCodeAt(15)], CHARCODEMAP[guid.charCodeAt(19)][guid.charCodeAt(20)], CHARCODEMAP[guid.charCodeAt(21)][guid.charCodeAt(22)], CHARCODEMAP[guid.charCodeAt(24)][guid.charCodeAt(25)], CHARCODEMAP[guid.charCodeAt(26)][guid.charCodeAt(27)], CHARCODEMAP[guid.charCodeAt(28)][guid.charCodeAt(29)], CHARCODEMAP[guid.charCodeAt(30)][guid.charCodeAt(31)], CHARCODEMAP[guid.charCodeAt(32)][guid.charCodeAt(33)], CHARCODEMAP[guid.charCodeAt(34)][guid.charCodeAt(35)]]); + return Array.isArray([CHARCODEMAP[guid.charCodeAt(6)][guid.charCodeAt(7)], CHARCODEMAP[guid.charCodeAt(4)][guid.charCodeAt(5)], CHARCODEMAP[guid.charCodeAt(2)][guid.charCodeAt(3)], CHARCODEMAP[guid.charCodeAt(0)][guid.charCodeAt(1)], CHARCODEMAP[guid.charCodeAt(11)][guid.charCodeAt(12)], CHARCODEMAP[guid.charCodeAt(9)][guid.charCodeAt(10)], CHARCODEMAP[guid.charCodeAt(16)][guid.charCodeAt(17)], CHARCODEMAP[guid.charCodeAt(14)][guid.charCodeAt(15)], CHARCODEMAP[guid.charCodeAt(19)][guid.charCodeAt(20)], CHARCODEMAP[guid.charCodeAt(21)][guid.charCodeAt(22)], CHARCODEMAP[guid.charCodeAt(24)][guid.charCodeAt(25)], CHARCODEMAP[guid.charCodeAt(26)][guid.charCodeAt(27)], CHARCODEMAP[guid.charCodeAt(28)][guid.charCodeAt(29)], CHARCODEMAP[guid.charCodeAt(30)][guid.charCodeAt(31)], CHARCODEMAP[guid.charCodeAt(32)][guid.charCodeAt(33)], CHARCODEMAP[guid.charCodeAt(34)][guid.charCodeAt(35)]]) } catch (error) { - return false; + return false } }, /** @@ -38,33 +36,34 @@ module.exports = function (RED) { * @returns {string} UUID */ v4: function () { - const buf = crypto.randomBytes(32); - let idx = 0; + const buf = crypto.randomBytes(32) + let idx = 0 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = buf[idx++] & 0xf; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + const r = buf[idx++] & 0xf + const v = c === 'x' ? r : (r & 0x3 | 0x8) + return v.toString(16) + }) } - }; - })(); + } + })() /** * extractTokens - borrowed from @0node-red/nodes/core/core/80-template.js */ - function extractTokens(tokens, set) { - set = set || new Set(); + function extractTokens (tokens, set) { + set = set || new Set() tokens.forEach(function (token) { if (token[0] !== 'text') { - set.add(token[1]); + set.add(token[1]) if (token.length > 4) { - extractTokens(token[4], set); + extractTokens(token[4], set) } } - }); - return set; + }) + return set } + /* eslint-disable quote-props */ const sqlParamTypes = { 'varchar': { params: [], sqlType: 'VarChar' }, 'varchar(?)': { params: ['nm'], sqlType: 'VarChar' }, @@ -110,84 +109,85 @@ module.exports = function (RED) { 'geography': { params: [], sqlType: 'Geography' }, 'geometry': { params: [], sqlType: 'Geometry' }, 'variant': { params: [], sqlType: 'Variant' } - }; - - function coerceType(sqlType) { - if (!sqlType) return null; //no type specified (infered) - const st = sqlType.trim(); - const sl = st.toLowerCase(); - const bp = sl.indexOf('('); - let typeName; let typeParams = ''; let p; let p2; - if (bp > -1) { //has bracket? - typeName = sl.slice(0, bp).trim();//get typename without params - typeParams = '(?)'; //additional typename param for sqlParamTypes lookup - const _params = st.slice(bp + 1, -1);//get params - const hasComma = _params.indexOf(',') > 0; //more than one param? + } + /* eslint-enable quote-props */ + + function coerceType (sqlType) { + if (!sqlType) return null // no type specified (inferred) + const st = sqlType.trim() + const sl = st.toLowerCase() + const bp = sl.indexOf('(') + let typeName; let typeParams = ''; let p; let p2 + if (bp > -1) { // has bracket? + typeName = sl.slice(0, bp).trim()// get typename without params + typeParams = '(?)' // additional typename param for sqlParamTypes lookup + const _params = st.slice(bp + 1, -1)// get params + const hasComma = _params.indexOf(',') > 0 // more than one param? if (hasComma && (sl.includes('decimal') || sl.includes('numeric'))) { - typeParams = '(?,?)'; //additional typename param for sqlParamTypes lookup - const params = _params.split(','); - p = params[0]; - p2 = params[1]; + typeParams = '(?,?)' // additional typename param for sqlParamTypes lookup + const params = _params.split(',') + p = params[0] + p2 = params[1] } else { - p = _params; + p = _params } } else { - typeName = sl; //use lowercase type name for sqlParamTypes lookup + typeName = sl // use lowercase type name for sqlParamTypes lookup } - //lookup the desired type from sqlParamTypes - const _type = sqlParamTypes[typeName + typeParams]; + // lookup the desired type from sqlParamTypes + const _type = sqlParamTypes[typeName + typeParams] if (_type) { - const params = []; + const params = [] if (_type.params.length >= 1) { if (_type.params[0] === 'nm' && (p + '').toLowerCase() === 'max') { - params.push('MAX'); + params.push('MAX') } else if (_type.params[0] === 'n') { - params.push(parseInt(p)); + params.push(parseInt(p)) } else { - params.push(p); + params.push(p) } } if (_type.params.length >= 2) { if (_type.params[1] === 'nm' && (p2 + '').toLowerCase() === 'max') { - params.push('MAX'); + params.push('MAX') } else if (_type.params[1] === 'n') { - params.push(parseInt(p2)); + params.push(parseInt(p2)) } else if (_type.params[0] === 'n') { - params.push(p2); + params.push(p2) } } if (params.length) { - return sql[_type.sqlType](...params);//e.g. sql.NChar(10) + return sql[_type.sqlType](...params)// e.g. sql.NChar(10) } - return sql[_type.sqlType]();//e.g. sql.NChar() + return sql[_type.sqlType]()// e.g. sql.NChar() } - throw new Error(`Unable to determine the type or its properties from '${sqlType}'`); + throw new Error(`Unable to determine the type or its properties from '${sqlType}'`) } /** * parseContext - borrowed from @0node-red/nodes/core/core/80-template.js */ - function parseContext(key) { - const match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key); + function parseContext (key) { + const match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key) if (match) { - const parts = {}; - parts.type = match[1]; - parts.store = (match[3] === '') ? 'default' : match[3]; - parts.field = match[4]; - return parts; + const parts = {} + parts.type = match[1] + parts.store = (match[3] === '') ? 'default' : match[3] + parts.field = match[4] + return parts } - return undefined; + return undefined } /** * parseEnv - borrowed from @0node-red/nodes/core/core/80-template.js */ - function parseEnv(key) { - const match = /^env\.(.+)/.exec(key); + function parseEnv (key) { + const match = /^env\.(.+)/.exec(key) if (match) { - return match[1]; + return match[1] } - return undefined; + return undefined } /** @@ -195,7 +195,7 @@ module.exports = function (RED) { * @param {string | number} n */ function isNumber (n) { - return !isNaN(parseFloat(n)) && isFinite(n); + return !isNaN(parseFloat(n)) && isFinite(n) } /** @@ -207,81 +207,81 @@ module.exports = function (RED) { */ function safeParseInt (n, defaultValue) { try { - const x = parseInt(n); + const x = parseInt(n) if (isNumber(x)) { - return x; + return x } } catch (error) { // do nothing } - return defaultValue; + return defaultValue } /** * NodeContext - borrowed from @0node-red/nodes/core/core/80-template.js */ function NodeContext (msg, nodeContext, parent, escapeStrings, cachedContextTokens) { - this.msgContext = new mustache.Context(msg, parent); - this.nodeContext = nodeContext; - this.escapeStrings = escapeStrings; - this.cachedContextTokens = cachedContextTokens; + this.msgContext = new mustache.Context(msg, parent) + this.nodeContext = nodeContext + this.escapeStrings = escapeStrings + this.cachedContextTokens = cachedContextTokens } - NodeContext.prototype = new mustache.Context(); + NodeContext.prototype = new mustache.Context() NodeContext.prototype.lookup = function (name) { - let value = this.msgContext.lookup(name); + let value = this.msgContext.lookup(name) if (value !== undefined) { if (this.escapeStrings && typeof value === 'string') { - value = value.replace(/\\/g, '\\\\'); - value = value.replace(/\n/g, '\\n'); - value = value.replace(/\t/g, '\\t'); - value = value.replace(/\r/g, '\\r'); - value = value.replace(/\f/g, '\\f'); - value = value.replace(/[\b]/g, '\\b'); - } - return value; + value = value.replace(/\\/g, '\\\\') + value = value.replace(/\n/g, '\\n') + value = value.replace(/\t/g, '\\t') + value = value.replace(/\r/g, '\\r') + value = value.replace(/\f/g, '\\f') + value = value.replace(/[\b]/g, '\\b') + } + return value } // try env if (parseEnv(name)) { - return this.cachedContextTokens[name]; + return this.cachedContextTokens[name] } // try flow/global context: - const context = parseContext(name); + const context = parseContext(name) if (context) { - const type = context.type; + const type = context.type // const store = context.store; // const field = context.field; - const target = this.nodeContext[type]; + const target = this.nodeContext[type] if (target) { - return this.cachedContextTokens[name]; + return this.cachedContextTokens[name] } } - return ''; - }; + return '' + } - NodeContext.prototype.push = function push(view) { - return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.cachedContextTokens); - }; + NodeContext.prototype.push = function push (view) { + return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.cachedContextTokens) + } - function connection(config) { - RED.nodes.createNode(this, config); - const node = this; + function connection (config) { + RED.nodes.createNode(this, config) + const node = this - //add mustache transformation to connection object - const configStr = JSON.stringify(config); - const transform1 = mustache.render(configStr, process.env); - config = JSON.parse(transform1); + // add mustache transformation to connection object + const configStr = JSON.stringify(config) + const transform1 = mustache.render(configStr, process.env) + config = JSON.parse(transform1) - //add mustache transformation to credentials object + // add mustache transformation to credentials object try { - const credStr = JSON.stringify(node.credentials); - const transform2 = mustache.render(credStr, process.env); - node.credentials = JSON.parse(transform2); + const credStr = JSON.stringify(node.credentials) + const transform2 = mustache.render(credStr, process.env) + node.credentials = JSON.parse(transform2) } catch (error) { - console.error(error); + console.error(error) } node.config = { @@ -294,40 +294,40 @@ module.exports = function (RED) { port: config.port ? safeParseInt(config.port, 1433) : undefined, tdsVersion: config.tdsVersion || '7_4', encrypt: config.encyption, - trustServerCertificate: !((config.trustServerCertificate === 'false' || config.trustServerCertificate === false)), //defaults to true for backwards compatibility - TODO: Review this default. + trustServerCertificate: !((config.trustServerCertificate === 'false' || config.trustServerCertificate === false)), // defaults to true for backwards compatibility - TODO: Review this default. useUTC: config.useUTC, connectTimeout: config.connectTimeout ? safeParseInt(config.connectTimeout, 15000) : undefined, requestTimeout: config.requestTimeout ? safeParseInt(config.requestTimeout, 15000) : undefined, cancelTimeout: config.cancelTimeout ? safeParseInt(config.cancelTimeout, 5000) : undefined, - camelCaseColumns: (config.camelCaseColumns === 'true' || config.camelCaseColumns === true) ? true : undefined, //defaults to undefined. - parseJSON: !!((config.parseJSON === 'true' || config.parseJSON === true)), //defaults to true. - enableArithAbort: !((config.enableArithAbort === 'false' || config.enableArithAbort === false)), //defaults to true. - readOnlyIntent: (config.readOnlyIntent === 'true' || config.readOnlyIntent === true) //defaults to false. + camelCaseColumns: (config.camelCaseColumns === 'true' || config.camelCaseColumns === true) ? true : undefined, // defaults to undefined. + parseJSON: !!((config.parseJSON === 'true' || config.parseJSON === true)), // defaults to true. + enableArithAbort: !((config.enableArithAbort === 'false' || config.enableArithAbort === false)), // defaults to true. + readOnlyIntent: (config.readOnlyIntent === 'true' || config.readOnlyIntent === true) // defaults to false. }, pool: { max: safeParseInt(config.pool, 5), min: 0, idleTimeoutMillis: 3000 - //log: (message, logLevel) => console.log(`POOL: [${logLevel}] ${message}`) + // log: (message, logLevel) => console.log(`POOL: [${logLevel}] ${message}`) } - }; + } - //config options seem to differ between pool and tedious connection - //so for compatibility I just repeat the ones that differ so they get picked up in _poolCreate () - node.config.port = node.config.options.port; - node.config.connectionTimeout = node.config.options.connectTimeout; - node.config.requestTimeout = node.config.options.requestTimeout; - node.config.cancelTimeout = node.config.options.cancelTimeout; - node.config.encrypt = node.config.options.encrypt; + // config options seem to differ between pool and tedious connection + // so for compatibility I just repeat the ones that differ so they get picked up in _poolCreate () + node.config.port = node.config.options.port + node.config.connectionTimeout = node.config.options.connectTimeout + node.config.requestTimeout = node.config.options.requestTimeout + node.config.cancelTimeout = node.config.options.cancelTimeout + node.config.encrypt = node.config.options.encrypt - node.connectedNodes = []; + node.connectedNodes = [] node.connectionCleanup = function (quiet) { - const updateStatusAndLog = !quiet; + const updateStatusAndLog = !quiet try { if (node.poolConnect) { - if (updateStatusAndLog) node.log(`Disconnecting server : ${node.config.server}, database : ${node.config.database}, port : ${node.config.options.port}, user : ${node.config.user}`); - node.poolConnect.then(_ => _.close()).catch(e => { console.error(e); }); + if (updateStatusAndLog) node.log(`Disconnecting server : ${node.config.server}, database : ${node.config.database}, port : ${node.config.options.port}, user : ${node.config.user}`) + node.poolConnect.then(_ => _.close()).catch(e => { console.error(e) }) } } catch (error) { } @@ -337,129 +337,129 @@ module.exports = function (RED) { if (node.pool && node.pool.close) { node.pool.close().catch(() => {}) } - if (updateStatusAndLog) node.status({ fill: 'grey', shape: 'dot', text: 'disconnected' }); - node.poolConnect = null; - }; + if (updateStatusAndLog) node.status({ fill: 'grey', shape: 'dot', text: 'disconnected' }) + node.poolConnect = null + } - node.pool = new sql.ConnectionPool(node.config); + node.pool = new sql.ConnectionPool(node.config) node.pool.on('error', err => { - node.error(err); - node.connectionCleanup(); - }); + node.error(err) + node.connectionCleanup() + }) node.connect = function () { if (node.poolConnect) { - return; + return } node.status({ fill: 'yellow', shape: 'dot', text: 'connecting' - }); - node.poolConnect = node.pool.connect(); - return node.poolConnect; - }; + }) + node.poolConnect = node.pool.connect() + return node.poolConnect + } node.execSql = async function (queryMode, sqlQuery, params, paramValues, callback) { - const _info = []; + const _info = [] try { if (!node.poolConnect && !!(await node.connect())) { - node.log(`Connected to server : ${node.config.server}, database : ${node.config.database}, port : ${node.config.options.port}, user : ${node.config.user}`); + node.log(`Connected to server : ${node.config.server}, database : ${node.config.database}, port : ${node.config.options.port}, user : ${node.config.user}`) } - //FUTURE: let req = queryMode == "prepared" ? new sql.PreparedStatement(node.pool) : node.pool.request(); - const req = node.pool.request(); + // FUTURE: let req = queryMode == "prepared" ? new sql.PreparedStatement(node.pool) : node.pool.request(); + const req = node.pool.request() req.on('info', info => { - _info.push(info); - }); - //If bulk mode, create a table & populate the rows - let bulkTable; + _info.push(info) + }) + // If bulk mode, create a table & populate the rows + let bulkTable if (queryMode === 'bulk') { - bulkTable = new sql.Table(sqlQuery); - bulkTable.create = true;//TODO: Present this as an option + bulkTable = new sql.Table(sqlQuery) + bulkTable.create = true// TODO: Present this as an option for (let rx = 0; rx < paramValues.length; rx++) { - bulkTable.rows.push(paramValues[rx]); + bulkTable.rows.push(paramValues[rx]) } } - //setup params/columns + // setup params/columns if (params && params.length) { for (let index = 0; index < params.length; index++) { - const p = params[index]; + const p = params[index] if (queryMode === 'bulk') { - const _opts = p.options || {}; - if (p.type && p.type.length) _opts.length = p.type.length; - bulkTable.columns.add(p.name, p.type, _opts); - continue; + const _opts = p.options || {} + if (p.type && p.type.length) _opts.length = p.type.length + bulkTable.columns.add(p.name, p.type, _opts) + continue } if (p.output === true || p.output === 'true') { if (p.type) { - req.output(p.name, p.type); + req.output(p.name, p.type) } else { - req.output(p.name); + req.output(p.name) } } else { - //if the data is a vtp/udt, coerce the type from string into sql.type + // if the data is a vtp/udt, coerce the type from string into sql.type if (p.type && (p.type.name === 'UDT' || p.type.name === 'TVP' || (p.type.type && p.type.type.name === 'TVP')) && typeof p.value === 'object' && p.value.columns && p.value.rows) { - const table = new sql.Table(); + const table = new sql.Table() for (let index = 0; index < p.value.columns.length; index++) { - const col = p.value.columns[index]; - table.columns.add(col.name, coerceType(col.type)); + const col = p.value.columns[index] + table.columns.add(col.name, coerceType(col.type)) } for (let index = 0; index < p.value.rows.length; index++) { - const row = p.value.rows[index]; + const row = p.value.rows[index] if (Array.isArray(row)) { - table.rows.add(...row); + table.rows.add(...row) } else { - table.rows.add(row); + table.rows.add(row) } } - req.input(p.name, p.type, table); + req.input(p.name, p.type, table) } else if (p.type) { - req.input(p.name, p.type, p.value); + req.input(p.name, p.type, p.value) } else { - req.input(p.name, p.value); + req.input(p.name, p.value) } } } } - let result; + let result switch (queryMode) { case 'bulk': - result = await req.bulk(bulkTable); - break; + result = await req.bulk(bulkTable) + break case 'execute': - result = await req.execute(sqlQuery); - break; + result = await req.execute(sqlQuery) + break // case "prepared": //FUTURE // await req.prepare(sqlQuery); // result = await req.execute(paramValues); // await req.unprepare(); // break; default: - result = await req.query(sqlQuery); - break; + result = await req.query(sqlQuery) + break } - callback(null, result, _info); + callback(null, result, _info) } catch (e) { - node.log(`Error connecting to server : ${node.config.server}, database : ${node.config.database}, port : ${node.config.options.port}, user : ${node.config.user}`); - console.error(e); - node.poolConnect = null; - callback(e); + node.log(`Error connecting to server : ${node.config.server}, database : ${node.config.database}, port : ${node.config.options.port}, user : ${node.config.user}`) + console.error(e) + node.poolConnect = null + callback(e) } - }; + } node.disconnect = function (nodeId) { - const index = node.connectedNodes.indexOf(nodeId); + const index = node.connectedNodes.indexOf(nodeId) if (index >= 0) { - node.connectedNodes.splice(index, 1); + node.connectedNodes.splice(index, 1) } if (node.connectedNodes.length === 0) { - node.connectionCleanup(); + node.connectionCleanup() } - }; + } } RED.nodes.registerType('MSSQL-CN', connection, { @@ -474,113 +474,113 @@ module.exports = function (RED) { type: 'text' } } - }); - - function mssql(config) { - RED.nodes.createNode(this, config); - const mssqlCN = RED.nodes.getNode(config.mssqlCN); - const node = this; - - node.query = config.query; - node.outField = config.outField || 'payload'; - node.returnType = config.returnType; - node.throwErrors = !(!config.throwErrors || config.throwErrors === '0'); - node.params = config.params; - node.queryMode = config.queryMode; - - node.modeOpt = config.modeOpt; - node.modeOptType = config.modeOptType || 'query'; - node.queryOpt = config.queryOpt; - node.queryOptType = config.queryOptType || 'editor'; - node.paramsOpt = config.paramsOpt; - node.paramsOptType = config.paramsOptType || 'none'; - node.rows = config.rows || 'rows'; - node.rowsType = config.rowsType || 'msg'; - node.parseMustache = config.parseMustache || true; + }) + + function mssql (config) { + RED.nodes.createNode(this, config) + const mssqlCN = RED.nodes.getNode(config.mssqlCN) + const node = this + + node.query = config.query + node.outField = config.outField || 'payload' + node.returnType = config.returnType + node.throwErrors = !(!config.throwErrors || config.throwErrors === '0') + node.params = config.params + node.queryMode = config.queryMode + + node.modeOpt = config.modeOpt + node.modeOptType = config.modeOptType || 'query' + node.queryOpt = config.queryOpt + node.queryOptType = config.queryOptType || 'editor' + node.paramsOpt = config.paramsOpt + node.paramsOptType = config.paramsOptType || 'none' + node.rows = config.rows || 'rows' + node.rowsType = config.rowsType || 'msg' + node.parseMustache = config.parseMustache || true const setResult = function (msg, field, value, returnType = 0) { // eslint-disable-next-line eqeqeq - const setValue = (returnType == 1 || msg.queryMode === 'bulk') ? value : value && value.recordset; + const setValue = (returnType == 1 || msg.queryMode === 'bulk') ? value : value && value.recordset const set = (obj, path, val) => { - const keys = path.split('.'); - const lastKey = keys.pop(); + const keys = path.split('.') + const lastKey = keys.pop() // eslint-disable-next-line no-return-assign const lastObj = keys.reduce((obj, key) => obj[key] = obj[key] || {}, - obj); - lastObj[lastKey] = val; - }; - set(msg, field, setValue); - }; + obj) + lastObj[lastKey] = val + } + set(msg, field, setValue) + } const updateOutputParams = function (params, data) { - if (!params || !params.length) return; - if (!data || !data.output) return; - const outputParams = params.filter(e => e.output); + if (!params || !params.length) return + if (!data || !data.output) return + const outputParams = params.filter(e => e.output) for (let index = 0; index < outputParams.length; index++) { - const param = outputParams[index]; - param.value = data.output[param.name]; + const param = outputParams[index] + param.value = data.output[param.name] } - }; + } - const validateQueryParam = function(p) { - //{name:string, type?:string, value?:any, output?:boolean} + const validateQueryParam = function (p) { + // {name:string, type?:string, value?:any, output?:boolean} if (typeof p !== 'object') { - throw new Error('Parameter is not an object'); + throw new Error('Parameter is not an object') } if (!p.name || typeof p.name !== 'string') { - throw new Error('Parameter does not have a valid name property'); + throw new Error('Parameter does not have a valid name property') } if (!p.output && !('value' in p)) { - throw new Error('Input parameter \'' + p.name + '\' does not have a value property'); + throw new Error('Input parameter \'' + p.name + '\' does not have a value property') } if (p.type && p.type.toLowerCase() === 'uniqueidentifier') { if (!UUID.isValid(p.value)) { - throw new Error('Unique identifier is not valid'); + throw new Error('Unique identifier is not valid') } } - return true; - }; - const validateBulkColumn = function(p) { - //{name:string, type?:string, value?:any, output?:boolean} + return true + } + const validateBulkColumn = function (p) { + // {name:string, type?:string, value?:any, output?:boolean} if (typeof p !== 'object') { - throw new Error('Column Parameter is not an object'); + throw new Error('Column Parameter is not an object') } if (!p.name || typeof p.name !== 'string') { - throw new Error('Column Parameter does not have a valid name property'); + throw new Error('Column Parameter does not have a valid name property') } if (!p.type || typeof p.type !== 'string') { - throw new Error('Column Parameter does not have a valid type property'); + throw new Error('Column Parameter does not have a valid type property') } if (p.type && p.type.toLowerCase() === 'uniqueidentifier') { if (!UUID.isValid(p.value)) { - throw new Error('Unique identifier is not valid'); + throw new Error('Unique identifier is not valid') } } if (p.output) { - throw new Error('Output parameter is not valid for bulk insert mode'); + throw new Error('Output parameter is not valid for bulk insert mode') } - return true; - }; + return true + } node.processError = function (err, msg) { - let errMsg = 'Error'; + let errMsg = 'Error' if (typeof err === 'string') { - errMsg = err; - msg.error = err; + errMsg = err + msg.error = err } else if (err && err.message) { - errMsg = err.message; + errMsg = err.message if (err.precedingErrors !== undefined && err.precedingErrors.length && err.precedingErrors[0].originalError !== undefined && err.precedingErrors[0].originalError.info !== null && err.precedingErrors[0].originalError.info.message !== null) { - errMsg += ' (' + err.precedingErrors[0].originalError.info.message + ')'; + errMsg += ' (' + err.precedingErrors[0].originalError.info.message + ')' } - //Make an error object from the err. NOTE: We cant just assign err to msg.error as a promise - //rejection occurs when the node has 2 wires on the output. - //(redUtil.cloneMessage(m) causes error "node-red Cannot assign to read only property 'originalError'") + // Make an error object from the err. NOTE: We cant just assign err to msg.error as a promise + // rejection occurs when the node has 2 wires on the output. + // (redUtil.cloneMessage(m) causes error "node-red Cannot assign to read only property 'originalError'") msg.error = { class: err.class, code: err.code, @@ -593,247 +593,247 @@ module.exports = function (RED) { serverName: err.serverName, state: err.state, toString: function () { - return this.message; + return this.message } - }; + } } node.status({ fill: 'red', shape: 'ring', text: errMsg - }); + }) if (node.throwErrors) { - node.error(msg.error, msg); + node.error(msg.error, msg) } else { - node.log(err); - node.send(msg); + node.log(err) + node.send(msg) } - }; + } node.on('input', function (msg) { - node.status({}); //clear node status - delete msg.error; //remove any .error property passed in from previous node + node.status({}) // clear node status + delete msg.error // remove any .error property passed in from previous node - //evaluate UI typedInput settings... - let queryMode; - if (['query', 'execute', /*"prepared",*/'bulk'].includes(node.modeOptType)) { - queryMode = node.modeOptType; + // evaluate UI typedInput settings... + let queryMode + if (['query', 'execute', /* "prepared", */'bulk'].includes(node.modeOptType)) { + queryMode = node.modeOptType } else { RED.util.evaluateNodeProperty(node.modeOpt, node.modeOptType, node, msg, (err, value) => { if (err) { - const errmsg = 'Unable to evaluate query mode choice'; - node.processError(errmsg, msg); + const errmsg = 'Unable to evaluate query mode choice' + node.processError(errmsg, msg) } else { - queryMode = value || 'query'; + queryMode = value || 'query' } - }); + }) } - if (!['query', 'execute', /*"prepared",*/'bulk'].includes(queryMode)) { - node.processError(`Query mode '${queryMode}' is not valid. Supported options are 'query' and 'execute'.`, msg); - return null;//halt flow + if (!['query', 'execute', /* "prepared", */'bulk'].includes(queryMode)) { + node.processError(`Query mode '${queryMode}' is not valid. Supported options are 'query' and 'execute'.`, msg) + return null// halt flow } - let query; + let query if (!node.paramsOptType || node.queryOptType === 'editor') { - query = node.query || msg.payload; //legacy + query = node.query || msg.payload // legacy } else { RED.util.evaluateNodeProperty(node.queryOpt, node.queryOptType, node, msg, (err, value) => { if (err) { - const errmsg = 'Unable to evaluate query choice'; - node.processError(errmsg, msg); + const errmsg = 'Unable to evaluate query choice' + node.processError(errmsg, msg) } else { - query = value; + query = value } - }); + }) } - let rows = null; + let rows = null if (queryMode === 'bulk') { if (node.paramsOptType === 'none') { - node.processError('Columns must be provided in bulk mode', msg); - return;//halt flow! + node.processError('Columns must be provided in bulk mode', msg) + return// halt flow! } RED.util.evaluateNodeProperty(node.rows, node.rowsType, node, msg, (err, value) => { if (err) { - const errmsg = 'Unable to evaluate rows field'; - node.processError(errmsg, msg); + const errmsg = 'Unable to evaluate rows field' + node.processError(errmsg, msg) } else { - rows = value; + rows = value } - }); + }) if (!rows || !Array.isArray(rows) || !rows.length || !(Array.isArray(rows[0]) || typeof rows[0] === 'object')) { - node.processError('In bulk mode, rows must be an array of arrays or objects', msg); - return;//halt flow! + node.processError('In bulk mode, rows must be an array of arrays or objects', msg) + return// halt flow! } } - const queryParams = []; - const queryParamValues = {}; + const queryParams = [] + const queryParamValues = {} if (node.paramsOptType === 'none') { - //no params + // no params } else if (!node.paramsOptType || node.paramsOptType === 'editor') { - const _params = node.params || []; + const _params = node.params || [] for (let iParam = 0; iParam < _params.length; iParam++) { - const param = RED.util.cloneMessage(_params[iParam]); + const param = RED.util.cloneMessage(_params[iParam]) if (param.output === false || param.output === 'false') { if (param.valueType === 'jsEpoch') { - param.value = Date.now(); + param.value = Date.now() } else if (param.valueType === 'unixEpoch') { - param.value = Math.floor(Date.now() / 1000); + param.value = Math.floor(Date.now() / 1000) } else if (param.valueType === 'datetime') { - param.value = new Date(); + param.value = new Date() } else if (param.valueType === 'uuidv4') { - param.value = UUID.v4(); + param.value = UUID.v4() } else { RED.util.evaluateNodeProperty(param.value, param.valueType, node, msg, (err, value) => { if (err) { - const errmsg = `Unable to evaluate value for parameter at index [${iParam}] named '${param.name}'`; - node.processError(errmsg, msg); + const errmsg = `Unable to evaluate value for parameter at index [${iParam}] named '${param.name}'` + node.processError(errmsg, msg) } else { - param.value = value; + param.value = value } - }); + }) } } - queryParams.push(param); + queryParams.push(param) } } else { RED.util.evaluateNodeProperty(node.paramsOpt, node.paramsOptType, node, msg, (err, value) => { if (err) { - const errmsg = 'Unable to evaluate parameter choice'; - node.processError(errmsg, msg); + const errmsg = 'Unable to evaluate parameter choice' + node.processError(errmsg, msg) } else { - const _params = value || []; + const _params = value || [] for (let iParam = 0; iParam < _params.length; iParam++) { - const param = RED.util.cloneMessage(_params[iParam]); - queryParams.push(param); + const param = RED.util.cloneMessage(_params[iParam]) + queryParams.push(param) } } - }); + }) } - msg.query = query; - msg.queryMode = queryMode; - msg.queryParams = queryParams || []; + msg.query = query + msg.queryMode = queryMode + msg.queryParams = queryParams || [] - //now validate params, remove superfluous properties & coerce type into sql.type + // now validate params, remove superfluous properties & coerce type into sql.type if (msg.queryParams && msg.queryParams.length) { if (queryMode === 'bulk') { for (let iParam = 0; iParam < msg.queryParams.length; iParam++) { - const param = msg.queryParams[iParam]; + const param = msg.queryParams[iParam] try { - validateBulkColumn(param); - param.type = coerceType(param.type); + validateBulkColumn(param) + param.type = coerceType(param.type) } catch (error) { - node.processError(`Column parameter at index [${iParam}] is not valid. ${error.message}.`, msg); - return null; + node.processError(`Column parameter at index [${iParam}] is not valid. ${error.message}.`, msg) + return null } } } else { for (let iParam = 0; iParam < msg.queryParams.length; iParam++) { - const param = msg.queryParams[iParam]; + const param = msg.queryParams[iParam] try { - validateQueryParam(param); - param.type = coerceType(param.type); + validateQueryParam(param) + param.type = coerceType(param.type) if (param.output) { - delete param.value; + delete param.value } else { - queryParamValues[param.name] = param.value; + queryParamValues[param.name] = param.value } - delete param.valueType; + delete param.valueType } catch (error) { - node.processError(`query parameter at index [${iParam}] is not valid. ${error.message}.`, msg); - return null; + node.processError(`query parameter at index [${iParam}] is not valid. ${error.message}.`, msg) + return null } } } } if (node.parseMustache) { - const promises = []; - const tokens = extractTokens(mustache.parse(msg.query)); - const resolvedTokens = {}; + const promises = [] + const tokens = extractTokens(mustache.parse(msg.query)) + const resolvedTokens = {} tokens.forEach(function (name) { - const envName = parseEnv(name); + const envName = parseEnv(name) if (envName) { const promise = new Promise((resolve, reject) => { - const val = RED.util.evaluateNodeProperty(envName, 'env', node); - resolvedTokens[name] = val; - resolve(); - }); - promises.push(promise); - return; + const val = RED.util.evaluateNodeProperty(envName, 'env', node) + resolvedTokens[name] = val + resolve() + }) + promises.push(promise) + return } - const context = parseContext(name); + const context = parseContext(name) if (context) { - const type = context.type; - const store = context.store; - const field = context.field; - const target = node.context()[type]; + const type = context.type + const store = context.store + const field = context.field + const target = node.context()[type] if (target) { const promise = new Promise((resolve, reject) => { target.get(field, store, (err, val) => { if (err) { - reject(err); + reject(err) } else { - resolvedTokens[name] = val; - resolve(); + resolvedTokens[name] = val + resolve() } - }); - }); - promises.push(promise); + }) + }) + promises.push(promise) } } - }); + }) Promise.all(promises).then(function () { - const value = mustache.render(msg.query, new NodeContext(msg, node.context(), null, false, resolvedTokens)); - msg.query = value; - const values = msg.queryMode === 'bulk' ? rows : queryParamValues; - doSQL(node, msg, values); + const value = mustache.render(msg.query, new NodeContext(msg, node.context(), null, false, resolvedTokens)) + msg.query = value + const values = msg.queryMode === 'bulk' ? rows : queryParamValues + doSQL(node, msg, values) }).catch(function (err) { - node.processError(err, msg); - }); + node.processError(err, msg) + }) } else { - const values = msg.queryMode === 'bulk' ? rows : queryParamValues; - doSQL(node, msg, values); + const values = msg.queryMode === 'bulk' ? rows : queryParamValues + doSQL(node, msg, values) } - }); + }) - function doSQL(node, msg, values) { + function doSQL (node, msg, values) { node.status({ fill: 'blue', shape: 'dot', text: 'requesting' - }); + }) try { mssqlCN.execSql(msg.queryMode, msg.query, msg.queryParams, values, function (err, data, info) { if (err) { - node.processError(err, msg); + node.processError(err, msg) } else { node.status({ fill: 'green', shape: 'dot', text: 'done' - }); - msg.sqlInfo = info; - updateOutputParams(msg.queryParams, data); - setResult(msg, node.outField, data, node.returnType); - node.send(msg); + }) + msg.sqlInfo = info + updateOutputParams(msg.queryParams, data) + setResult(msg, node.outField, data, node.returnType) + node.send(msg) } - }); + }) } catch (err) { - node.processError(err, msg); + node.processError(err, msg) } } node.on('close', function () { - mssqlCN.disconnect(node.id); - }); + mssqlCN.disconnect(node.id) + }) } - RED.nodes.registerType('MSSQL', mssql); -}; + RED.nodes.registerType('MSSQL', mssql) +} diff --git a/test/mssql-plus_spec.js b/test/mssql-plus_spec.js index ce6667f..4381782 100644 --- a/test/mssql-plus_spec.js +++ b/test/mssql-plus_spec.js @@ -1,15 +1,15 @@ /* global describe,beforeEach,afterEach,it */ -const should = require('should'); -const helper = require('node-red-node-test-helper'); -const mssqlPlusNode = require('../src/mssql.js'); +const should = require('should') +const helper = require('node-red-node-test-helper') +const mssqlPlusNode = require('../src/mssql.js') -helper.init(require.resolve('node-red')); +helper.init(require.resolve('node-red')) -let testConnectionConfig; +let testConnectionConfig try { - testConnectionConfig = require('../test/config.json') || {}; + testConnectionConfig = require('../test/config.json') || {} } catch (error) { - testConnectionConfig = {}; + testConnectionConfig = {} } function getConfigNode (id, options) { @@ -31,133 +31,133 @@ function getConfigNode (id, options) { parseJSON: false, enableArithAbort: true, readOnlyIntent: false - }; - const configNode = Object.assign({}, defConf, options); - configNode.id = id || configNode.id; - return configNode; + } + const configNode = Object.assign({}, defConf, options) + configNode.id = id || configNode.id + return configNode } describe('Load MSSQL Plus Node', function () { - 'use strict'; + 'use strict' beforeEach(function (done) { - helper.startServer(done); - }); + helper.startServer(done) + }) afterEach(function (done) { helper.unload().then(function () { - helper.stopServer(done); - }); - }); + helper.stopServer(done) + }) + }) it('should be loaded', function (done) { - const cn = getConfigNode('configNode', testConnectionConfig); + const cn = getConfigNode('configNode', testConnectionConfig) const flow = [ cn, { id: 'helperNode', type: 'helper' }, { id: 'sqlNode', type: 'MSSQL', name: 'mssql', mssqlCN: cn.id, wires: [['helperNode']] } - ]; + ] helper.load(mssqlPlusNode, flow, function () { - const helperNode = helper.getNode('helperNode'); - const sqlNode = helper.getNode('sqlNode'); - const configNode = helper.getNode('configNode'); + const helperNode = helper.getNode('helperNode') + const sqlNode = helper.getNode('sqlNode') + const configNode = helper.getNode('configNode') - should(helperNode).not.be.undefined(); - should(sqlNode).not.be.undefined(); - should(configNode).not.be.undefined(); + should(helperNode).not.be.undefined() + should(sqlNode).not.be.undefined() + should(configNode).not.be.undefined() - sqlNode.should.have.property('type', 'MSSQL'); - sqlNode.should.have.property('modeOptType', 'query'); + sqlNode.should.have.property('type', 'MSSQL') + sqlNode.should.have.property('modeOptType', 'query') - configNode.should.have.property('config'); - configNode.should.have.property('pool'); - configNode.should.have.property('type', 'MSSQL-CN'); + configNode.should.have.property('config') + configNode.should.have.property('pool') + configNode.should.have.property('type', 'MSSQL-CN') - done(); - }); - }); + done() + }) + }) // Dynamic Tests // TODO: expose internal functionality (like row/column creation, coerceType helper functions to permit testing) it('should perform a simple query', function (done) { - this.timeout((testConnectionConfig.requestTimeout || 5000) + 1000); // timeout with an error if done() isn't called within alloted time + this.timeout((testConnectionConfig.requestTimeout || 5000) + 1000) // timeout with an error if done() isn't called within alloted time - const cn = getConfigNode('configNode', testConnectionConfig); + const cn = getConfigNode('configNode', testConnectionConfig) const flow = [ cn, { id: 'helperNode', type: 'helper' }, { id: 'sqlNode', type: 'MSSQL', name: 'mssql', mssqlCN: cn.id, wires: [['helperNode']] } - ]; + ] helper.load(mssqlPlusNode, flow, function () { - const query = 'SELECT GETDATE() as now'; - const helperNode = helper.getNode('helperNode'); - const sqlNode = helper.getNode('sqlNode'); - const configNode = helper.getNode('configNode'); + const query = 'SELECT GETDATE() as now' + const helperNode = helper.getNode('helperNode') + const sqlNode = helper.getNode('sqlNode') + const configNode = helper.getNode('configNode') - configNode.config.user = testConnectionConfig.username; - configNode.config.password = testConnectionConfig.password; - configNode.pool.config.user = testConnectionConfig.username; - configNode.pool.config.password = testConnectionConfig.password; + configNode.config.user = testConnectionConfig.username + configNode.config.password = testConnectionConfig.password + configNode.pool.config.user = testConnectionConfig.username + configNode.pool.config.password = testConnectionConfig.password - configNode.should.have.property('id', 'configNode'); + configNode.should.have.property('id', 'configNode') helperNode.on('input', function (msg) { try { - msg.should.have.property('query', query); - msg.should.have.property('payload'); - should(Array.isArray(msg.payload)).be.true('payload must be an array'); - should(msg.payload.length).eql(1, 'payload array must have 1 element'); - msg.payload[0].should.have.property('now'); - should(msg.payload[0].now).not.be.undefined(); - done(); + msg.should.have.property('query', query) + msg.should.have.property('payload') + should(Array.isArray(msg.payload)).be.true('payload must be an array') + should(msg.payload.length).eql(1, 'payload array must have 1 element') + msg.payload[0].should.have.property('now') + should(msg.payload[0].now).not.be.undefined() + done() } catch (error) { - done(error); + done(error) } - }); + }) - sqlNode.receive({ payload: query }); // fire input of testNode - }); - }); + sqlNode.receive({ payload: query }) // fire input of testNode + }) + }) it('should can create table and insert/select data', function (done) { - const cn = getConfigNode('configNode', testConnectionConfig); + const cn = getConfigNode('configNode', testConnectionConfig) const flow = [ cn, { id: 'helperNode', type: 'helper' }, { id: 'sqlNode', type: 'MSSQL', name: 'mssql', mssqlCN: cn.id, wires: [['helperNode']] } - ]; + ] helper.load(mssqlPlusNode, flow, function () { - const query = "create table #t (id int PRIMARY KEY, data varchar(20)); insert into #t (id, data) values(1, 'a'); insert into #t (id, data) values(2, 'b'); select * from #t;"; - const helperNode = helper.getNode('helperNode'); - const sqlNode = helper.getNode('sqlNode'); - const configNode = helper.getNode('configNode'); + const query = "create table #t (id int PRIMARY KEY, data varchar(20)); insert into #t (id, data) values(1, 'a'); insert into #t (id, data) values(2, 'b'); select * from #t;" + const helperNode = helper.getNode('helperNode') + const sqlNode = helper.getNode('sqlNode') + const configNode = helper.getNode('configNode') - configNode.config.user = testConnectionConfig.username; - configNode.config.password = testConnectionConfig.password; - configNode.pool.config.user = testConnectionConfig.username; - configNode.pool.config.password = testConnectionConfig.password; + configNode.config.user = testConnectionConfig.username + configNode.config.password = testConnectionConfig.password + configNode.pool.config.user = testConnectionConfig.username + configNode.pool.config.password = testConnectionConfig.password - configNode.should.have.property('id', 'configNode'); + configNode.should.have.property('id', 'configNode') helperNode.on('input', function (msg) { try { - msg.should.have.property('query', query); - msg.should.have.property('payload'); - should(Array.isArray(msg.payload)).be.true('payload must be an array'); - should(msg.payload.length).eql(2, 'payload array must have 2 element'); - msg.payload[0].should.have.property('id'); - msg.payload[0].should.have.property('data'); - should(msg.payload[0].id).not.be.undefined(); - done(); + msg.should.have.property('query', query) + msg.should.have.property('payload') + should(Array.isArray(msg.payload)).be.true('payload must be an array') + should(msg.payload.length).eql(2, 'payload array must have 2 element') + msg.payload[0].should.have.property('id') + msg.payload[0].should.have.property('data') + should(msg.payload[0].id).not.be.undefined() + done() } catch (error) { - done(error); + done(error) } - }); + }) - sqlNode.receive({ payload: query }); // fire input of testNode - }); - }); -}); + sqlNode.receive({ payload: query }) // fire input of testNode + }) + }) +})