diff --git a/Common/config/default.json b/Common/config/default.json index c22fe5aa..63fab085 100644 --- a/Common/config/default.json +++ b/Common/config/default.json @@ -178,7 +178,8 @@ "connectionlimit": 10, "max_allowed_packet": 1048575, "pgPoolExtraOptions": {}, - "damengExtraOptions": {} + "damengExtraOptions": {}, + "oracleExtraOptions": {} }, "redis": { "name": "redis", diff --git a/DocService/npm-shrinkwrap.json b/DocService/npm-shrinkwrap.json index 4f04521f..1698e672 100644 --- a/DocService/npm-shrinkwrap.json +++ b/DocService/npm-shrinkwrap.json @@ -1364,6 +1364,11 @@ "ee-first": "1.1.1" } }, + "oracledb": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.0.1.tgz", + "integrity": "sha512-N5/Y4IkCFQCumqjEXi3snrclDKjDMMeawq/z/5Ydm5+BxjV3kg9wCaTAp++JTLlCWASb4JMXqK70PctmAkZh3g==" + }, "packet-reader": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", diff --git a/DocService/package.json b/DocService/package.json index c62e7e69..3129a757 100644 --- a/DocService/package.json +++ b/DocService/package.json @@ -33,6 +33,7 @@ "multi-integer-range": "^4.0.7", "multiparty": "^4.2.1", "mysql2": "^2.3.3", + "oracledb": "^6.0.1", "pg": "^8.8.0", "redis": "^4.6.5", "retry": "^0.12.0", diff --git a/DocService/sources/baseConnector.js b/DocService/sources/baseConnector.js index d184b72e..0ae04cd2 100644 --- a/DocService/sources/baseConnector.js +++ b/DocService/sources/baseConnector.js @@ -36,15 +36,18 @@ var sqlDataBaseType = { mySql : 'mysql', mariaDB : 'mariadb', postgreSql : 'postgres', - dameng : 'dameng' + dameng : 'dameng', + oracle: 'oracle' }; +const connectorUtilities = require('./connectorUtilities'); var bottleneck = require("bottleneck"); var config = require('config'); var configSql = config.get('services.CoAuthoring.sql'); +const dbType = configSql.get('type'); var baseConnector; -switch (configSql.get('type')) { +switch (dbType) { case sqlDataBaseType.mySql: case sqlDataBaseType.mariaDB: baseConnector = require('./mySqlBaseConnector'); @@ -52,11 +55,13 @@ switch (configSql.get('type')) { case sqlDataBaseType.dameng: baseConnector = require('./damengBaseConnector'); break; + case sqlDataBaseType.oracle: + baseConnector = require('./oracleBaseConnector'); + break; default: baseConnector = require('./postgreSqlBaseConnector'); break; } -let constants = require('./../../Common/sources/constants'); const cfgTableResult = configSql.get('tableResult'); const cfgTableChanges = configSql.get('tableChanges'); @@ -108,7 +113,6 @@ exports.insertChangesPromise = function (ctx, objChanges, docId, index, user) { } else { return exports.insertChangesPromiseCompatibility(ctx, objChanges, docId, index, user); } - }; function _getDateTime2(oDate) { return oDate.toISOString().slice(0, 19).replace('T', ' '); @@ -122,10 +126,12 @@ function _insertChangesCallback (ctx, startIndex, objChanges, docId, index, user if (i === l) return; + const indexBytes = 4; + const timeBytes = 8; for (; i < l; ++i, ++index) { - //44 - length of "($1001,... $1007)," + //49 - length of "($1001,... $1008)," //4 is max utf8 bytes per symbol - lengthUtf8Row = 44 + 4 * (docId.length + user.id.length + user.idOriginal.length + user.username.length + objChanges[i].change.length) + 4 + 8; + lengthUtf8Row = 49 + 4 * (ctx.tenant.length + docId.length + user.id.length + user.idOriginal.length + user.username.length + objChanges[i].change.length) + indexBytes + timeBytes; if (lengthUtf8Row + lengthUtf8Current >= maxPacketSize && i > startIndex) { sqlCommand += ';'; (function(tmpStart, tmpIndex) { @@ -275,7 +281,18 @@ exports.healthCheck = function (ctx) { return new Promise(function(resolve, reject) { //SELECT 1; usefull for H2, MySQL, Microsoft SQL Server, PostgreSQL, SQLite //http://stackoverflow.com/questions/3668506/efficient-sql-test-query-or-validation-query-that-will-work-across-all-or-most - baseConnector.sqlQuery(ctx, 'SELECT 1;', function(error, result) { + let sql; + switch (dbType) { + case sqlDataBaseType.oracle: { + sql = 'SELECT 1 FROM DUAL'; + break; + } + default: { + sql = 'SELECT 1;'; + } + } + + baseConnector.sqlQuery(ctx, sql, function(error, result) { if (error) { reject(error); } else { @@ -313,156 +330,6 @@ exports.getTableColumns = function(ctx, tableName) { }); } }; -function UserCallback() { - this.userIndex = undefined; - this.callback = undefined; -} -UserCallback.prototype.fromValues = function(userIndex, callback){ - if(null !== userIndex){ - this.userIndex = userIndex; - } - if(null !== callback){ - this.callback = callback; - } -}; -UserCallback.prototype.delimiter = constants.CHAR_DELIMITER; -UserCallback.prototype.toSQLInsert = function(){ - return this.delimiter + JSON.stringify(this); -}; -UserCallback.prototype.getCallbackByUserIndex = function(ctx, callbacksStr, opt_userIndex) { - ctx.logger.debug("getCallbackByUserIndex: userIndex = %s callbacks = %s", opt_userIndex, callbacksStr); - if (!callbacksStr || !callbacksStr.startsWith(UserCallback.prototype.delimiter)) { - let index = callbacksStr.indexOf(UserCallback.prototype.delimiter); - if (-1 === index) { - //old format - return callbacksStr; - } else { - //mix of old and new format - callbacksStr = callbacksStr.substring(index); - } - } - let callbacks = callbacksStr.split(UserCallback.prototype.delimiter); - let callbackUrl = ""; - for (let i = 1; i < callbacks.length; ++i) { - let callback = JSON.parse(callbacks[i]); - callbackUrl = callback.callback; - if (callback.userIndex === opt_userIndex) { - break; - } - } - return callbackUrl; -}; -UserCallback.prototype.getCallbacks = function(ctx, callbacksStr) { - ctx.logger.debug("getCallbacks: callbacks = %s", callbacksStr); - if (!callbacksStr || !callbacksStr.startsWith(UserCallback.prototype.delimiter)) { - let index = callbacksStr.indexOf(UserCallback.prototype.delimiter); - if (-1 === index) { - //old format - return [callbacksStr]; - } else { - //mix of old and new format - callbacksStr = callbacksStr.substring(index); - } - } - let callbacks = callbacksStr.split(UserCallback.prototype.delimiter); - let res = []; - for (let i = 1; i < callbacks.length; ++i) { - let callback = JSON.parse(callbacks[i]); - res.push(callback.callback); - } - return res; -}; -exports.UserCallback = UserCallback; - -function DocumentPassword() { - this.password = undefined; - this.change = undefined; -} -DocumentPassword.prototype.fromString = function(passwordStr){ - var parsed = JSON.parse(passwordStr); - this.fromValues(parsed.password, parsed.change); -}; -DocumentPassword.prototype.fromValues = function(password, change){ - if(null !== password){ - this.password = password; - } - if(null !== change) { - this.change = change; - } -}; -DocumentPassword.prototype.delimiter = constants.CHAR_DELIMITER; -DocumentPassword.prototype.toSQLInsert = function(){ - return this.delimiter + JSON.stringify(this); -}; -DocumentPassword.prototype.isInitial = function(){ - return !this.change; -}; -DocumentPassword.prototype.getDocPassword = function(ctx, docPasswordStr) { - let res = {initial: undefined, current: undefined, change: undefined}; - if (docPasswordStr) { - ctx.logger.debug("getDocPassword: passwords = %s", docPasswordStr); - let passwords = docPasswordStr.split(UserCallback.prototype.delimiter); - - for (let i = 1; i < passwords.length; ++i) { - let password = new DocumentPassword(); - password.fromString(passwords[i]); - if (password.isInitial()) { - res.initial = password.password; - } else { - res.change = password.change; - } - res.current = password.password; - } - } - return res; -}; -DocumentPassword.prototype.getCurPassword = function(ctx, docPasswordStr) { - let docPassword = this.getDocPassword(ctx, docPasswordStr); - return docPassword.current; -}; -DocumentPassword.prototype.hasPasswordChanges = function(ctx, docPasswordStr) { - let docPassword = this.getDocPassword(ctx, docPasswordStr); - return docPassword.initial !== docPassword.current; -}; -exports.DocumentPassword = DocumentPassword; - -function DocumentAdditional() { - this.data = []; -} -DocumentAdditional.prototype.delimiter = constants.CHAR_DELIMITER; -DocumentAdditional.prototype.toSQLInsert = function() { - if (this.data.length) { - let vals = this.data.map((currentValue) => { - return JSON.stringify(currentValue); - }); - return this.delimiter + vals.join(this.delimiter); - } else { - return null; - } -}; -DocumentAdditional.prototype.fromString = function(str) { - if (!str) { - return; - } - let vals = str.split(this.delimiter).slice(1); - this.data = vals.map((currentValue) => { - return JSON.parse(currentValue); - }); -}; -DocumentAdditional.prototype.setOpenedAt = function(time, timezoneOffset) { - let additional = new DocumentAdditional(); - additional.data.push({time: time, timezoneOffset: timezoneOffset}); - return additional.toSQLInsert(); -}; -DocumentAdditional.prototype.getOpenedAt = function(str) { - let res; - let val = new DocumentAdditional(); - val.fromString(str); - val.data.forEach((elem) => { - if (undefined !== elem.timezoneOffset) { - res = elem.time - (elem.timezoneOffset * 60 * 1000); - } - }); - return res; -}; -exports.DocumentAdditional = DocumentAdditional; +exports.UserCallback = connectorUtilities.UserCallback; +exports.DocumentPassword = connectorUtilities.DocumentPassword; +exports.DocumentAdditional = connectorUtilities.DocumentAdditional; \ No newline at end of file diff --git a/DocService/sources/connectorUtilities.js b/DocService/sources/connectorUtilities.js new file mode 100644 index 00000000..92c0c529 --- /dev/null +++ b/DocService/sources/connectorUtilities.js @@ -0,0 +1,155 @@ +const constants = require('./../../Common/sources/constants'); + +function UserCallback() { + this.userIndex = undefined; + this.callback = undefined; +} +UserCallback.prototype.fromValues = function(userIndex, callback){ + if(null !== userIndex){ + this.userIndex = userIndex; + } + if(null !== callback){ + this.callback = callback; + } +}; +UserCallback.prototype.delimiter = constants.CHAR_DELIMITER; +UserCallback.prototype.toSQLInsert = function(){ + return this.delimiter + JSON.stringify(this); +}; +UserCallback.prototype.getCallbackByUserIndex = function(ctx, callbacksStr, opt_userIndex) { + ctx.logger.debug("getCallbackByUserIndex: userIndex = %s callbacks = %s", opt_userIndex, callbacksStr); + if (!callbacksStr || !callbacksStr.startsWith(UserCallback.prototype.delimiter)) { + let index = callbacksStr.indexOf(UserCallback.prototype.delimiter); + if (-1 === index) { + //old format + return callbacksStr; + } else { + //mix of old and new format + callbacksStr = callbacksStr.substring(index); + } + } + let callbacks = callbacksStr.split(UserCallback.prototype.delimiter); + let callbackUrl = ""; + for (let i = 1; i < callbacks.length; ++i) { + let callback = JSON.parse(callbacks[i]); + callbackUrl = callback.callback; + if (callback.userIndex === opt_userIndex) { + break; + } + } + return callbackUrl; +}; +UserCallback.prototype.getCallbacks = function(ctx, callbacksStr) { + ctx.logger.debug("getCallbacks: callbacks = %s", callbacksStr); + if (!callbacksStr || !callbacksStr.startsWith(UserCallback.prototype.delimiter)) { + let index = callbacksStr.indexOf(UserCallback.prototype.delimiter); + if (-1 === index) { + //old format + return [callbacksStr]; + } else { + //mix of old and new format + callbacksStr = callbacksStr.substring(index); + } + } + let callbacks = callbacksStr.split(UserCallback.prototype.delimiter); + let res = []; + for (let i = 1; i < callbacks.length; ++i) { + let callback = JSON.parse(callbacks[i]); + res.push(callback.callback); + } + return res; +}; +exports.UserCallback = UserCallback; + +function DocumentPassword() { + this.password = undefined; + this.change = undefined; +} +DocumentPassword.prototype.fromString = function(passwordStr){ + var parsed = JSON.parse(passwordStr); + this.fromValues(parsed.password, parsed.change); +}; +DocumentPassword.prototype.fromValues = function(password, change){ + if(null !== password){ + this.password = password; + } + if(null !== change) { + this.change = change; + } +}; +DocumentPassword.prototype.delimiter = constants.CHAR_DELIMITER; +DocumentPassword.prototype.toSQLInsert = function(){ + return this.delimiter + JSON.stringify(this); +}; +DocumentPassword.prototype.isInitial = function(){ + return !this.change; +}; +DocumentPassword.prototype.getDocPassword = function(ctx, docPasswordStr) { + let res = {initial: undefined, current: undefined, change: undefined}; + if (docPasswordStr) { + ctx.logger.debug("getDocPassword: passwords = %s", docPasswordStr); + let passwords = docPasswordStr.split(UserCallback.prototype.delimiter); + + for (let i = 1; i < passwords.length; ++i) { + let password = new DocumentPassword(); + password.fromString(passwords[i]); + if (password.isInitial()) { + res.initial = password.password; + } else { + res.change = password.change; + } + res.current = password.password; + } + } + return res; +}; +DocumentPassword.prototype.getCurPassword = function(ctx, docPasswordStr) { + let docPassword = this.getDocPassword(ctx, docPasswordStr); + return docPassword.current; +}; +DocumentPassword.prototype.hasPasswordChanges = function(ctx, docPasswordStr) { + let docPassword = this.getDocPassword(ctx, docPasswordStr); + return docPassword.initial !== docPassword.current; +}; +exports.DocumentPassword = DocumentPassword; + +function DocumentAdditional() { + this.data = []; +} +DocumentAdditional.prototype.delimiter = constants.CHAR_DELIMITER; +DocumentAdditional.prototype.toSQLInsert = function() { + if (this.data.length) { + let vals = this.data.map((currentValue) => { + return JSON.stringify(currentValue); + }); + return this.delimiter + vals.join(this.delimiter); + } else { + return null; + } +}; +DocumentAdditional.prototype.fromString = function(str) { + if (!str) { + return; + } + let vals = str.split(this.delimiter).slice(1); + this.data = vals.map((currentValue) => { + return JSON.parse(currentValue); + }); +}; +DocumentAdditional.prototype.setOpenedAt = function(time, timezoneOffset) { + let additional = new DocumentAdditional(); + additional.data.push({time: time, timezoneOffset: timezoneOffset}); + return additional.toSQLInsert(); +}; +DocumentAdditional.prototype.getOpenedAt = function(str) { + let res; + let val = new DocumentAdditional(); + val.fromString(str); + val.data.forEach((elem) => { + if (undefined !== elem.timezoneOffset) { + res = elem.time - (elem.timezoneOffset * 60 * 1000); + } + }); + return res; +}; +exports.DocumentAdditional = DocumentAdditional; \ No newline at end of file diff --git a/DocService/sources/damengBaseConnector.js b/DocService/sources/damengBaseConnector.js index 1e8d3bed..815cb620 100644 --- a/DocService/sources/damengBaseConnector.js +++ b/DocService/sources/damengBaseConnector.js @@ -33,7 +33,7 @@ 'use strict'; const co = require('co'); -var sqlBase = require('./baseConnector'); +const connectorUtilities = require('./connectorUtilities'); const db = require("dmdb"); const config = require('config'); @@ -146,7 +146,7 @@ exports.upsert = function(ctx, task, opt_updateUserIndex) { let values = []; let cbInsert = task.callback; if (task.callback) { - let userCallback = new sqlBase.UserCallback(); + let userCallback = new connectorUtilities.UserCallback(); userCallback.fromValues(task.userIndex, task.callback); cbInsert = userCallback.toSQLInsert(); } @@ -166,7 +166,7 @@ exports.upsert = function(ctx, task, opt_updateUserIndex) { sqlCommand += `WHEN MATCHED THEN UPDATE SET last_open_date = ${p9}`; if (task.callback) { let p10 = addSqlParam(JSON.stringify(task.callback), values); - sqlCommand += `, callback = CONCAT(callback , '${sqlBase.UserCallback.prototype.delimiter}{"userIndex":' , (user_index + 1) , ',"callback":', ${p10}, '}')`; + sqlCommand += `, callback = CONCAT(callback , '${connectorUtilities.UserCallback.prototype.delimiter}{"userIndex":' , (user_index + 1) , ',"callback":', ${p10}, '}')`; } if (task.baseurl) { let p11 = addSqlParam(task.baseurl, values); diff --git a/DocService/sources/mySqlBaseConnector.js b/DocService/sources/mySqlBaseConnector.js index 5f5bfc5b..249d17a2 100644 --- a/DocService/sources/mySqlBaseConnector.js +++ b/DocService/sources/mySqlBaseConnector.js @@ -33,7 +33,7 @@ 'use strict'; var mysql = require('mysql2'); -var sqlBase = require('./baseConnector'); +var connectorUtilities = require('./connectorUtilities'); const config = require('config'); const configSql = config.get('services.CoAuthoring.sql'); @@ -92,7 +92,7 @@ exports.upsert = function(ctx, task, opt_updateUserIndex) { let values = []; let cbInsert = task.callback; if (task.callback) { - let userCallback = new sqlBase.UserCallback(); + let userCallback = new connectorUtilities.UserCallback(); userCallback.fromValues(task.userIndex, task.callback); cbInsert = userCallback.toSQLInsert(); } @@ -111,7 +111,7 @@ exports.upsert = function(ctx, task, opt_updateUserIndex) { ` last_open_date = ${p9}`; if (task.callback) { let p10 = addSqlParam(JSON.stringify(task.callback), values); - sqlCommand += `, callback = CONCAT(callback , '${sqlBase.UserCallback.prototype.delimiter}{"userIndex":' , (user_index + 1) , ',"callback":', ${p10}, '}')`; + sqlCommand += `, callback = CONCAT(callback , '${connectorUtilities.UserCallback.prototype.delimiter}{"userIndex":' , (user_index + 1) , ',"callback":', ${p10}, '}')`; } if (task.baseurl) { let p11 = addSqlParam(task.baseurl, values); diff --git a/DocService/sources/oracleBaseConnector.js b/DocService/sources/oracleBaseConnector.js new file mode 100644 index 00000000..03fa7192 --- /dev/null +++ b/DocService/sources/oracleBaseConnector.js @@ -0,0 +1,311 @@ +/* + * (c) Copyright Ascensio System SIA 2010-2023 + * + * This program is a free software product. You can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License (AGPL) + * version 3 as published by the Free Software Foundation. In accordance with + * Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + * that Ascensio System SIA expressly excludes the warranty of non-infringement + * of any third-party rights. + * + * This program is distributed WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + * details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + * + * You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish + * street, Riga, Latvia, EU, LV-1050. + * + * The interactive user interfaces in modified source and object code versions + * of the Program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU AGPL version 3. + * + * Pursuant to Section 7(b) of the License you must retain the original Product + * logo when distributing the program. Pursuant to Section 7(e) we decline to + * grant you any rights under trademark law for use of our trademarks. + * + * All the Product's GUI elements, including illustrations and icon sets, as + * well as technical writing content are licensed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International. See the License + * terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + * + */ + +'use strict'; + +const oracledb = require('oracledb'); +const config = require('config'); +const connectorUtilities = require('./connectorUtilities'); + +const configSql = config.get('services.CoAuthoring.sql'); +const cfgTableResult = configSql.get('tableResult'); +const cfgMaxPacketSize = configSql.get('max_allowed_packet'); + +const connectionConfiguration = { + user: configSql.get('dbUser'), + password: configSql.get('dbPass'), + connectString: `${configSql.get('dbHost')}:${configSql.get('dbPort')}/${configSql.get('dbName')}`, + poolMin: 0, + poolMax: configSql.get('connectionlimit') +}; +const additionalOptions = configSql.get('oracleExtraOptions'); +const configuration = Object.assign({}, connectionConfiguration, additionalOptions); +let pool = null; + +oracledb.fetchAsString = [ oracledb.NCLOB, oracledb.CLOB ]; +oracledb.autoCommit = true; + +function columnsToLowercase(rows) { + const formattedRows = []; + for (const row of rows) { + const newRow = {}; + for (const column in row) { + if (row.hasOwnProperty(column)) { + newRow[column.toLowerCase()] = row[column]; + } + } + + formattedRows.push(newRow); + } + + return formattedRows; +} + +function sqlQuery(ctx, sqlCommand, callbackFunction, opt_noModifyRes = false, opt_noLog = false, opt_values = []) { + return executeQuery(ctx, sqlCommand, opt_values, opt_noModifyRes, opt_noLog).then( + result => callbackFunction?.(null, result), + error => callbackFunction?.(error) + ); +} + +async function executeQuery(ctx, sqlCommand, values = [], noModifyRes = false, noLog = false) { + // Query must not have any ';' in oracle connector. + const correctedSql = sqlCommand.replace(/;/g, ''); + + let connection = null; + try { + if (!pool) { + pool = await oracledb.createPool(configuration); + } + + connection = await pool.getConnection(); + + const bondedValues = values ?? []; + const outputFormat = { outFormat: !noModifyRes ? oracledb.OUT_FORMAT_OBJECT : oracledb.OUT_FORMAT_ARRAY }; + const result = await connection.execute(correctedSql, bondedValues, outputFormat); + + let output = { rows: [], affectedRows: 0 }; + if (!noModifyRes) { + if (result?.rowsAffected) { + output = { affectedRows: result.rowsAffected }; + } + + if (result?.rows) { + output = columnsToLowercase(result.rows); + } + } else { + output = result; + } + + return output; + } catch (error) { + if (!noLog) { + ctx.logger.error(`sqlQuery() error while executing query: ${sqlCommand}\n${error.stack}`); + } + + throw error; + } finally { + connection?.close(); + } +} + +async function executeBunch(ctx, sqlCommand, values = [], noLog = false) { + let connection = null; + try { + if (!pool) { + pool = await oracledb.createPool(configuration); + } + + connection = await pool.getConnection(); + + const result = await connection.executeMany(sqlCommand, values); + + return { affectedRows: result?.rowsAffected ?? 0 }; + } catch (error) { + if (!noLog) { + ctx.logger.error(`sqlQuery() error while executing query: ${sqlCommand}\n${error.stack}`); + } + + throw error; + } finally { + connection?.close(); + } +} + +function addSqlParameter(parameter, accumulatedArray) { + const currentIndex = accumulatedArray.push(parameter) - 1; + return `:${currentIndex}`; +} + +function concatParams(firstParameter, secondParameter) { + return `${firstParameter} || ${secondParameter} || ''`; +} + +function getTableColumns(ctx, tableName) { + return executeQuery(ctx, `SELECT LOWER(column_name) AS column_name FROM user_tab_columns WHERE table_name = '${tableName.toUpperCase()}'`); +} + +function makeUpdateSql(dateNow, task, values, opt_updateUserIndex) { + const lastOpenDate = addSqlParameter(dateNow, values); + + let callback = ''; + if (task.callback) { + const parameter = addSqlParameter(JSON.stringify(task.callback), values); + callback = `, callback = callback || '${connectorUtilities.UserCallback.prototype.delimiter}{"userIndex":' || (user_index + 1) || ',"callback":' || ${parameter} || '}'`; + } + + let baseUrl = ''; + if (task.baseurl) { + const parameter = addSqlParameter(task.baseurl, values); + baseUrl = `, baseurl = ${parameter}`; + } + + let userIndex = ''; + if (opt_updateUserIndex) { + userIndex = ', user_index = user_index + 1'; + } + + const updateQuery = `last_open_date = ${lastOpenDate}${callback}${baseUrl}${userIndex}` + const tenant = addSqlParameter(task.tenant, values); + const id = addSqlParameter(task.key, values); + const condition = `tenant = ${tenant} AND id = ${id}` + + const returning = addSqlParameter({ type: oracledb.NUMBER, dir: oracledb.BIND_OUT }, values); + + return `UPDATE ${cfgTableResult} SET ${updateQuery} WHERE ${condition} RETURNING user_index INTO ${returning}`; +} + +function getReturnedValue(returned) { + return returned?.outBinds?.pop()?.pop(); +} + +async function upsert(ctx, task, opt_updateUserIndex) { + task.completeDefaults(); + + let cbInsert = task.callback; + if (task.callback) { + const userCallback = new connectorUtilities.UserCallback(); + userCallback.fromValues(task.userIndex, task.callback); + cbInsert = userCallback.toSQLInsert(); + } + + const dateNow = new Date(); + + const insertValues = []; + const insertValuesPlaceholder = [ + addSqlParameter(task.tenant, insertValues), + addSqlParameter(task.key, insertValues), + addSqlParameter(task.status, insertValues), + addSqlParameter(task.statusInfo, insertValues), + addSqlParameter(dateNow, insertValues), + addSqlParameter(task.userIndex, insertValues), + addSqlParameter(task.changeId, insertValues), + addSqlParameter(cbInsert, insertValues), + addSqlParameter(task.baseurl, insertValues) + ]; + + const returned = addSqlParameter({ type: oracledb.NUMBER, dir: oracledb.BIND_OUT }, insertValues); + let sqlInsertTry = `INSERT INTO ${cfgTableResult} (tenant, id, status, status_info, last_open_date, user_index, change_id, callback, baseurl) ` + + `VALUES(${insertValuesPlaceholder.join(', ')}) RETURNING user_index INTO ${returned}`; + + try { + const insertResult = await executeQuery(ctx, sqlInsertTry, insertValues, true, true); + const insertId = getReturnedValue(insertResult); + + return { affectedRows: 1, insertId }; + } catch (insertError) { + if (insertError.code !== 'ORA-00001') { + throw insertError; + } + + const values = []; + const updateResult = await executeQuery(ctx, makeUpdateSql(dateNow, task, values, opt_updateUserIndex), values, true); + const insertId = getReturnedValue(updateResult); + + return { affectedRows: 2, insertId }; + } +} + +function insertChanges(ctx, tableChanges, startIndex, objChanges, docId, index, user, callback) { + insertChangesAsync(ctx, tableChanges, startIndex, objChanges, docId, index, user).then( + result => callback(null, result, true), + error => callback(error, null, true) + ); +} + +async function insertChangesAsync(ctx, tableChanges, startIndex, objChanges, docId, index, user) { + if (startIndex === objChanges.length) { + return { affectedRows: 0 }; + } + + const parametersCount = 8; + const maxPlaceholderLength = ':99'.length; + // (parametersCount - 1) - separator symbols length. + const maxInsertStatementLength = `INSERT /*+ APPEND_VALUES*/INTO ${tableChanges} VALUES()`.length + maxPlaceholderLength * parametersCount + (parametersCount - 1); + let packetCapacityReached = false; + + const values = []; + const indexBytes = 4; + const timeBytes = 8; + let lengthUtf8Current = 0; + let currentIndex = startIndex; + for (; currentIndex < objChanges.length; ++currentIndex, ++index) { + // 4 bytes is maximum for utf8 symbol. + const lengthUtf8Row = maxInsertStatementLength + indexBytes + timeBytes + + 4 * (ctx.tenant.length + docId.length + user.id.length + user.idOriginal.length + user.username.length + objChanges[currentIndex].change.length); + + if (lengthUtf8Row + lengthUtf8Current >= cfgMaxPacketSize && currentIndex > startIndex) { + packetCapacityReached = true; + break; + } + + const parameters = [ + ctx.tenant, + docId, + index, + user.id, + user.idOriginal, + user.username, + objChanges[currentIndex].change, + objChanges[currentIndex].time + ]; + + const rowValues = { ...parameters }; + + values.push(rowValues); + lengthUtf8Current += lengthUtf8Row; + } + + const placeholder = []; + for (let i = 0; i < parametersCount; i++) { + placeholder.push(`:${i}`); + } + + const sqlInsert = `INSERT /*+ APPEND_VALUES*/INTO ${tableChanges} VALUES(${placeholder.join(',')})` + const result = await executeBunch(ctx, sqlInsert, values); + + if (packetCapacityReached) { + const recursiveValue = await insertChangesAsync(ctx, tableChanges, currentIndex, objChanges, docId, index, user); + result.affectedRows += recursiveValue.affectedRows; + } + + return result; +} + +module.exports = { + sqlQuery, + addSqlParameter, + concatParams, + getTableColumns, + upsert, + insertChanges +} diff --git a/DocService/sources/postgreSqlBaseConnector.js b/DocService/sources/postgreSqlBaseConnector.js index d185ef0f..9a3b8a5a 100644 --- a/DocService/sources/postgreSqlBaseConnector.js +++ b/DocService/sources/postgreSqlBaseConnector.js @@ -35,7 +35,7 @@ var pg = require('pg'); var co = require('co'); var types = require('pg').types; -var sqlBase = require('./baseConnector'); +const connectorUtilities = require('./connectorUtilities'); const config = require('config'); var configSql = config.get('services.CoAuthoring.sql'); const cfgTableResult = config.get('services.CoAuthoring.sql.tableResult'); @@ -109,7 +109,7 @@ function getUpsertString(task, values) { let dateNow = new Date(); let cbInsert = task.callback; if (isSupportOnConflict && task.callback) { - let userCallback = new sqlBase.UserCallback(); + let userCallback = new connectorUtilities.UserCallback(); userCallback.fromValues(task.userIndex, task.callback); cbInsert = userCallback.toSQLInsert(); } @@ -130,7 +130,7 @@ function getUpsertString(task, values) { sqlCommand += ` ON CONFLICT (tenant, id) DO UPDATE SET last_open_date = ${p9}`; if (task.callback) { let p10 = addSqlParam(JSON.stringify(task.callback), values); - sqlCommand += `, callback = ${cfgTableResult}.callback || '${sqlBase.UserCallback.prototype.delimiter}{"userIndex":' `; + sqlCommand += `, callback = ${cfgTableResult}.callback || '${connectorUtilities.UserCallback.prototype.delimiter}{"userIndex":' `; sqlCommand += ` || (${cfgTableResult}.user_index + 1)::text || ',"callback":' || ${p10}::text || '}'`; } if (task.baseurl) { diff --git a/schema/oracle/createdb.sql b/schema/oracle/createdb.sql new file mode 100644 index 00000000..89de67d8 --- /dev/null +++ b/schema/oracle/createdb.sql @@ -0,0 +1,42 @@ +-- You must be logged in as SYS(sysdba) user. +-- Here, "onlyoffice" is a PBD(service) name. +alter session set container = xepdb1; + +-- In tables creation section "onlyoffice" is a user name. +-- ---------------------------- +-- Table structure for doc_changes +-- ---------------------------- + +CREATE TABLE onlyoffice.doc_changes ( + tenant NVARCHAR2(255) NOT NULL, + id NVARCHAR2(255) NOT NULL, + change_id NUMBER NOT NULL, + user_id NVARCHAR2(255) NOT NULL, + user_id_original NVARCHAR2(255) NOT NULL, + user_name NVARCHAR2(255) NOT NULL, + change_data NCLOB NOT NULL, + change_date TIMESTAMP NOT NULL, + CONSTRAINT doc_changes_unique UNIQUE (tenant, id, change_id), + CONSTRAINT doc_changes_unsigned_int CHECK (change_id between 0 and 4294967295) +); + +-- ---------------------------- +-- Table structure for task_result +-- ---------------------------- + +CREATE TABLE onlyoffice.task_result ( + tenant NVARCHAR2(255) NOT NULL, + id NVARCHAR2(255) NOT NULL, + status NUMBER NOT NULL, + status_info NUMBER NOT NULL, + created_at TIMESTAMP DEFAULT SYSDATE NOT NULL, + last_open_date TIMESTAMP NOT NULL, + user_index NUMBER DEFAULT 1 NOT NULL, + change_id NUMBER DEFAULT 0 NOT NULL, + callback NCLOB, -- codebase uses '' as default values here, but Oracle treat '' as NULL, so NULL permitted for this value. + baseurl NCLOB, -- codebase uses '' as default values here, but Oracle treat '' as NULL, so NULL permitted for this value. + password NCLOB NULL, + additional NCLOB NULL, + CONSTRAINT task_result_unique UNIQUE (tenant, id), + CONSTRAINT task_result_unsigned_int CHECK (user_index BETWEEN 0 AND 4294967295 AND change_id BETWEEN 0 AND 4294967295) +); diff --git a/schema/oracle/removedb.sql b/schema/oracle/removedb.sql new file mode 100644 index 00000000..e4e04ae2 --- /dev/null +++ b/schema/oracle/removedb.sql @@ -0,0 +1,5 @@ +-- You must be logged in as SYS(sysdba) user. +-- Here, "onlyoffice" is a PBD(service) name. +alter session set container = xepdb1; + +DROP USER onlyoffice CASCADE; \ No newline at end of file diff --git a/schema/oracle/removetbl.sql b/schema/oracle/removetbl.sql new file mode 100644 index 00000000..ba79fbde --- /dev/null +++ b/schema/oracle/removetbl.sql @@ -0,0 +1,6 @@ +-- You must be logged in as SYS(sysdba) user. +-- Here, "onlyoffice" is a PBD(service) name. +alter session set container = xepdb1; + +DROP TABLE onlyoffice.doc_changes CASCADE CONSTRAINTS PURGE; +DROP TABLE onlyoffice.task_result CASCADE CONSTRAINTS PURGE; \ No newline at end of file