From 0e3866d9e866582f847859d01bfe64d761ddadca Mon Sep 17 00:00:00 2001 From: adamant-al Date: Wed, 29 Jun 2022 15:15:07 +0300 Subject: [PATCH 01/36] Uppade installation scripts --- tools/install_node.sh | 10 +++++----- tools/install_node_centos.sh | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/install_node.sh b/tools/install_node.sh index 6c9432ef..8a86dcf2 100644 --- a/tools/install_node.sh +++ b/tools/install_node.sh @@ -37,7 +37,7 @@ while getopts 'b:n:' OPTION; do done printf "\n" -printf "Welcome to the ADAMANT node installer v2.0.1 for Ubuntu 18, 20. Make sure you got this file from adamant.im website or GitHub.\n" +printf "Welcome to the ADAMANT node installer v2.0.2 for Ubuntu 18, 20, 22. Make sure you got this file from adamant.im website or GitHub.\n" printf "This installer is the easiest way to run ADAMANT node. We still recommend to consult IT specialist if you are not familiar with Linux systems.\n" printf "You can see full installation instructions on https://medium.com/adamant-im/how-to-run-your-adamant-node-on-ubuntu-990e391e8fcc\n" printf "The installer will ask you to set database and user passwords during the installation.\n" @@ -106,10 +106,10 @@ fi #Packages printf "Updating system packages…\n\n" sudo apt update && sudo apt upgrade -y -printf "\n\nInstalling postgresql, python and other prerequisites…\n\n" +printf "\n\nInstalling postgresql and other prerequisites…\n\n" sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" > /etc/apt/sources.list.d/pgdg.list' && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O - | sudo apt-key add - sudo apt update && sudo DEBIAN_FRONTEND=noninteractive apt-get -yq upgrade -sudo apt install -y python build-essential curl automake autoconf libtool rpl mc git postgresql postgresql-contrib libpq-dev redis-server +sudo apt install -y build-essential curl automake autoconf libtool rpl mc git postgresql postgresql-contrib libpq-dev redis-server #Start postgres. This step is necessary for Windows Subsystem for Linux machines sudo service postgresql start @@ -126,11 +126,11 @@ su - "$username" < Date: Thu, 7 Jul 2022 13:37:30 +0600 Subject: [PATCH 02/36] chore: update socket.io version --- app.js | 6 ++++-- modules/clientWs.js | 5 ++++- package.json | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index a03c5a2c..eb0d1787 100644 --- a/app.js +++ b/app.js @@ -309,7 +309,9 @@ d.run(function () { app.options('*', cors()); var server = require('http').createServer(app); - var io = require('socket.io')(server); + + const { Server } = require('socket.io'); + const io = new Server(server); var privateKey, certificate, https, https_io; @@ -323,7 +325,7 @@ d.run(function () { ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:' + 'ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:' + '!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA' }, app); - https_io = require('socket.io')(https); + https_io = new Server(https); } cb(null, { diff --git a/modules/clientWs.js b/modules/clientWs.js index 66183ec8..ed295421 100644 --- a/modules/clientWs.js +++ b/modules/clientWs.js @@ -1,10 +1,13 @@ +const { Server } = require('socket.io'); + class ClientWs { constructor (config, logger, cb) { if (!config || !config.enabled) { return false; } const port = config.portWS; - const io = require('socket.io')(port); + const io = new Server(port); + this.describes = {}; this.logger = logger; io.sockets.on('connection', (socket) => { diff --git a/package.json b/package.json index 6efe68fc..67d6fe4f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "encryption", "crypto", "cryptocurrency" - ], + ], "author": "ADAMANT Tech Labs , Lisk Foundation , lightcurve GmbH ", "license": "GPL-3.0", "dependencies": { @@ -67,7 +67,7 @@ "redis": "=2.7.1", "rimraf": "=2.5.4", "semver": "=5.3.0", - "socket.io": "^1.7.2", + "socket.io": "^4.5.1", "sodium": "^3.0.2", "sodium-browserify-tweetnacl": "*", "strftime": "=0.10.0", From 30c6c7e832861c796d05d32c5a9c9add834f1b23 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 13:46:09 +0600 Subject: [PATCH 03/36] chore: update bignumber.js version --- helpers/RoundChanges.js | 15 ++++++++++----- helpers/database.js | 2 +- logic/transaction.js | 2 +- package.json | 2 +- test/unit/logic/transaction.js | 2 +- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/helpers/RoundChanges.js b/helpers/RoundChanges.js index ad43c928..7ace61e0 100644 --- a/helpers/RoundChanges.js +++ b/helpers/RoundChanges.js @@ -21,11 +21,16 @@ function RoundChanges (scope) { if (exceptions.rounds[scope.round]) { // Apply rewards factor this.roundRewards.forEach(function (reward, index) { - this.roundRewards[index] = new bignum(reward.toPrecision(15)).times(exceptions.rounds[scope.round].rewards_factor).floor(); + this.roundRewards[index] = new bignum(reward.toPrecision(15)) + .times(exceptions.rounds[scope.round].rewards_factor) + .integerValue(bignum.ROUND_FLOOR); }.bind(this)); // Apply fees factor and bonus - this.roundFees = new bignum(this.roundFees.toPrecision(15)).times(exceptions.rounds[scope.round].fees_factor).plus(exceptions.rounds[scope.round].fees_bonus).floor(); + this.roundFees = new bignum(this.roundFees.toPrecision(15)) + .times(exceptions.rounds[scope.round].fees_factor) + .plus(exceptions.rounds[scope.round].fees_bonus) + .integerValue(bignum.ROUND_FLOOR); } } @@ -39,15 +44,15 @@ function RoundChanges (scope) { * @return {Object} Contains fees, feesRemaining, rewards, balance */ RoundChanges.prototype.at = function (index) { - var fees = new bignum(this.roundFees.toPrecision(15)).dividedBy(slots.delegates).floor(); + var fees = new bignum(this.roundFees.toPrecision(15)).dividedBy(slots.delegates).integerValue(bignum.ROUND_FLOOR); var feesRemaining = new bignum(this.roundFees.toPrecision(15)).minus(fees.times(slots.delegates)); - var rewards = new bignum(this.roundRewards[index].toPrecision(15)).floor() || 0; + var rewards = new bignum(this.roundRewards[index].toPrecision(15)).integerValue(bignum.ROUND_FLOOR) || 0; return { fees: Number(fees.toFixed()), feesRemaining: Number(feesRemaining.toFixed()), rewards: Number(rewards.toFixed()), - balance: Number(fees.add(rewards).toFixed()) + balance: Number(fees.plus(rewards).toFixed()) }; }; diff --git a/helpers/database.js b/helpers/database.js index 9cbe75e3..1449929d 100644 --- a/helpers/database.js +++ b/helpers/database.js @@ -91,7 +91,7 @@ function Migrator (pgp, db) { (file.id && file.name) && fs.statSync(file.path).isFile() && /\.sql$/.test(file.path) ); }).forEach(function (file) { - if (!lastMigration || file.id.greaterThan(lastMigration.id)) { + if (!lastMigration || file.id.isGreaterThan(lastMigration.id)) { pendingMigrations.push(file); } }); diff --git a/logic/transaction.js b/logic/transaction.js index a6e6c3a6..6df61c1a 100644 --- a/logic/transaction.js +++ b/logic/transaction.js @@ -378,7 +378,7 @@ Transaction.prototype.checkConfirmed = function (trs, cb) { * @return {Object} With exceeded boolean and error: address, balance */ Transaction.prototype.checkBalance = function (amount, balance, trs, sender) { - var exceededBalance = new bignum(sender[balance].toString()).lessThan(amount); + var exceededBalance = new bignum(sender[balance].toString()).isLessThan(amount); var exceeded = (trs.blockId !== this.scope.genesisblock.block.id && exceededBalance); return { diff --git a/package.json b/package.json index 67d6fe4f..1bf7c313 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "license": "GPL-3.0", "dependencies": { "async": "=2.1.4", - "bignumber.js": "=4.0.0", + "bignumber.js": "=9.0.2", "bitcore-mnemonic": "=1.1.1", "body-parser": "=1.16.0", "bytebuffer": "=5.0.1", diff --git a/test/unit/logic/transaction.js b/test/unit/logic/transaction.js index def08c4a..360c1512 100644 --- a/test/unit/logic/transaction.js +++ b/test/unit/logic/transaction.js @@ -905,7 +905,7 @@ describe('transaction', function () { accountModule.getAccount({ publicKey: trs.senderPublicKey }, function (err, accountAfter) { var balanceAfter = new bignum(accountAfter.balance.toString()); - expect(balanceBefore.plus(amount.mul(2)).toString()).to.not.equal(balanceAfter.toString()); + expect(balanceBefore.plus(amount.multipliedBy(2)).toString()).to.not.equal(balanceAfter.toString()); expect(balanceBefore.toString()).to.equal(balanceAfter.toString()); done(); }); From caf28f4364f8fd781023f582cc6ada408f268a8c Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 13:49:09 +0600 Subject: [PATCH 04/36] chore: update redis version --- helpers/cache.js | 34 +++++++++++++++------------- modules/cache.js | 59 ++++++++++++++++++++++++++++++++---------------- package.json | 2 +- schema/config.js | 15 ++---------- test/config.json | 6 ++--- 5 files changed, 62 insertions(+), 54 deletions(-) diff --git a/helpers/cache.js b/helpers/cache.js index 45c83f16..c8e1ed86 100644 --- a/helpers/cache.js +++ b/helpers/cache.js @@ -20,23 +20,25 @@ module.exports.connect = function (cacheEnabled, config, logger, cb) { if (config.password === null) { delete config.password; } - var client = redis.createClient(config); - client.on('ready', function () { - logger.info('App connected with redis server'); + var client = redis.createClient(config); - if (!isRedisLoaded) { - isRedisLoaded = true; - return cb(null, { cacheEnabled: cacheEnabled, client: client }); - } - }); + client.connect() + .then(() => { + logger.info('App connected with redis server'); - client.on('error', function (err) { - logger.error('Redis:', err); - // Only throw an error if cache was enabled in config but were unable to load it properly - if (!isRedisLoaded) { - isRedisLoaded = true; - return cb(null, { cacheEnabled: cacheEnabled, client: null }); - } - }); + if (!isRedisLoaded) { + isRedisLoaded = true; + client.ready = isRedisLoaded; + return cb(null, { cacheEnabled: cacheEnabled, client: client }); + } + }) + .catch((err) => { + logger.error('Redis:', err); + // Only throw an error if cache was enabled in config but were unable to load it properly + if (!isRedisLoaded) { + isRedisLoaded = true; + return cb(null, { cacheEnabled: cacheEnabled, client: null }); + } + }); }; diff --git a/modules/cache.js b/modules/cache.js index c9d6eaee..3a3d8052 100644 --- a/modules/cache.js +++ b/modules/cache.js @@ -48,13 +48,14 @@ Cache.prototype.getJsonForKey = function (key, cb) { if (!self.isConnected()) { return cb(errorCacheDisabled); } - client.get(key, function (err, value) { - if (err) { - return cb(err, value); - } - // parsing string to json - return cb(null, JSON.parse(value)); - }); + client.get(key) + .then((value) => { + // parsing string to json + return cb(null, JSON.parse(value)); + }) + .catch((err) => { + return cb(err, key); + }); }; /** @@ -67,8 +68,15 @@ Cache.prototype.setJsonForKey = function (key, value, cb) { if (!self.isConnected()) { return cb(errorCacheDisabled); } + // redis calls toString on objects, which converts it to object [object] so calling stringify before saving - client.set(key, JSON.stringify(value), cb); + client.set(key, JSON.stringify(value)) + .then((res) => { + cb(null, res) + }) + .catch((err) => { + cb(err, value) + }); }; /** @@ -79,7 +87,10 @@ Cache.prototype.deleteJsonForKey = function (key, cb) { if (!self.isConnected()) { return cb(errorCacheDisabled); } - client.del(key, cb); + + client.del(key) + .then((res) => cb(null, res)) + .catch((err) => cb(err, key)); }; /** @@ -93,19 +104,21 @@ Cache.prototype.removeByPattern = function (pattern, cb) { } var keys, cursor = 0; async.doWhilst(function iteratee (whilstCb) { - client.scan(cursor, 'MATCH', pattern, function (err, res) { - if (err) { - return whilstCb(err); - } else { - cursor = Number(res.shift()); - keys = res.shift(); + client.scan(cursor, { MATCH: pattern }) + .then((res) => { + cursor = res.cursor; + keys = res.keys; if (keys.length > 0) { - client.del(keys, whilstCb); + client.del(keys) + .then((res) => whilstCb(null, res)) + .catch((err) => whilstCb(err)); } else { return whilstCb(); } - } - }); + }) + .catch((err) => { + return whilstCb(err); + }); }, function test () { return cursor > 0; }, cb); @@ -119,7 +132,10 @@ Cache.prototype.flushDb = function (cb) { if (!self.isConnected()) { return cb(errorCacheDisabled); } - client.flushdb(cb); + + client.flushDb() + .then((res) => cb(null, res)) + .catch((err) => cb(err)); }; /** @@ -139,7 +155,10 @@ Cache.prototype.quit = function (cb) { // because connection isn't established in the first place. return cb(); } - client.quit(cb); + + client.quit() + .then((res) => cb(null, res)) + .catch((err) => cb(err)); }; /** diff --git a/package.json b/package.json index 1bf7c313..9d67d642 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "pg-promise": "=5.5.6", "popsicle": "=9.0.0", "randomstring": "=1.1.5", - "redis": "=2.7.1", + "redis": "=4.1.0", "rimraf": "=2.5.4", "semver": "=5.3.0", "socket.io": "^4.5.1", diff --git a/schema/config.js b/schema/config.js index 4e9c294b..b3c2811a 100644 --- a/schema/config.js +++ b/schema/config.js @@ -70,25 +70,14 @@ module.exports = { redis: { type: 'object', properties: { - host: { + url: { type: 'string', - format: 'ip' - }, - port: { - type: 'integer', - minimum: 1, - maximum: 65535 - }, - db: { - type: 'integer', - minimum: 0, - maximum: 15 }, password: { type: ['string', 'null'] } }, - required: ['host', 'port', 'db', 'password'] + required: ['url', 'password'] }, api: { type: 'object', diff --git a/test/config.json b/test/config.json index 49424861..aa833504 100644 --- a/test/config.json +++ b/test/config.json @@ -21,9 +21,7 @@ ] }, "redis": { - "host": "127.0.0.1", - "port": 6379, - "db": 1, + "url": "redis://127.0.0.1:6379/1", "password": null }, "api": { @@ -216,6 +214,6 @@ "wsClient": { "portWS": 36665, "enabled": true - }, + }, "nethash": "38f153a81332dea86751451fd992df26a9249f0834f72f58f84ac31cceb70f43" } From a7ea129a1bea7549df2bb47df53ca68b54a8ef79 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 13:59:59 +0600 Subject: [PATCH 05/36] chore: replace old npm api with execa --- modules/chats.js | 1 - modules/dapps.js | 30 ++++++++---------------------- modules/states.js | 1 - package.json | 2 +- 4 files changed, 9 insertions(+), 25 deletions(-) diff --git a/modules/chats.js b/modules/chats.js index 76777e90..efa4d990 100644 --- a/modules/chats.js +++ b/modules/chats.js @@ -5,7 +5,6 @@ var crypto = require('crypto'); var Chat = require('../logic/chat.js'); var extend = require('extend'); var ip = require('ip'); -var npm = require('npm'); var OrderBy = require('../helpers/orderBy.js'); var path = require('path'); var popsicle = require('popsicle'); diff --git a/modules/dapps.js b/modules/dapps.js index cb3f74ba..9835c8aa 100644 --- a/modules/dapps.js +++ b/modules/dapps.js @@ -10,7 +10,7 @@ var extend = require('extend'); var fs = require('fs'); var ip = require('ip'); var InTransfer = require('../logic/inTransfer.js'); -var npm = require('npm'); +var execa = require('execa'); var OrderBy = require('../helpers/orderBy.js'); var OutTransfer = require('../logic/outTransfer.js'); var path = require('path'); @@ -296,27 +296,13 @@ __private.createBasePaths = function (cb) { __private.installDependencies = function (dapp, cb) { var dappPath = path.join(__private.dappsPath, dapp.transactionId); - var packageJson = path.join(dappPath, 'package.json'); - var config = null; - - try { - config = JSON.parse(fs.readFileSync(packageJson)); - } catch (e) { - return setImmediate(cb, 'Failed to open package.json file'); - } - - npm.load(config, function (err) { - if (err) { - return setImmediate(cb, err); - } - - npm.root = path.join(dappPath, 'node_modules'); - npm.prefix = dappPath; - - npm.commands.install(function (err, data) { - return setImmediate(cb, null); - }); - }); + execa('npm', ['install', `-g --prefix ${dappPath}`]) + .then(function () { + return setImmediate(cb, null); + }) + .catch(function (err) { + return setImmediate(cb, err); + }); }; /** diff --git a/modules/states.js b/modules/states.js index e916b111..91ce0724 100644 --- a/modules/states.js +++ b/modules/states.js @@ -5,7 +5,6 @@ var crypto = require('crypto'); var State = require('../logic/state.js'); var extend = require('extend'); var ip = require('ip'); -var npm = require('npm'); var OrderBy = require('../helpers/orderBy.js'); var path = require('path'); var popsicle = require('popsicle'); diff --git a/package.json b/package.json index 9d67d642..072f3bd5 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "cors": "=2.8.1", "decompress-zip": "LiskHQ/decompress-zip#f46b0a3", "ejs": "=2.5.5", + "execa": "^5.1.1", "express": "=4.14.1", "express-domain-middleware": "=0.1.0", "express-query-int": "=1.0.1", @@ -58,7 +59,6 @@ "lisk-sandbox": "LiskHQ/lisk-sandbox#162da38", "lodash": "=4.17.15", "method-override": "=2.3.10", - "npm": "=6.13.4", "pg-monitor": "=0.7.1", "pg-native": "=1.10.0", "pg-promise": "=5.5.6", From c24e7ff81e76b915e4de9a875dda549ae4140cd9 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 14:04:32 +0600 Subject: [PATCH 06/36] chore: replace popsicle with axios --- modules/chats.js | 1 - modules/dapps.js | 41 +++++++++---------- modules/states.js | 1 - modules/transport.js | 16 ++++---- package.json | 2 +- test/node.js | 96 +++++++++++++++++++------------------------- 6 files changed, 71 insertions(+), 86 deletions(-) diff --git a/modules/chats.js b/modules/chats.js index efa4d990..318a00d8 100644 --- a/modules/chats.js +++ b/modules/chats.js @@ -7,7 +7,6 @@ var extend = require('extend'); var ip = require('ip'); var OrderBy = require('../helpers/orderBy.js'); var path = require('path'); -var popsicle = require('popsicle'); var Router = require('../helpers/router.js'); var schema = require('../schema/chats.js'); var sql = require('../sql/chats.js'); diff --git a/modules/dapps.js b/modules/dapps.js index 9835c8aa..ee4d2b98 100644 --- a/modules/dapps.js +++ b/modules/dapps.js @@ -14,7 +14,7 @@ var execa = require('execa'); var OrderBy = require('../helpers/orderBy.js'); var OutTransfer = require('../logic/outTransfer.js'); var path = require('path'); -var popsicle = require('popsicle'); +var axios = require('axios').default; var rmdir = require('rimraf'); var Router = require('../helpers/router.js'); var Sandbox = require('lisk-sandbox'); @@ -379,7 +379,7 @@ __private.removeDApp = function (dapp, cb) { /** * Creates a temp dir, downloads the dapp as stream and decompress it. * @private - * @implements {popsicle} + * @implements {axios} * @implements {DecompressZip} * @param {dapp} dapp * @param {string} dappPath @@ -418,29 +418,28 @@ __private.downloadLink = function (dapp, dappPath, cb) { }); } - var request = popsicle.get({ - url: dapp.link, - transport: popsicle.createTransport({ type: 'stream' }) - }); - - request.then(function (res) { - if (res.status !== 200) { - return setImmediate(serialCb, ['Received bad response code', res.status].join(' ')); - } - - var stream = fs.createWriteStream(tmpPath); + var stream = fs.createWriteStream(tmpPath); - stream.on('error', cleanup); + axios({ + method: 'get', + url: dapp.link, + responseType: 'stream' + }) + .then((res) => { + if (res.status !== 200) { + return setImmediate(serialCb, ['Received bad response code', res.status].join(' ')); + } - stream.on('finish', function () { - library.logger.info(dapp.transactionId, 'Finished downloading'); - stream.close(serialCb); - }); + stream.on('error', cleanup); - res.body.pipe(stream); - }); + stream.on('finish', function () { + library.logger.info(dapp.transactionId, 'Finished downloading'); + stream.close(serialCb); + }); - request.catch(cleanup); + res.data.pipe(writer); + }) + .catch(cleanup); }, decompressZip: function (serialCb) { var unzipper = new DecompressZip(tmpPath); diff --git a/modules/states.js b/modules/states.js index 91ce0724..792769b0 100644 --- a/modules/states.js +++ b/modules/states.js @@ -7,7 +7,6 @@ var extend = require('extend'); var ip = require('ip'); var OrderBy = require('../helpers/orderBy.js'); var path = require('path'); -var popsicle = require('popsicle'); var Router = require('../helpers/router.js'); var schema = require('../schema/states.js'); var sql = require('../sql/states.js'); diff --git a/modules/transport.js b/modules/transport.js index 0b36049d..7953a2fd 100644 --- a/modules/transport.js +++ b/modules/transport.js @@ -9,7 +9,7 @@ var constants = require('../helpers/constants.js'); var crypto = require('crypto'); var extend = require('extend'); var ip = require('ip'); -var popsicle = require('popsicle'); +var axios = require('axios').default; var schema = require('../schema/transport.js'); var sandboxHelper = require('../helpers/sandbox.js'); var sql = require('../sql/transport.js'); @@ -330,7 +330,7 @@ Transport.prototype.getFromRandomPeer = function (config, options, cb) { * - same network (response headers nethash) * - compatible version (response headers version) * Removes peer if error, updates peer otherwise - * @requires {popsicle} + * @requires {axios} * @implements {library.logic.peers.create} * @implements {library.schema.validate} * @implements {modules.system.networkCompatible} @@ -362,12 +362,11 @@ Transport.prototype.getFromPeer = function (peer, options, cb) { }; if (options.data) { - req.body = options.data; + req.data = options.data; } - popsicle.request(req) - .use(popsicle.plugins.parse(['json'], false)) - .then(function (res) { + axios(req) + .then((res) => { if (res.status !== 200) { // Remove peer __private.removePeer({ peer: peer, code: 'ERESPONSE ' + res.status }, req.method + ' ' + req.url); @@ -400,9 +399,10 @@ Transport.prototype.getFromPeer = function (peer, options, cb) { modules.peers.update(peer); - return setImmediate(cb, null, { body: res.body, peer: peer }); + return setImmediate(cb, null, { body: res.data, peer: peer }); } - }).catch(function (err) { + }) + .catch((err) => { if (peer) { __private.removePeer({ peer: peer, code: err.code }, req.method + ' ' + req.url); } diff --git a/package.json b/package.json index 072f3bd5..46e46426 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "license": "GPL-3.0", "dependencies": { "async": "=2.1.4", + "axios": "^0.27.2", "bignumber.js": "=9.0.2", "bitcore-mnemonic": "=1.1.1", "body-parser": "=1.16.0", @@ -62,7 +63,6 @@ "pg-monitor": "=0.7.1", "pg-native": "=1.10.0", "pg-promise": "=5.5.6", - "popsicle": "=9.0.0", "randomstring": "=1.1.5", "redis": "=4.1.0", "rimraf": "=2.5.4", diff --git a/test/node.js b/test/node.js index a7943b3f..58d50ab1 100644 --- a/test/node.js +++ b/test/node.js @@ -23,7 +23,7 @@ node.accounts = require('../helpers/accounts.js'); node._ = require('lodash'); node.async = require('async'); -node.popsicle = require('popsicle'); +node.axios = require('axios').default; node.expect = require('chai').expect; node.chai = require('chai'); node.chai.config.includeStack = true; @@ -468,21 +468,17 @@ node.getId = function (trs) { // Returns current block height node.getHeight = function (cb) { - var request = node.popsicle.get(node.baseUrl + '/api/blocks/getHeight'); - - request.use(node.popsicle.plugins.parse(['json'])); - - request.then(function (res) { - if (res.status !== 200) { - return setImmediate(cb, ['Received bad response code', res.status, res.url].join(' ')); - } else { - return setImmediate(cb, null, res.body.height); - } - }); - - request.catch(function (err) { - return setImmediate(cb, err); - }); + node.axios.get(node.baseUrl + '/api/blocks/getHeight') + .then((res) => { + if (res.status !== 200 || !res.data) { + return setImmediate(cb, ['Received bad response code', res.status, res.config.url].join(' ')); + } else { + return setImmediate(cb, null, res.data.height); + } + }) + .catch((err) => { + return setImmediate(cb, err); + }); }; // Run callback on new round @@ -538,27 +534,23 @@ node.waitForNewBlock = function (height, blocksToWait, cb) { node.async.doWhilst( function (cb) { - var request = node.popsicle.get(node.baseUrl + '/api/blocks/getHeight'); - - request.use(node.popsicle.plugins.parse(['json'])); - - request.then(function (res) { - if (res.status !== 200) { - return cb(['Received bad response code', res.status, res.url].join(' ')); - } - - node.debug('== Waiting for block:'.grey, 'Height:'.grey, res.body.height, 'Target:'.grey, target, 'Second:'.grey, counter++); - - if (res.body.height >= target) { - height = res.body.height; - } - - setTimeout(cb, 1000); - }); - - request.catch(function (err) { - return cb(err); - }); + node.axios.get(node.baseUrl + '/api/blocks/getHeight') + .then((res) => { + if (res.status !== 200) { + return cb(['Received bad response code', res.status, res.config.url].join(' ')); + } + + node.debug('== Waiting for block:'.grey, 'Height:'.grey, res.data.height, 'Target:'.grey, target, 'Second:'.grey, counter++); + + if (res.data.height >= target) { + height = res.data.height; + } + + setTimeout(cb, 1000); + }) + .catch(function (err) { + return cb(err); + }); }, function () { return actualHeight === height; @@ -586,7 +578,7 @@ node.addPeers = function (numOfPeers, ip, cb) { os = operatingSystems[node.randomizeSelection(operatingSystems.length)]; version = node.version; - var request = node.popsicle.get({ + node.axios.get({ url: node.baseUrl + '/peer/height', headers: { broadhash: node.config.nethash, @@ -598,22 +590,18 @@ node.addPeers = function (numOfPeers, ip, cb) { version: version, nonce: 'randomNonce' } - }); - - request.use(node.popsicle.plugins.parse(['json'])); - - request.then(function (res) { - if (res.status !== 200) { - return next(['Received bad response code', res.status, res.url].join(' ')); - } else { - i++; - next(); - } - }); - - request.catch(function (err) { - return next(err); - }); + }) + .then(function (res) { + if (res.status !== 200) { + return next(['Received bad response code', res.status, res.config.url].join(' ')); + } else { + i++; + next(); + } + }) + .catch(function (err) { + return next(err); + }); }, function (err) { // Wait for peer to be swept to db setTimeout(function () { From bfeb15ae2395e1256994a15eba2ce823a6f523a9 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 14:14:47 +0600 Subject: [PATCH 07/36] chore: update async version --- modules/cache.js | 4 ++-- modules/loader.js | 8 ++++---- modules/sql.js | 4 ++-- package.json | 2 +- test/api/multisignatures.js | 4 ++-- test/api/peer.transactions.main.js | 4 ++-- test/api/peer.transactions.stress.js | 8 ++++---- test/node.js | 8 ++++---- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/modules/cache.js b/modules/cache.js index 3a3d8052..ba346c0f 100644 --- a/modules/cache.js +++ b/modules/cache.js @@ -119,8 +119,8 @@ Cache.prototype.removeByPattern = function (pattern, cb) { .catch((err) => { return whilstCb(err); }); - }, function test () { - return cursor > 0; + }, function test (...args) { + return args[args.length - 1](null, cursor > 0); }, cb); }; diff --git a/modules/loader.js b/modules/loader.js index c8c35825..95bd8001 100644 --- a/modules/loader.js +++ b/modules/loader.js @@ -342,8 +342,8 @@ __private.loadBlockChain = function () { }, loadBlocksOffset: function (seriesCb) { async.until( - function () { - return count < offset; + function (testCb) { + return testCb(null, count < offset); }, function (cb) { if (count > 1) { library.logger.info('Rebuilding blockchain, current block height: ' + (offset + 1)); @@ -532,8 +532,8 @@ __private.loadBlocksFromNetwork = function (cb) { return setImmediate(cb, err); } else { async.whilst( - function () { - return !loaded && errorCount < 5; + function (testCb) { + return testCb(null, !loaded && errorCount < 5); }, function (next) { var peer = network.peers[Math.floor(Math.random() * network.peers.length)]; diff --git a/modules/sql.js b/modules/sql.js index 00c8281f..e4a3a957 100644 --- a/modules/sql.js +++ b/modules/sql.js @@ -159,9 +159,9 @@ __private.query = function (action, config, cb) { } else { var batchPack = []; async.until( - function () { + function (testCb) { batchPack = config.values.splice(0, 10); - return batchPack.length === 0; + return testCb(null, batchPack.length === 0); }, function (cb) { var fields = Object.keys(config.fields).map(function (field) { return __private.escape2(config.fields[field]); // Add double quotes to field identifiers diff --git a/package.json b/package.json index 46e46426..6c19d32b 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "author": "ADAMANT Tech Labs , Lisk Foundation , lightcurve GmbH ", "license": "GPL-3.0", "dependencies": { - "async": "=2.1.4", + "async": "=3.2.4", "axios": "^0.27.2", "bignumber.js": "=9.0.2", "bitcore-mnemonic": "=1.1.1", diff --git a/test/api/multisignatures.js b/test/api/multisignatures.js index e0bd5278..41904196 100644 --- a/test/api/multisignatures.js +++ b/test/api/multisignatures.js @@ -57,8 +57,8 @@ function confirmTransaction (transactionId, passphrases, done) { var count = 0; async.until( - function () { - return (count >= passphrases.length); + function (testCb) { + return testCb(null, count >= passphrases.length); }, function (untilCb) { var passphrase = passphrases[count]; diff --git a/test/api/peer.transactions.main.js b/test/api/peer.transactions.main.js index 5601334f..c363e495 100644 --- a/test/api/peer.transactions.main.js +++ b/test/api/peer.transactions.main.js @@ -268,8 +268,8 @@ describe('POST /peer/transactions', function () { count++; return next(); }); - }, function () { - return count === 10; + }, function (testCb) { + return testCb(null, count === 10); }, function () { return done(); }); diff --git a/test/api/peer.transactions.stress.js b/test/api/peer.transactions.stress.js index 732a867e..7a0eabc8 100644 --- a/test/api/peer.transactions.stress.js +++ b/test/api/peer.transactions.stress.js @@ -60,8 +60,8 @@ describe('POST /peer/transactions', function () { next(); }); }); - }, function () { - return (count >= maximum); + }, function (testCb) { + return testCb(null, count >= maximum); }, function (err) { done(err); }); @@ -110,8 +110,8 @@ describe('POST /peer/transactions', function () { count++; next(); }); - }, function () { - return (count >= maximum); + }, function (testCb) { + return testCb(null, count >= maximum); }, function (err) { done(err); }); diff --git a/test/node.js b/test/node.js index 58d50ab1..40d06a39 100644 --- a/test/node.js +++ b/test/node.js @@ -552,8 +552,8 @@ node.waitForNewBlock = function (height, blocksToWait, cb) { return cb(err); }); }, - function () { - return actualHeight === height; + function (testCb) { + return testCb(null, actualHeight === height); }, function (err) { if (err) { @@ -572,8 +572,8 @@ node.addPeers = function (numOfPeers, ip, cb) { var os, version; var i = 0; - node.async.whilst(function () { - return i < numOfPeers; + node.async.whilst(function (testCb) { + return testCb(null, i < numOfPeers); }, function (next) { os = operatingSystems[node.randomizeSelection(operatingSystems.length)]; version = node.version; From e88f50a7b4ae7bde7197dc25f3c00c8b77cff8f6 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 14:32:09 +0600 Subject: [PATCH 08/36] chore: update pg-related dependencies --- helpers/database.js | 4 ++-- helpers/inserts.js | 4 ++-- package.json | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/helpers/database.js b/helpers/database.js index 1449929d..a56dac68 100644 --- a/helpers/database.js +++ b/helpers/database.js @@ -190,10 +190,10 @@ module.exports.connect = function (config, logger, cb) { monitor.attach(pgOptions, config.logEvents); monitor.setTheme('matrix'); - monitor.log = function (msg, info) { + monitor.setLog(function (msg, info) { logger.log(info.event, info.text); info.display = false; - }; + }); config.user = config.user || process.env.USER; diff --git a/helpers/inserts.js b/helpers/inserts.js index abe04a8b..42517125 100644 --- a/helpers/inserts.js +++ b/helpers/inserts.js @@ -52,9 +52,9 @@ function Inserts (record, values, concat) { return pgp.as.format('INSERT INTO $1~($2^) VALUES $3^', [record.table, fields, values]); }; - this._rawDBType = true; + this.rawType = true; - this.formatDBType = function () { + this.toPostgres = function () { return values.map(function (v) { return '(' + pgp.as.format(self._template, v) + ')'; }).join(','); diff --git a/package.json b/package.json index 6c19d32b..f588a956 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,9 @@ "lisk-sandbox": "LiskHQ/lisk-sandbox#162da38", "lodash": "=4.17.15", "method-override": "=2.3.10", - "pg-monitor": "=0.7.1", - "pg-native": "=1.10.0", - "pg-promise": "=5.5.6", + "pg-monitor": "=1.4.1", + "pg-native": "=3.0.0", + "pg-promise": "=10.11.1", "randomstring": "=1.1.5", "redis": "=4.1.0", "rimraf": "=2.5.4", From 1aeff5aea01b257d66fd67b6ac14f5ad8d594c60 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 14:49:09 +0600 Subject: [PATCH 09/36] chore: update commander version --- app.js | 28 +++++++++++++++------------- package.json | 2 +- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app.js b/app.js index eb0d1787..3953d41b 100644 --- a/app.js +++ b/app.js @@ -65,24 +65,26 @@ program .option('-s, --snapshot ', 'verify snapshot') .parse(process.argv); +var programOpts = program.opts(); + /** * @property {object} - The default list of configuration options. Can be updated by CLI. * @default 'config.json' */ -var appConfig = require('./helpers/config.js')(program.config); -var genesisblock = require(path.resolve(process.cwd(), (program.genesis || 'genesisBlock.json'))); +var appConfig = require('./helpers/config.js')(programOpts.config); +var genesisblock = require(path.resolve(process.cwd(), (programOpts.genesis || 'genesisBlock.json'))); -if (program.port) { - appConfig.port = program.port; +if (programOpts.port) { + appConfig.port = programOpts.port; } -if (program.address) { - appConfig.address = program.address; +if (programOpts.address) { + appConfig.address = programOpts.address; } -if (program.peers) { - if (typeof program.peers === 'string') { - appConfig.peers.list = program.peers.split(',').map(function (peer) { +if (programOpts.peers) { + if (typeof programOpts.peers === 'string') { + appConfig.peers.list = programOpts.peers.split(',').map(function (peer) { peer = peer.split(':'); return { ip: peer.shift(), @@ -94,13 +96,13 @@ if (program.peers) { } } -if (program.log) { - appConfig.consoleLogLevel = program.log; +if (programOpts.log) { + appConfig.consoleLogLevel = programOpts.log; } -if (program.snapshot) { +if (programOpts.snapshot) { appConfig.loading.snapshot = Math.abs( - Math.floor(program.snapshot) + Math.floor(programOpts.snapshot) ); } diff --git a/package.json b/package.json index f588a956..525f9620 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "bytebuffer": "=5.0.1", "change-case": "=3.0.1", "colors": "=1.1.2", - "commander": "=2.9.0", + "commander": "=9.3.0", "compression": "=1.6.2", "cors": "=2.8.1", "decompress-zip": "LiskHQ/decompress-zip#f46b0a3", From 672207bfa577d3d02b1d664856dbbc717c3e655f Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 15:13:45 +0600 Subject: [PATCH 10/36] chore: update express version --- helpers/request-limiter.js | 7 ++++--- package.json | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/helpers/request-limiter.js b/helpers/request-limiter.js index f5548677..0932bd5f 100644 --- a/helpers/request-limiter.js +++ b/helpers/request-limiter.js @@ -1,6 +1,7 @@ 'use strict'; -var RateLimit = require('express-rate-limit'); +var rateLimit = require('express-rate-limit'); +var slowDown = require('express-slow-down'); var defaults = { max: 0, // Disabled @@ -54,8 +55,8 @@ module.exports = function (app, config) { }; limits.middleware = { - client: app.use('/api/', new RateLimit(limits.client)), - peer: app.use('/peer/', new RateLimit(limits.peer)) + client: app.use('/api/', rateLimit(limits.client), slowDown(limits.client)), + peer: app.use('/peer/', rateLimit(limits.peer), slowDown(limits.peer)) }; return limits; diff --git a/package.json b/package.json index 525f9620..d815c707 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,11 @@ "decompress-zip": "LiskHQ/decompress-zip#f46b0a3", "ejs": "=2.5.5", "execa": "^5.1.1", - "express": "=4.14.1", + "express": "=4.18.1", "express-domain-middleware": "=0.1.0", - "express-query-int": "=1.0.1", - "express-rate-limit": "=2.6.0", + "express-query-int": "=3.0.0", + "express-rate-limit": "=6.4.0", + "express-slow-down": "1.4.0", "extend": "=3.0.2", "ip": "=1.1.5", "js-nacl": "^1.2.2", From 3094c425e07d96f1e46eca2faa575a5cf21118ee Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 15:19:28 +0600 Subject: [PATCH 11/36] chore: update dev dependencies --- Gruntfile.js | 2 +- package.json | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 9fab42df..d57fc772 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -107,7 +107,7 @@ module.exports = function (grunt) { eslint: { options: { - configFile: '.eslintrc.json', + overrideConfigFile: '.eslintrc.json', format: 'codeframe', fix: false }, diff --git a/package.json b/package.json index d815c707..44dcf29d 100644 --- a/package.json +++ b/package.json @@ -78,22 +78,23 @@ "z-schema": "=3.18.2" }, "devDependencies": { - "chai": "^4.3.4", + "chai": "^4.3.6", "chai-bignumber": "^3.0.0", "eslint-config-google": "^0.14.0", - "grunt": "^1.4.1", + "eslint-formatter-codeframe": "^7.32.1", + "grunt": "^1.5.3", "grunt-cli": "^1.4.3", "grunt-contrib-compress": "^2.0.0", - "grunt-eslint": "^23.0.0", - "grunt-exec": "^3.0.0", "grunt-contrib-obfuscator": "^8.0.0", - "jsdoc": "^3.6.7", - "mocha": "^9.1.1", - "moment": "^2.29.1", + "grunt-eslint": "^24.0.0", + "grunt-exec": "^3.0.0", + "jsdoc": "^3.6.10", + "mocha": "^10.0.0", + "moment": "^2.29.3", "nyc": "^15.1.0", - "nyc-middleware": "^1.0.2", - "sinon": "^11.1.2", - "supertest": "^6.1.6" + "nyc-middleware": "^1.0.4", + "sinon": "^14.0.0", + "supertest": "^6.2.3" }, "config": { "minVersion": ">=0.4.0" From 991b66741d65db86b73fbeeec3231efe436e7f86 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 15:21:00 +0600 Subject: [PATCH 12/36] fix: npm script to server jsdoc --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 44dcf29d..8b1b4212 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "eslint:fixrule": "npx eslint -f visualstudio --no-eslintrc --fix --env browser,node,es2021,commonjs --parser-options=ecmaVersion:12 --rule", "fetchCoverage": "npx grunt exec:fetchCoverage --verbose", "jsdoc": "npx jsdoc -c docs/conf.json --verbose --pedantic", - "server-docs": "npx jsdoc && http-server docs/jsdoc/" + "server-docs": "npx jsdoc && npx http-server docs/jsdoc/" }, "keywords": [ "adm", From ec6e8cdb75981da476d44dca538ce11a0f365e4c Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 15:25:43 +0600 Subject: [PATCH 13/36] chore: update non-breakable dependencies and fix some vulnerabilities --- package.json | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 8b1b4212..b38b502e 100644 --- a/package.json +++ b/package.json @@ -37,16 +37,16 @@ "async": "=3.2.4", "axios": "^0.27.2", "bignumber.js": "=9.0.2", - "bitcore-mnemonic": "=1.1.1", - "body-parser": "=1.16.0", + "bitcore-mnemonic": "=8.25.30", + "body-parser": "=1.20.0", "bytebuffer": "=5.0.1", - "change-case": "=3.0.1", - "colors": "=1.1.2", + "change-case": "=4.1.2", + "colors": "=1.4.0", "commander": "=9.3.0", - "compression": "=1.6.2", - "cors": "=2.8.1", + "compression": "=1.7.4", + "cors": "=2.8.5", "decompress-zip": "LiskHQ/decompress-zip#f46b0a3", - "ejs": "=2.5.5", + "ejs": "=3.1.8", "execa": "^5.1.1", "express": "=4.18.1", "express-domain-middleware": "=0.1.0", @@ -54,28 +54,28 @@ "express-rate-limit": "=6.4.0", "express-slow-down": "1.4.0", "extend": "=3.0.2", - "ip": "=1.1.5", - "js-nacl": "^1.2.2", - "json-schema": "=0.2.3", + "ip": "=1.1.8", + "js-nacl": "^1.4.0", + "json-schema": "=0.4.0", "json-sql": "LiskHQ/json-sql#27e1ed1", "lisk-sandbox": "LiskHQ/lisk-sandbox#162da38", - "lodash": "=4.17.15", - "method-override": "=2.3.10", + "lodash": "=4.17.21", + "method-override": "=3.0.0", + "npm": "=8.13.1", "pg-monitor": "=1.4.1", "pg-native": "=3.0.0", "pg-promise": "=10.11.1", - "randomstring": "=1.1.5", + "randomstring": "=1.2.2", "redis": "=4.1.0", - "rimraf": "=2.5.4", - "semver": "=5.3.0", + "rimraf": "=3.0.2", + "semver": "=7.3.7", "socket.io": "^4.5.1", "sodium": "^3.0.2", "sodium-browserify-tweetnacl": "*", - "strftime": "=0.10.0", + "strftime": "=0.10.1", "valid-url": "=1.0.9", - "validator": "=6.2.1", - "validator.js": "=2.0.3", - "z-schema": "=3.18.2" + "validator.js": "=2.0.4", + "z-schema": "=5.0.3" }, "devDependencies": { "chai": "^4.3.6", @@ -96,6 +96,14 @@ "sinon": "^14.0.0", "supertest": "^6.2.3" }, + "overrides": { + "grunt-contrib-obfuscator": { + "javascript-obfuscator": "^4.0.0" + }, + "json-sql": { + "underscore": "^1.13.4" + } + }, "config": { "minVersion": ">=0.4.0" }, From 0c16df6aa2e508d5d028ca60d4afad6e5afd42e8 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 15:45:34 +0600 Subject: [PATCH 14/36] fix: eslint errors --- config.json | 404 +++++++++++++++++++++++++++++----------- helpers/RoundChanges.js | 10 +- helpers/cache.js | 32 ++-- modules/cache.js | 66 +++---- schema/config.js | 2 +- 5 files changed, 348 insertions(+), 166 deletions(-) diff --git a/config.json b/config.json index 1d59e3fd..6b050073 100644 --- a/config.json +++ b/config.json @@ -1,117 +1,299 @@ -{ - "port": 36666, - "address": "0.0.0.0", - "fileLogLevel": "warn", - "logFileName": "logs/adamant.log", - "consoleLogLevel": "info", - "trustProxy": false, - "topAccounts": false, - "cacheEnabled": false, - "db": { - "host": "localhost", - "port": 5432, - "database": "adamant_main", - "user": "adamant", - "password": "password", - "poolSize": 95, - "poolIdleTimeout": 30000, - "reapIntervalMillis": 1000, - "logEvents": [ - "error" - ] - }, - "redis": { - "host": "127.0.0.1", - "port": 6379, - "db": 0, - "password": null - }, - "api": { - "enabled": true, - "access": { - "public": false, - "whiteList": [ - "127.0.0.1" - ] +'use strict'; + +module.exports = { + config: { + id: 'appCon', + type: 'object', + properties: { + port: { + type: 'integer', + minimum: 1, + maximum: 65535 + }, + address: { + type: 'string', + format: 'ip' + }, + fileLogLevel: { + type: 'string' + }, + logFileName: { + type: 'string' + }, + consoleLogLevel: { + type: 'string' + }, + trustProxy: { + type: 'boolean' + }, + topAccounts: { + type: 'boolean' + }, + cacheEnabled: { + type: 'boolean' + }, + db: { + type: 'object', + properties: { + host: { + type: 'string' + }, + port: { + type: 'integer', + minimum: 1, + maximum: 65535 + }, + database: { + type: 'string' + }, + user: { + type: 'string' + }, + password: { + type: 'string' + }, + poolSize: { + type: 'integer' + }, + poolIdleTimeout: { + type: 'integer' + }, + reapIntervalMillis: { + type: 'integer' + }, + logEvents: { + type: 'array' + } }, - "options": { - "limits": { - "max": 0, - "delayMs": 0, - "delayAfter": 0, - "windowMs": 60000 - } - } - }, - "peers": { - "enabled": true, - "list": [ - { - "ip": "51.15.221.205", - "port": 36666 + required: ['host', 'port', 'database', 'user', 'password', 'poolSize', 'poolIdleTimeout', 'reapIntervalMillis', 'logEvents'] + }, + redis: { + type: 'object', + properties: { + url: { + type: 'string' + }, + password: { + type: ['string', 'null'] + } + }, + required: ['url', 'password'] + }, + api: { + type: 'object', + properties: { + enabled: { + type: 'boolean' + }, + access: { + type: 'object', + properties: { + public: { + type: 'boolean' + }, + whiteList: { + type: 'array' + } }, - { - "ip": "51.15.88.53", - "port": 36666 + required: ['public', 'whiteList'] + }, + options: { + type: 'object', + properties: { + limits: { + type: 'object', + properties: { + max: { + type: 'integer' + }, + delayMs: { + type: 'integer' + }, + delayAfter: { + type: 'integer' + }, + windowMs: { + type: 'integer' + } + }, + required: ['max', 'delayMs', 'delayAfter', 'windowMs'] + } }, - { - "ip": "51.15.217.230", - "port": 36666 - } - ], - "access": { - "blackList": [] + required: ['limits'] + } }, - "options": { - "limits": { - "max": 0, - "delayMs": 0, - "delayAfter": 0, - "windowMs": 60000 + required: ['enabled', 'access', 'options'] + }, + peers: { + type: 'object', + properties: { + enabled: { + type: 'boolean' + }, + list: { + type: 'array' + }, + access: { + type: 'object', + properties: { + blackList: { + type: 'array' + } }, - "timeout": 5000 - } - }, - "broadcasts": { - "broadcastInterval": 1500, - "broadcastLimit": 20, - "parallelLimit": 20, - "releaseLimit": 25, - "relayLimit": 3 - }, - "transactions": { - "maxTxsPerQueue": 1000 - }, - "forging": { - "force": false, - "secret": [], - "access": { - "whiteList": [ - "127.0.0.1" - ] - } - }, - "loading": { - "verifyOnLoading": false, - "loadPerIteration": 5000 - }, - "ssl": { - "enabled": false, - "options": { - "port": 443, - "address": "0.0.0.0", - "key": "./ssl/lisk.key", - "cert": "./ssl/lisk.crt" - } - }, - "dapp": { - "masterrequired": true, - "masterpassword": "", - "autoexec": [] - }, - "wsClient": { - "portWS": 36668, - "enabled": true + required: ['blackList'] + }, + options: { + properties: { + limits: { + type: 'object', + properties: { + max: { + type: 'integer' + }, + delayMs: { + type: 'integer' + }, + delayAfter: { + type: 'integer' + }, + windowMs: { + type: 'integer' + } + }, + required: ['max', 'delayMs', 'delayAfter', 'windowMs'] + }, + timeout: { + type: 'integer' + } + }, + required: ['limits', 'timeout'] + } + }, + required: ['enabled', 'list', 'access', 'options'] + }, + broadcasts: { + type: 'object', + properties: { + broadcastInterval: { + type: 'integer', + minimum: 1000, + maximum: 60000 + }, + broadcastLimit: { + type: 'integer', + minimum: 1, + maximum: 100 + }, + parallelLimit: { + type: 'integer', + minimum: 1, + maximum: 100 + }, + releaseLimit: { + type: 'integer', + minimum: 1, + maximum: 25 + }, + relayLimit: { + type: 'integer', + minimum: 1, + maximum: 100 + } + }, + required: ['broadcastInterval', 'broadcastLimit', 'parallelLimit', 'releaseLimit', 'relayLimit'] + }, + transactions: { + type: 'object', + maxTxsPerQueue: { + type: 'integer', + minimum: 100, + maximum: 5000 + }, + required: ['maxTxsPerQueue'] + }, + forging: { + type: 'object', + properties: { + force: { + type: 'boolean' + }, + secret: { + type: 'array' + }, + access: { + type: 'object', + properties: { + whiteList: { + type: 'array' + } + }, + required: ['whiteList'] + } + }, + required: ['force', 'secret', 'access'] + }, + loading: { + type: 'object', + properties: { + verifyOnLoading: { + type: 'boolean' + }, + loadPerIteration: { + type: 'integer', + minimum: 1, + maximum: 5000 + } + }, + required: ['verifyOnLoading', 'loadPerIteration'] + }, + ssl: { + type: 'object', + properties: { + enabled: { + type: 'boolean' + }, + options: { + type: 'object', + properties: { + port: { + type: 'integer' + }, + address: { + type: 'string', + format: 'ip' + }, + key: { + type: 'string' + }, + cert: { + type: 'string' + } + }, + required: ['port', 'address', 'key', 'cert'] + } + }, + required: ['enabled', 'options'] + }, + dapp: { + type: 'object', + properties: { + masterrequired: { + type: 'boolean' + }, + masterpassword: { + type: 'string' + }, + autoexec: { + type: 'array' + } + }, + required: ['masterrequired', 'masterpassword', 'autoexec'] + }, + nethash: { + type: 'string', + format: 'hex' + } }, - "nethash": "bd330166898377fb28743ceef5e43a5d9d0a3efd9b3451fb7bc53530bb0a6d64" -} + required: ['port', 'address', 'fileLogLevel', 'logFileName', 'consoleLogLevel', 'trustProxy', 'topAccounts', 'db', 'api', 'peers', 'broadcasts', 'transactions', 'forging', 'loading', 'ssl', 'dapp', 'nethash', 'cacheEnabled', 'redis'] + } +}; diff --git a/helpers/RoundChanges.js b/helpers/RoundChanges.js index 7ace61e0..fda57ca9 100644 --- a/helpers/RoundChanges.js +++ b/helpers/RoundChanges.js @@ -22,15 +22,15 @@ function RoundChanges (scope) { // Apply rewards factor this.roundRewards.forEach(function (reward, index) { this.roundRewards[index] = new bignum(reward.toPrecision(15)) - .times(exceptions.rounds[scope.round].rewards_factor) - .integerValue(bignum.ROUND_FLOOR); + .times(exceptions.rounds[scope.round].rewards_factor) + .integerValue(bignum.ROUND_FLOOR); }.bind(this)); // Apply fees factor and bonus this.roundFees = new bignum(this.roundFees.toPrecision(15)) - .times(exceptions.rounds[scope.round].fees_factor) - .plus(exceptions.rounds[scope.round].fees_bonus) - .integerValue(bignum.ROUND_FLOOR); + .times(exceptions.rounds[scope.round].fees_factor) + .plus(exceptions.rounds[scope.round].fees_bonus) + .integerValue(bignum.ROUND_FLOOR); } } diff --git a/helpers/cache.js b/helpers/cache.js index c8e1ed86..4f063bea 100644 --- a/helpers/cache.js +++ b/helpers/cache.js @@ -24,21 +24,21 @@ module.exports.connect = function (cacheEnabled, config, logger, cb) { var client = redis.createClient(config); client.connect() - .then(() => { - logger.info('App connected with redis server'); + .then(() => { + logger.info('App connected with redis server'); - if (!isRedisLoaded) { - isRedisLoaded = true; - client.ready = isRedisLoaded; - return cb(null, { cacheEnabled: cacheEnabled, client: client }); - } - }) - .catch((err) => { - logger.error('Redis:', err); - // Only throw an error if cache was enabled in config but were unable to load it properly - if (!isRedisLoaded) { - isRedisLoaded = true; - return cb(null, { cacheEnabled: cacheEnabled, client: null }); - } - }); + if (!isRedisLoaded) { + isRedisLoaded = true; + client.ready = isRedisLoaded; + return cb(null, { cacheEnabled: cacheEnabled, client: client }); + } + }) + .catch((err) => { + logger.error('Redis:', err); + // Only throw an error if cache was enabled in config but were unable to load it properly + if (!isRedisLoaded) { + isRedisLoaded = true; + return cb(null, { cacheEnabled: cacheEnabled, client: null }); + } + }); }; diff --git a/modules/cache.js b/modules/cache.js index ba346c0f..94b52d31 100644 --- a/modules/cache.js +++ b/modules/cache.js @@ -49,13 +49,13 @@ Cache.prototype.getJsonForKey = function (key, cb) { return cb(errorCacheDisabled); } client.get(key) - .then((value) => { - // parsing string to json - return cb(null, JSON.parse(value)); - }) - .catch((err) => { - return cb(err, key); - }); + .then((value) => { + // parsing string to json + return cb(null, JSON.parse(value)); + }) + .catch((err) => { + return cb(err, key); + }); }; /** @@ -71,12 +71,12 @@ Cache.prototype.setJsonForKey = function (key, value, cb) { // redis calls toString on objects, which converts it to object [object] so calling stringify before saving client.set(key, JSON.stringify(value)) - .then((res) => { - cb(null, res) - }) - .catch((err) => { - cb(err, value) - }); + .then((res) => { + cb(null, res) + }) + .catch((err) => { + cb(err, value) + }); }; /** @@ -89,8 +89,8 @@ Cache.prototype.deleteJsonForKey = function (key, cb) { } client.del(key) - .then((res) => cb(null, res)) - .catch((err) => cb(err, key)); + .then((res) => cb(null, res)) + .catch((err) => cb(err, key)); }; /** @@ -105,20 +105,20 @@ Cache.prototype.removeByPattern = function (pattern, cb) { var keys, cursor = 0; async.doWhilst(function iteratee (whilstCb) { client.scan(cursor, { MATCH: pattern }) - .then((res) => { - cursor = res.cursor; - keys = res.keys; - if (keys.length > 0) { - client.del(keys) - .then((res) => whilstCb(null, res)) - .catch((err) => whilstCb(err)); - } else { - return whilstCb(); - } - }) - .catch((err) => { - return whilstCb(err); - }); + .then((res) => { + cursor = res.cursor; + keys = res.keys; + if (keys.length > 0) { + client.del(keys) + .then((res) => whilstCb(null, res)) + .catch((err) => whilstCb(err)); + } else { + return whilstCb(); + } + }) + .catch((err) => { + return whilstCb(err); + }); }, function test (...args) { return args[args.length - 1](null, cursor > 0); }, cb); @@ -134,8 +134,8 @@ Cache.prototype.flushDb = function (cb) { } client.flushDb() - .then((res) => cb(null, res)) - .catch((err) => cb(err)); + .then((res) => cb(null, res)) + .catch((err) => cb(err)); }; /** @@ -157,8 +157,8 @@ Cache.prototype.quit = function (cb) { } client.quit() - .then((res) => cb(null, res)) - .catch((err) => cb(err)); + .then((res) => cb(null, res)) + .catch((err) => cb(err)); }; /** diff --git a/schema/config.js b/schema/config.js index b3c2811a..6b050073 100644 --- a/schema/config.js +++ b/schema/config.js @@ -71,7 +71,7 @@ module.exports = { type: 'object', properties: { url: { - type: 'string', + type: 'string' }, password: { type: ['string', 'null'] From 56059c2068a1db65399eec474a5eff5442f8aa52 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 15:49:39 +0600 Subject: [PATCH 15/36] fix: undo changes in config.json --- config.json | 404 +++++++++++++++------------------------------------- 1 file changed, 111 insertions(+), 293 deletions(-) diff --git a/config.json b/config.json index 6b050073..1d59e3fd 100644 --- a/config.json +++ b/config.json @@ -1,299 +1,117 @@ -'use strict'; - -module.exports = { - config: { - id: 'appCon', - type: 'object', - properties: { - port: { - type: 'integer', - minimum: 1, - maximum: 65535 - }, - address: { - type: 'string', - format: 'ip' - }, - fileLogLevel: { - type: 'string' - }, - logFileName: { - type: 'string' - }, - consoleLogLevel: { - type: 'string' - }, - trustProxy: { - type: 'boolean' - }, - topAccounts: { - type: 'boolean' - }, - cacheEnabled: { - type: 'boolean' - }, - db: { - type: 'object', - properties: { - host: { - type: 'string' - }, - port: { - type: 'integer', - minimum: 1, - maximum: 65535 - }, - database: { - type: 'string' - }, - user: { - type: 'string' - }, - password: { - type: 'string' - }, - poolSize: { - type: 'integer' - }, - poolIdleTimeout: { - type: 'integer' - }, - reapIntervalMillis: { - type: 'integer' - }, - logEvents: { - type: 'array' - } - }, - required: ['host', 'port', 'database', 'user', 'password', 'poolSize', 'poolIdleTimeout', 'reapIntervalMillis', 'logEvents'] - }, - redis: { - type: 'object', - properties: { - url: { - type: 'string' - }, - password: { - type: ['string', 'null'] - } - }, - required: ['url', 'password'] - }, - api: { - type: 'object', - properties: { - enabled: { - type: 'boolean' - }, - access: { - type: 'object', - properties: { - public: { - type: 'boolean' - }, - whiteList: { - type: 'array' - } - }, - required: ['public', 'whiteList'] - }, - options: { - type: 'object', - properties: { - limits: { - type: 'object', - properties: { - max: { - type: 'integer' - }, - delayMs: { - type: 'integer' - }, - delayAfter: { - type: 'integer' - }, - windowMs: { - type: 'integer' - } - }, - required: ['max', 'delayMs', 'delayAfter', 'windowMs'] - } - }, - required: ['limits'] - } +{ + "port": 36666, + "address": "0.0.0.0", + "fileLogLevel": "warn", + "logFileName": "logs/adamant.log", + "consoleLogLevel": "info", + "trustProxy": false, + "topAccounts": false, + "cacheEnabled": false, + "db": { + "host": "localhost", + "port": 5432, + "database": "adamant_main", + "user": "adamant", + "password": "password", + "poolSize": 95, + "poolIdleTimeout": 30000, + "reapIntervalMillis": 1000, + "logEvents": [ + "error" + ] + }, + "redis": { + "host": "127.0.0.1", + "port": 6379, + "db": 0, + "password": null + }, + "api": { + "enabled": true, + "access": { + "public": false, + "whiteList": [ + "127.0.0.1" + ] }, - required: ['enabled', 'access', 'options'] - }, - peers: { - type: 'object', - properties: { - enabled: { - type: 'boolean' - }, - list: { - type: 'array' - }, - access: { - type: 'object', - properties: { - blackList: { - type: 'array' - } + "options": { + "limits": { + "max": 0, + "delayMs": 0, + "delayAfter": 0, + "windowMs": 60000 + } + } + }, + "peers": { + "enabled": true, + "list": [ + { + "ip": "51.15.221.205", + "port": 36666 }, - required: ['blackList'] - }, - options: { - properties: { - limits: { - type: 'object', - properties: { - max: { - type: 'integer' - }, - delayMs: { - type: 'integer' - }, - delayAfter: { - type: 'integer' - }, - windowMs: { - type: 'integer' - } - }, - required: ['max', 'delayMs', 'delayAfter', 'windowMs'] - }, - timeout: { - type: 'integer' - } + { + "ip": "51.15.88.53", + "port": 36666 }, - required: ['limits', 'timeout'] - } - }, - required: ['enabled', 'list', 'access', 'options'] - }, - broadcasts: { - type: 'object', - properties: { - broadcastInterval: { - type: 'integer', - minimum: 1000, - maximum: 60000 - }, - broadcastLimit: { - type: 'integer', - minimum: 1, - maximum: 100 - }, - parallelLimit: { - type: 'integer', - minimum: 1, - maximum: 100 - }, - releaseLimit: { - type: 'integer', - minimum: 1, - maximum: 25 - }, - relayLimit: { - type: 'integer', - minimum: 1, - maximum: 100 - } - }, - required: ['broadcastInterval', 'broadcastLimit', 'parallelLimit', 'releaseLimit', 'relayLimit'] - }, - transactions: { - type: 'object', - maxTxsPerQueue: { - type: 'integer', - minimum: 100, - maximum: 5000 + { + "ip": "51.15.217.230", + "port": 36666 + } + ], + "access": { + "blackList": [] }, - required: ['maxTxsPerQueue'] - }, - forging: { - type: 'object', - properties: { - force: { - type: 'boolean' - }, - secret: { - type: 'array' - }, - access: { - type: 'object', - properties: { - whiteList: { - type: 'array' - } + "options": { + "limits": { + "max": 0, + "delayMs": 0, + "delayAfter": 0, + "windowMs": 60000 }, - required: ['whiteList'] - } - }, - required: ['force', 'secret', 'access'] - }, - loading: { - type: 'object', - properties: { - verifyOnLoading: { - type: 'boolean' - }, - loadPerIteration: { - type: 'integer', - minimum: 1, - maximum: 5000 - } - }, - required: ['verifyOnLoading', 'loadPerIteration'] - }, - ssl: { - type: 'object', - properties: { - enabled: { - type: 'boolean' - }, - options: { - type: 'object', - properties: { - port: { - type: 'integer' - }, - address: { - type: 'string', - format: 'ip' - }, - key: { - type: 'string' - }, - cert: { - type: 'string' - } - }, - required: ['port', 'address', 'key', 'cert'] - } - }, - required: ['enabled', 'options'] - }, - dapp: { - type: 'object', - properties: { - masterrequired: { - type: 'boolean' - }, - masterpassword: { - type: 'string' - }, - autoexec: { - type: 'array' - } - }, - required: ['masterrequired', 'masterpassword', 'autoexec'] - }, - nethash: { - type: 'string', - format: 'hex' - } + "timeout": 5000 + } + }, + "broadcasts": { + "broadcastInterval": 1500, + "broadcastLimit": 20, + "parallelLimit": 20, + "releaseLimit": 25, + "relayLimit": 3 + }, + "transactions": { + "maxTxsPerQueue": 1000 + }, + "forging": { + "force": false, + "secret": [], + "access": { + "whiteList": [ + "127.0.0.1" + ] + } + }, + "loading": { + "verifyOnLoading": false, + "loadPerIteration": 5000 + }, + "ssl": { + "enabled": false, + "options": { + "port": 443, + "address": "0.0.0.0", + "key": "./ssl/lisk.key", + "cert": "./ssl/lisk.crt" + } + }, + "dapp": { + "masterrequired": true, + "masterpassword": "", + "autoexec": [] + }, + "wsClient": { + "portWS": 36668, + "enabled": true }, - required: ['port', 'address', 'fileLogLevel', 'logFileName', 'consoleLogLevel', 'trustProxy', 'topAccounts', 'db', 'api', 'peers', 'broadcasts', 'transactions', 'forging', 'loading', 'ssl', 'dapp', 'nethash', 'cacheEnabled', 'redis'] - } -}; + "nethash": "bd330166898377fb28743ceef5e43a5d9d0a3efd9b3451fb7bc53530bb0a6d64" +} From 6351e62b7080aaa925d3b7cf41ae014af28ddd96 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 7 Jul 2022 15:51:38 +0600 Subject: [PATCH 16/36] fix: eslint errors --- modules/cache.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/cache.js b/modules/cache.js index 94b52d31..0ed99575 100644 --- a/modules/cache.js +++ b/modules/cache.js @@ -72,10 +72,10 @@ Cache.prototype.setJsonForKey = function (key, value, cb) { // redis calls toString on objects, which converts it to object [object] so calling stringify before saving client.set(key, JSON.stringify(value)) .then((res) => { - cb(null, res) + cb(null, res); }) .catch((err) => { - cb(err, value) + cb(err, value); }); }; @@ -110,8 +110,8 @@ Cache.prototype.removeByPattern = function (pattern, cb) { keys = res.keys; if (keys.length > 0) { client.del(keys) - .then((res) => whilstCb(null, res)) - .catch((err) => whilstCb(err)); + .then((res) => whilstCb(null, res)) + .catch((err) => whilstCb(err)); } else { return whilstCb(); } From c954aad00c7d64703955ad4347f802298fb016f9 Mon Sep 17 00:00:00 2001 From: martiliones Date: Sun, 17 Jul 2022 02:05:21 +0600 Subject: [PATCH 17/36] chore: replace lisk with adamant --- config.json | 4 ++-- modules/blocks/verify.js | 2 +- modules/peers.js | 6 +++--- modules/transactions.js | 2 +- sql/memoryTables.sql | 2 +- test/api/accounts.js | 16 +++++++-------- test/api/dapps.js | 16 +++++++-------- test/api/delegates.js | 6 +++--- test/api/multisignatures.js | 20 +++++++++---------- test/api/peer.transactions.multisignatures.js | 4 ++-- test/api/peer.transactions.signatures.js | 4 ++-- test/api/signatures.js | 12 +++++------ test/api/transactions.js | 4 ++-- test/node.js | 6 +++--- test/unit/logic/peers.js | 4 ++-- test/unit/modules/peers.js | 4 ++-- 16 files changed, 56 insertions(+), 56 deletions(-) diff --git a/config.json b/config.json index 1d59e3fd..37575d18 100644 --- a/config.json +++ b/config.json @@ -100,8 +100,8 @@ "options": { "port": 443, "address": "0.0.0.0", - "key": "./ssl/lisk.key", - "cert": "./ssl/lisk.crt" + "key": "./ssl/adamant.key", + "cert": "./ssl/adamant.crt" } }, "dapp": { diff --git a/modules/blocks/verify.js b/modules/blocks/verify.js index ee7f42e6..12420e63 100644 --- a/modules/blocks/verify.js +++ b/modules/blocks/verify.js @@ -476,7 +476,7 @@ Verify.prototype.processBlock = function (block, broadcast, cb, saveBlock) { } else { // The block and the transactions are OK i.e: // * Block and transactions have valid values (signatures, block slots, etc...) - // * The check against database state passed (for instance sender has enough LSK, votes are under 101, etc...) + // * The check against database state passed (for instance sender has enough ADM, votes are under 101, etc...) // We thus update the database with the transactions values, save the block and tick it modules.blocks.chain.applyBlock(block, broadcast, cb, saveBlock); } diff --git a/modules/peers.js b/modules/peers.js index ba1bd507..3eb6e4f1 100644 --- a/modules/peers.js +++ b/modules/peers.js @@ -415,9 +415,9 @@ Peers.prototype.acceptable = function (peers) { .filter(function (peer) { // Removing peers with private address or nonce equal to self if ((process.env['NODE_ENV'] || '').toUpperCase() === 'TEST') { - return peer.nonce !== modules.system.getNonce() && (peer.os !== 'lisk-js-api'); + return peer.nonce !== modules.system.getNonce() && (peer.os !== 'adm-js-api'); } - return !ip.isPrivate(peer.ip) && peer.nonce !== modules.system.getNonce() && (peer.os !== 'lisk-js-api'); + return !ip.isPrivate(peer.ip) && peer.nonce !== modules.system.getNonce() && (peer.os !== 'adm-js-api'); }).value(); }; @@ -679,7 +679,7 @@ Peers.prototype.shared = { * @return {Object} cb.obj Anonymous object with version info * @return {String} cb.obj.build Build information (if available, otherwise '') * @return {String} cb.obj.commit Hash of last git commit (if available, otherwise '') - * @return {String} cb.obj.version Lisk version from config file + * @return {String} cb.obj.version Adamant version from config file */ version: function (req, cb) { return setImmediate(cb, null, { diff --git a/modules/transactions.js b/modules/transactions.js index 035b84d8..7d15d215 100644 --- a/modules/transactions.js +++ b/modules/transactions.js @@ -128,7 +128,7 @@ __private.list = function (filter, cb) { // Mutating parameters when unix timestamp is supplied if (_.includes(['fromUnixTime', 'toUnixTime'], field[1])) { - // Lisk epoch is 1464109200 as unix timestamp + // Adamant epoch is 1464109200 as unix timestamp value = value - constants.epochTime.getTime() / 1000; field[1] = field[1].replace('UnixTime', 'Timestamp'); } diff --git a/sql/memoryTables.sql b/sql/memoryTables.sql index 0da9d142..2e4850f5 100644 --- a/sql/memoryTables.sql +++ b/sql/memoryTables.sql @@ -1,4 +1,4 @@ -/* Lisk Memory Tables +/* Adamant Memory Tables * */ diff --git a/test/api/accounts.js b/test/api/accounts.js index 91dc93d7..fd613b93 100644 --- a/test/api/accounts.js +++ b/test/api/accounts.js @@ -119,9 +119,9 @@ describe('GET /api/accounts/getBalance?address=', function () { }); it('using invalid address should fail', function (done) { - getBalance('thisIsNOTALiskAddress', function (err, res) { + getBalance('thisIsNOTAnAdamantAddress', function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.eql('Object didn\'t pass validation for format address: thisIsNOTALiskAddress'); + node.expect(res.body).to.have.property('error').to.eql('Object didn\'t pass validation for format address: thisIsNOTAnAdamantAddress'); done(); }); }); @@ -158,9 +158,9 @@ describe('GET /api/accounts/getPublicKey?address=', function () { }); it('using invalid address should fail', function (done) { - getPublicKey('thisIsNOTALiskAddress', function (err, res) { + getPublicKey('thisIsNOTAnAdamantAddress', function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; - node.expect(res.body).to.have.property('error').to.contain('Object didn\'t pass validation for format address: thisIsNOTALiskAddress'); + node.expect(res.body).to.have.property('error').to.contain('Object didn\'t pass validation for format address: thisIsNOTAnAdamantAddress'); done(); }); }); @@ -295,10 +295,10 @@ describe('GET /accounts', function () { }); it('using invalid address should fail', function (done) { - getAccounts('address=' + 'thisIsNOTALiskAddress', function (err, res) { + getAccounts('address=' + 'thisIsNOTAnAdamantAddress', function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; node.expect(res.body).to.have.property('error'); - node.expect(res.body.error).to.contain('Object didn\'t pass validation for format address: thisIsNOTALiskAddress'); + node.expect(res.body.error).to.contain('Object didn\'t pass validation for format address: thisIsNOTAnAdamantAddress'); done(); }); }); @@ -346,10 +346,10 @@ describe('GET /accounts', function () { }); it('using invalid publicKey should fail', function (done) { - getAccounts('publicKey=' + 'thisIsNOTALiskAccountPublicKey', function (err, res) { + getAccounts('publicKey=' + 'thisIsNOTAnAdamantAccountPublicKey', function (err, res) { node.expect(res.body).to.have.property('success').to.be.not.ok; node.expect(res.body).to.have.property('error'); - node.expect(res.body.error).to.contain('Object didn\'t pass validation for format publicKey: thisIsNOTALiskAccountPublicKey'); + node.expect(res.body.error).to.contain('Object didn\'t pass validation for format publicKey: thisIsNOTAnAdamantAccountPublicKey'); done(); }); }); diff --git a/test/api/dapps.js b/test/api/dapps.js index 9246ed53..0de4afb3 100644 --- a/test/api/dapps.js +++ b/test/api/dapps.js @@ -43,28 +43,28 @@ before(function (done) { }); before(function (done) { - // Send to LISK to account 1 address + // Send to ADM to account 1 address setTimeout(function () { - var randomLISK = node.randomLISK(); - var expectedFee = node.expectedFee(randomLISK); + var randomADM = node.randomADM(); + var expectedFee = node.expectedFee(randomADM); putTransaction({ secret: node.iAccount.password, - amount: randomLISK, + amount: randomADM, recipientId: account.address }, done); }, 2000); }); before(function (done) { - // Send to LISK to account 2 address + // Send to Adamant to account 2 address setTimeout(function () { - var randomLISK = node.randomLISK(); - var expectedFee = node.expectedFee(randomLISK); + var randomADM = node.randomADM(); + var expectedFee = node.expectedFee(randomADM); putTransaction({ secret: node.iAccount.password, - amount: randomLISK, + amount: randomADM, recipientId: account2.address }, done); }, 2000); diff --git a/test/api/delegates.js b/test/api/delegates.js index f0a74e09..78285b42 100644 --- a/test/api/delegates.js +++ b/test/api/delegates.js @@ -85,7 +85,7 @@ describe('PUT /api/accounts/delegates with funds', function () { before(function (done) { sendADM({ secret: node.iAccount.password, - amount: node.randomLISK(), + amount: node.randomADM(), recipientId: account.address }, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; @@ -267,7 +267,7 @@ describe('PUT /api/delegates with funds', function () { beforeEach(function (done) { sendADM({ secret: node.iAccount.password, - amount: node.randomLISK(), + amount: node.randomADM(), recipientId: account.address }, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; @@ -785,7 +785,7 @@ describe('GET /api/delegates/voters', function () { before(function (done) { sendADM({ secret: node.iAccount.password, - amount: node.randomLISK(), + amount: node.randomADM(), recipientId: account.address }, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; diff --git a/test/api/multisignatures.js b/test/api/multisignatures.js index 41904196..9bdca71e 100644 --- a/test/api/multisignatures.js +++ b/test/api/multisignatures.js @@ -25,23 +25,23 @@ var multiSigTx = { txId: '' }; -function sendLISK (account, i, done) { - var randomLISK = node.randomLISK(); +function sendADM (account, i, done) { + var randomADM = node.randomADM(); node.put('/api/transactions/', { secret: node.iAccount.password, - amount: randomLISK, + amount: randomADM, recipientId: account.address }, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; if (res.body.success && i != null) { - accounts[i].balance = randomLISK / node.normalizer; + accounts[i].balance = randomADM / node.normalizer; } done(); }); } -function sendLISKFromMultisigAccount (password, amount, recipient, done) { +function sendADMFromMultisigAccount (password, amount, recipient, done) { node.put('/api/transactions/', { secret: password, amount: amount, @@ -96,7 +96,7 @@ function makeKeysGroup () { before(function (done) { var i = 0; async.eachSeries(accounts, function (account, eachCb) { - sendLISK(account, i, function () { + sendADM(account, i, function () { i++; return eachCb(); }); @@ -106,7 +106,7 @@ before(function (done) { }); before(function (done) { - sendLISK(multisigAccount, null, done); + sendADM(multisigAccount, null, done); }); before(function (done) { @@ -453,7 +453,7 @@ describe('PUT /api/multisignatures', function () { before(function (done) { async.every([multisigAccount1, multisigAccount2, multisigAccount3], function (acc, cb) { - sendLISK(acc, 0, cb); + sendADM(acc, 0, cb); }, done); }); @@ -572,7 +572,7 @@ describe('GET /api/multisignatures/pending', function () { describe('PUT /api/transactions', function () { it('when group transaction is pending should be ok', function (done) { - sendLISKFromMultisigAccount(multisigAccount.password, 100000000, node.iAccount.address, function (err, transactionId) { + sendADMFromMultisigAccount(multisigAccount.password, 100000000, node.iAccount.address, function (err, transactionId) { node.onNewBlock(function (err) { node.get('/api/transactions/get?id=' + transactionId, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; @@ -681,7 +681,7 @@ describe('POST /api/multisignatures/sign (transaction)', function () { }); before(function (done) { - sendLISKFromMultisigAccount(multisigAccount.password, 100000000, node.iAccount.address, function (err, transactionId) { + sendADMFromMultisigAccount(multisigAccount.password, 100000000, node.iAccount.address, function (err, transactionId) { multiSigTx.txId = transactionId; node.onNewBlock(function (err) { done(); diff --git a/test/api/peer.transactions.multisignatures.js b/test/api/peer.transactions.multisignatures.js index 78785a0f..7e46a6ea 100644 --- a/test/api/peer.transactions.multisignatures.js +++ b/test/api/peer.transactions.multisignatures.js @@ -16,7 +16,7 @@ function postTransaction (transaction, done) { }); } -function sendLISK (params, done) { +function sendADM (params, done) { var transaction = node.createSendTransaction({ keyPair: account.keypair, amount: 100000000, @@ -52,7 +52,7 @@ describe('POST /peer/transactions', function () { describe('when account has funds', function () { before(function (done) { - sendLISK({ + sendADM({ secret: node.iAccount.password, amount: node.fees.multisignatureRegistrationFee * 10, recipientId: multisigAccount.address diff --git a/test/api/peer.transactions.signatures.js b/test/api/peer.transactions.signatures.js index b8786ae1..4aea1164 100644 --- a/test/api/peer.transactions.signatures.js +++ b/test/api/peer.transactions.signatures.js @@ -20,7 +20,7 @@ function postSignatureTransaction (transaction, done) { }); } -function sendLISK (params, done) { +function sendADM (params, done) { var transaction = node.createSendTransaction({ keyPair: node.createKeypairFromPassphrase(params.secret), amount: params.amount, @@ -75,7 +75,7 @@ describe('POST /peer/transactions', function () { describe('when account has funds', function () { before(function (done) { - sendLISK({ + sendADM({ secret: node.iAccount.password, amount: node.fees.secondPasswordFee + 100000000, recipientId: account.address diff --git a/test/api/signatures.js b/test/api/signatures.js index c55f68c3..36bc7205 100644 --- a/test/api/signatures.js +++ b/test/api/signatures.js @@ -18,13 +18,13 @@ function putDelegate (params, done) { node.put('/api/delegates', params, done); } -function sendLISK (account, done) { - var randomLISK = node.randomLISK(); - var expectedFee = node.expectedFee(randomLISK); +function sendADM (account, done) { + var randomADM = node.randomADM(); + var expectedFee = node.expectedFee(randomADM); putTransaction({ secret: node.iAccount.password, - amount: randomLISK, + amount: randomADM, recipientId: account.address }, function (err, res) { node.expect(res.body).to.have.property('success').to.be.ok; @@ -34,13 +34,13 @@ function sendLISK (account, done) { before(function (done) { setTimeout(function () { - sendLISK(account, done); + sendADM(account, done); }, 2000); }); before(function (done) { setTimeout(function () { - sendLISK(account2, done); + sendADM(account2, done); }, 2000); describe('PUT /api/transactions from account with second signature enabled', function () { diff --git a/test/api/transactions.js b/test/api/transactions.js index ebb1263e..dc71324a 100644 --- a/test/api/transactions.js +++ b/test/api/transactions.js @@ -70,13 +70,13 @@ function sendADM2voter (params, done) { before(function (done) { setTimeout(function () { - sendADM(account, node.randomLISK(), done); + sendADM(account, node.randomADM(), done); }, 2000); }); before(function (done) { setTimeout(function () { - sendADM(account2, node.randomLISK(), done); + sendADM(account2, node.randomADM(), done); }, 2000); }); diff --git a/test/node.js b/test/node.js index 40d06a39..e8474825 100644 --- a/test/node.js +++ b/test/node.js @@ -39,7 +39,7 @@ node.baseUrl = 'http://' + node.config.address + ':' + node.config.port; // node.baseUrl = 'http://' + node.config.peers.list[2].ip + ':' + node.config.peers.list[2].port; node.api = node.supertest(node.baseUrl); -node.normalizer = 100000000; // Use this to convert LISK amount to normal value +node.normalizer = 100000000; // Use this to convert ADM amount to normal value node.blockTime = 10000; // Block time in milliseconds node.blockTimePlus = 12000; // Block time + 2 seconds in milliseconds node.version = packageJson.version; // Node version @@ -138,8 +138,8 @@ if (process.env.SILENT === 'true') { node.debug = console.log; } -// Returns random LSK amount -node.randomLISK = function () { +// Returns random ADM amount +node.randomADM = function () { return Math.floor(Math.random() * (10000 * 100000000)) + (1000 * 100000000); }; diff --git a/test/unit/logic/peers.js b/test/unit/logic/peers.js index 3bbe0a7a..6f10e015 100644 --- a/test/unit/logic/peers.js +++ b/test/unit/logic/peers.js @@ -100,10 +100,10 @@ describe('peers', function () { removeAll(); }); - it('should not insert new peer with lisk-js-api os', function () { + it('should not insert new peer with adm-js-api os', function () { removeAll(); var modifiedPeer = _.clone(randomPeer); - modifiedPeer.os = 'lisk-js-api'; + modifiedPeer.os = 'adm-js-api'; peers.upsert(modifiedPeer); expect(peers.list().length).equal(0); removeAll(); diff --git a/test/unit/modules/peers.js b/test/unit/modules/peers.js index 4816a665..1a6007ee 100644 --- a/test/unit/modules/peers.js +++ b/test/unit/modules/peers.js @@ -209,9 +209,9 @@ describe('peers', function () { expect(peers.acceptable([privatePeer])).that.is.an('array').and.to.be.empty; }); - it('should not accept peer with lisk-js-api os', function () { + it('should not accept peer with adm-js-api os', function () { var privatePeer = _.clone(randomPeer); - privatePeer.os = 'lisk-js-api'; + privatePeer.os = 'adm-js-api'; expect(peers.acceptable([privatePeer])).that.is.an('array').and.to.be.empty; }); From 17b9ce6a238ae77a16fd36c3b54e31d9dec2af52 Mon Sep 17 00:00:00 2001 From: martiliones Date: Sun, 17 Jul 2022 23:52:32 +0600 Subject: [PATCH 18/36] fix: redis config for mainnet --- config.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config.json b/config.json index 37575d18..9dec46ff 100644 --- a/config.json +++ b/config.json @@ -21,9 +21,7 @@ ] }, "redis": { - "host": "127.0.0.1", - "port": 6379, - "db": 0, + "url": "redis://127.0.0.1:6379/0", "password": null }, "api": { From bce5014e1a70db5737865938830c0856bbc854d9 Mon Sep 17 00:00:00 2001 From: martiliones Date: Mon, 18 Jul 2022 01:37:57 +0600 Subject: [PATCH 19/36] chore: move lisk-sandbox from depends to legacy directory --- legacy/lisk-sandbox.js | 231 +++++++++++++++++++++++++++++++++++++++++ modules/dapps.js | 2 +- package.json | 1 - 3 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 legacy/lisk-sandbox.js diff --git a/legacy/lisk-sandbox.js b/legacy/lisk-sandbox.js new file mode 100644 index 00000000..defbc643 --- /dev/null +++ b/legacy/lisk-sandbox.js @@ -0,0 +1,231 @@ +/* +The MIT License (MIT) + +Copyright (c) 2016 Lisk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// https://github.com/LiskArchive/lisk-sandbox + +var EventEmitter = require('events').EventEmitter, + util = require('util'), + spawn = require('child_process').spawn, + path = require('path'), + querystring = require('querystring'), + async = require('async'); + +var callbacks = {}; +var magic = "=%5a$ng*a8="; +var magicData = ""; +var debugPort = 5859; + +function Sandbox(file, id, params, apiHandler, debug) { + EventEmitter.call(this); + + if (typeof file !== "string" || file === undefined || file === null) { + throw new Error("First argument should be a path to file to launch in vm"); + } + + if (typeof id !== "string" || id === undefined || id === null) { + throw new Error("Second argument should be a id of dapp"); + } + + if (typeof apiHandler !== "function" || apiHandler === undefined || apiHandler === null) { + throw new Error("Third argument should be a api hanlder callback"); + } + + this.params = params; + this.file = file; + this.id = id; + this.apiHandler = apiHandler; + this.child = null; + this.queue = null; + this.debug = debug || false; +} + +util.inherits(Sandbox, EventEmitter); + +Sandbox.prototype._parse = function (data) { + try { + var json = JSON.parse(data); + } catch (e) { + return this._onError(new Error("Can't parse JSON response from DApp: \n" + data + "\n" + e.toString())); + } + + if (json.callback_id === null || json.callback_id === undefined) { + return this._onError(new Error("Incorrect response from vm, missed callback id field")); + } + + try { + var callback_id = parseInt(json.callback_id); + } catch (e) { + return this._onError(new Error("Incorrect callback_id field, callback_id should be a number")); + } + + if (isNaN(callback_id)) { + return this._onError(new Error("Incorrect callback_id field, callback_id should be a number")); + } + + if (json.type == "dapp_response") { + var callback = callbacks[callback_id]; + + if (!callback) { + return this._onError(new Error("Lisk can't find callback_id from vm")); + } + + var error = json.error; + var response = json.response; + + setImmediate(callback, error, response); + } else if (json.type == "dapp_call") { + var message = json.message; + + if (message === null || message === undefined) { + return this._onError(new Error("Lisk can't find message for request from vm")); + } + + message.dappid = this.id; + + this.apiHandler(message, function (err, response) { + var responseObj = { + type: "lisk_response", + callback_id: callback_id, + error: err, + response: response || {} + }; + + try { + var responseString = JSON.stringify(responseObj); + } catch (e) { + return this._onError(new Error("Can't make response: " + e.toString())); + } + + this.queue.push({message: responseString + magic}); + }.bind(this)); + } else { + this._onError(new Error("Incorrect response type from vm")); + } +} + +Sandbox.prototype.run = function () { + var params = [this.file].concat(this.params); + if (this.debug) { + params.unshift("--debug=" + debugPort); + console.log("DebugPort " + params[1] + " : " + debugPort++); + } + this.child = spawn(path.join(__dirname, "../../nodejs/node"), params, { + stdio: ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'] + }); + + var self = this; + + this.queue = async.queue(function (task, callback) { + try { + var size = Buffer.byteLength(task.message, 'utf8'); + if (size > 16000) { + console.log("incoming " + (size) + " bytes"); + } + self.child.stdio[3].write(task.message); + } catch (e) { + console.log(e.toString()) + } finally { + setTimeout(callback, 10); + } + }, 1); + + // Catch errors... + this.child.on('error', this._onError.bind(this)); + this.child.stdio[0].on('error', this._onError.bind(this)); + this.child.stdio[1].on('error', this._onError.bind(this)); + this.child.stdio[2].on('error', this._onError.bind(this)); + this.child.stdio[3].on('error', this._onError.bind(this)); + this.child.stdio[4].on('error', this._onError.bind(this)); + + this.child.stdio[4].on('data', this._listen.bind(this)); + + if (this.debug) { + this.child.stdio[1].on('data', this._debug.bind(this)); + } + + this.child.stdio[2].on('data', this._debug.bind(this)); +} + +Sandbox.prototype.setApi = function (apiHanlder) { + if (typeof apiHanlder != "function" || apiHanlder === null || apiHanlder === undefined) { + throw new Error("First argument should be a function"); + } + this.apiHandler = apiHanlder; +} + +Sandbox.prototype.sendMessage = function (message, callback) { + var callback_id = Object.keys(callbacks).length + 1; + + var messageObj = { + callback_id: callback_id, + type: "lisk_call", + message: message + }; + + try { + var messageString = JSON.stringify(messageObj); + } catch (e) { + return setImmediate(callback, "Can't stringify message: " + e.toString()); + } + + this.queue.push({message: messageString + magic}); + + callbacks[callback_id] = callback; +} + +Sandbox.prototype.exit = function () { + if (this.child) { + this.child.kill(); + this.emit("exit"); + } +} + +Sandbox.prototype._debug = function (data) { + console.log("Debug " + this.file + ": \n"); + console.log(data.toString('utf8')); +} + +Sandbox.prototype._onError = function (err) { + console.log(err.stack) + this.exit(); + this.emit("error", err); +} + +Sandbox.prototype._listen = function (dataraw) { + var data = querystring.unescape(dataraw.toString('utf8')); + magicData += data; + if (data.substring(data.length - 11) == magic) { + var fullMessage = magicData; + magicData = ""; + + var parts = fullMessage.split(magic); + parts.pop(); + parts.forEach(function (jsonmessage) { + this._parse(jsonmessage); + }.bind(this)); + + } +} + +module.exports = Sandbox; \ No newline at end of file diff --git a/modules/dapps.js b/modules/dapps.js index ee4d2b98..aebfaac4 100644 --- a/modules/dapps.js +++ b/modules/dapps.js @@ -17,7 +17,7 @@ var path = require('path'); var axios = require('axios').default; var rmdir = require('rimraf'); var Router = require('../helpers/router.js'); -var Sandbox = require('lisk-sandbox'); +var Sandbox = require('../legacy/lisk-sandbox.js'); var sandboxHelper = require('../helpers/sandbox.js'); var schema = require('../schema/dapps.js'); var sql = require('../sql/dapps.js'); diff --git a/package.json b/package.json index b38b502e..2d2158e3 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "js-nacl": "^1.4.0", "json-schema": "=0.4.0", "json-sql": "LiskHQ/json-sql#27e1ed1", - "lisk-sandbox": "LiskHQ/lisk-sandbox#162da38", "lodash": "=4.17.21", "method-override": "=3.0.0", "npm": "=8.13.1", From e2d179b672d37efa874429bc5ba5d8a8f2b4773c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=8C=EF=B8=8F?= Date: Tue, 26 Jul 2022 23:36:47 +0600 Subject: [PATCH 20/36] style: change the name of blockchain to upper case Co-authored-by: adamant-al <33592982+adamant-al@users.noreply.github.com> --- modules/peers.js | 2 +- modules/transactions.js | 2 +- sql/memoryTables.sql | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/peers.js b/modules/peers.js index 3eb6e4f1..fb5ac5de 100644 --- a/modules/peers.js +++ b/modules/peers.js @@ -679,7 +679,7 @@ Peers.prototype.shared = { * @return {Object} cb.obj Anonymous object with version info * @return {String} cb.obj.build Build information (if available, otherwise '') * @return {String} cb.obj.commit Hash of last git commit (if available, otherwise '') - * @return {String} cb.obj.version Adamant version from config file + * @return {String} cb.obj.version ADAMANT version from config file */ version: function (req, cb) { return setImmediate(cb, null, { diff --git a/modules/transactions.js b/modules/transactions.js index 7d15d215..6f3fbf3f 100644 --- a/modules/transactions.js +++ b/modules/transactions.js @@ -128,7 +128,7 @@ __private.list = function (filter, cb) { // Mutating parameters when unix timestamp is supplied if (_.includes(['fromUnixTime', 'toUnixTime'], field[1])) { - // Adamant epoch is 1464109200 as unix timestamp + // ADAMANT epoch is 1464109200 as unix timestamp value = value - constants.epochTime.getTime() / 1000; field[1] = field[1].replace('UnixTime', 'Timestamp'); } diff --git a/sql/memoryTables.sql b/sql/memoryTables.sql index 2e4850f5..ee80c8b0 100644 --- a/sql/memoryTables.sql +++ b/sql/memoryTables.sql @@ -1,4 +1,4 @@ -/* Adamant Memory Tables +/* ADAMANT Memory Tables * */ From 909fb9803b6d8995518d91c200d895741c6efb1f Mon Sep 17 00:00:00 2001 From: martiliones Date: Wed, 27 Jul 2022 00:03:20 +0600 Subject: [PATCH 21/36] feat: enable compatibility with Socket.IO v2 clients --- app.js | 8 ++++++-- modules/clientWs.js | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app.js b/app.js index 3953d41b..08f7e451 100644 --- a/app.js +++ b/app.js @@ -313,7 +313,9 @@ d.run(function () { var server = require('http').createServer(app); const { Server } = require('socket.io'); - const io = new Server(server); + const io = new Server(server, { + allowEIO3: true + }); var privateKey, certificate, https, https_io; @@ -327,7 +329,9 @@ d.run(function () { ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:' + 'ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:' + '!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA' }, app); - https_io = new Server(https); + https_io = new Server(https, { + allowEIO3: true + }); } cb(null, { diff --git a/modules/clientWs.js b/modules/clientWs.js index ed295421..585f76d4 100644 --- a/modules/clientWs.js +++ b/modules/clientWs.js @@ -6,7 +6,9 @@ class ClientWs { return false; } const port = config.portWS; - const io = new Server(port); + const io = new Server(port, { + allowEIO3: true + }); this.describes = {}; this.logger = logger; From 94972606c2bd158c458f7b5ae9fefd487302bc08 Mon Sep 17 00:00:00 2001 From: martiliones Date: Sun, 31 Jul 2022 07:41:21 +0600 Subject: [PATCH 22/36] chore: replace json-sql with knex.js --- logic/account.js | 159 ++++++++++++++++++++----------------- modules/accounts.js | 3 +- modules/delegates.js | 38 +++++++-- modules/multisignatures.js | 8 +- modules/sql.js | 121 ++++++++++++++++++++++------ 5 files changed, 221 insertions(+), 108 deletions(-) diff --git a/logic/account.js b/logic/account.js index 3640730b..bdbc46e3 100644 --- a/logic/account.js +++ b/logic/account.js @@ -3,8 +3,10 @@ var async = require('async'); var pgp = require('pg-promise'); var path = require('path'); -var jsonSql = require('json-sql')(); -jsonSql.setDialect('postgresql'); +var knex = require('knex')({ + client: 'pg' +}); + var constants = require('../helpers/constants.js'); var slots = require('../helpers/slots.js'); @@ -456,14 +458,14 @@ Account.prototype.removeTables = function (cb) { 'mem_accounts2u_delegates', 'mem_accounts2multisignatures', 'mem_accounts2u_multisignatures'].forEach(function (table) { - sql = jsonSql.build({ - type: 'remove', - table: table - }); - sqles.push(sql.query); + const sql = knex.delete() + .from(table) + .toString(); + + sqles.push(sql); }); - this.scope.db.query(sqles.join('')).then(function () { + this.scope.db.query(sqles.join('; ')).then(function () { return setImmediate(cb); }).catch(function (err) { library.logger.error(err.stack); @@ -582,6 +584,8 @@ Account.prototype.getAll = function (filter, fields, cb) { }.bind(this)); var limit, offset, sort; + const rawFilters = filter.raw || []; + delete filter.raw; if (filter.limit > 0) { limit = filter.limit; @@ -599,23 +603,39 @@ Account.prototype.getAll = function (filter, fields, cb) { delete filter.sort; if (typeof filter.address === 'string') { - filter.address = { - $upper: ['address', filter.address] - }; + rawFilters.push( + knex.raw('address = UPPER(?)', filter.address) + ); + delete filter.address; } - var sql = jsonSql.build({ - type: 'select', - table: this.table, - limit: limit, - offset: offset, - sort: sort, - alias: 'a', - condition: filter, - fields: realFields - }); + const sql = knex(this.table) + .select(realFields) + .from({ a: this.table }) + .modify(function (queryBuilder) { + if (limit) { + queryBuilder.limit(limit); + } + + if (offset) { + queryBuilder.offset(offset); + } + + if (sort) { + queryBuilder.orderBy(Array.isArray(sort) ? sort : [sort]); + } + + if (Object.keys(filter).length >= 1) { + builder.where(filter); + } + + if (rawFilters.length >= 1) { + builder.where(rawFilters.join(' and ')); + } + }) + .toString(); - this.scope.db.query(sql.query, sql.values).then(function (rows) { + this.scope.db.query(sql).then(function (rows) { return setImmediate(cb, null, rows); }).catch(function (err) { library.logger.error(err.stack); @@ -638,15 +658,13 @@ Account.prototype.set = function (address, fields, cb) { address = String(address).toUpperCase(); fields.address = address; - var sql = jsonSql.build({ - type: 'insertorupdate', - table: this.table, - conflictFields: ['address'], - values: this.toDB(fields), - modifier: this.toDB(fields) - }); + const sql = knex(this.table) + .insert(this.toDB(fields)) + .onConflict(['address']) + .merge(this.toDB(fields)) + .toString(); - this.scope.db.none(sql.query, sql.values).then(function () { + this.scope.db.none(sql).then(function () { return setImmediate(cb); }).catch(function (err) { library.logger.error(err.stack); @@ -797,16 +815,16 @@ Account.prototype.merge = function (address, diff, cb) { var sqles = []; + const tableName = `${self.table}2${el}`; + if (Object.keys(remove).length) { Object.keys(remove).forEach(function (el) { - var sql = jsonSql.build({ - type: 'remove', - table: self.table + '2' + el, - condition: { - dependentId: { $in: remove[el] }, - accountId: address - } - }); + const sql = knex(tableName) + .whereIn('dependentId', remove[el]) + .andWhere('accountId', address) + .del() + .toString(); + sqles.push(sql); }); } @@ -814,14 +832,13 @@ Account.prototype.merge = function (address, diff, cb) { if (Object.keys(insert).length) { Object.keys(insert).forEach(function (el) { for (var i = 0; i < insert[el].length; i++) { - var sql = jsonSql.build({ - type: 'insert', - table: self.table + '2' + el, - values: { - accountId: address, - dependentId: insert[el][i] - } - }); + const sql = knex(tableName) + .insert({ + accountId: address, + dependentId: insert[el][i] + }) + .toString(); + sqles.push(sql); } }); @@ -830,11 +847,11 @@ Account.prototype.merge = function (address, diff, cb) { if (Object.keys(remove_object).length) { Object.keys(remove_object).forEach(function (el) { remove_object[el].accountId = address; - var sql = jsonSql.build({ - type: 'remove', - table: self.table + '2' + el, - condition: remove_object[el] - }); + const sql = knex(tableName) + .where(remove_object[el]) + .del() + .toString(); + sqles.push(sql); }); } @@ -843,25 +860,21 @@ Account.prototype.merge = function (address, diff, cb) { Object.keys(insert_object).forEach(function (el) { insert_object[el].accountId = address; for (var i = 0; i < insert_object[el].length; i++) { - var sql = jsonSql.build({ - type: 'insert', - table: self.table + '2' + el, - values: insert_object[el] - }); + const sql = knex(tableName) + .insert(insert_object[el]) + .toString(); + sqles.push(sql); } }); } if (Object.keys(update).length) { - var sql = jsonSql.build({ - type: 'update', - table: this.table, - modifier: update, - condition: { - address: address - } - }); + const sql = knex(this.table) + .where({ address }) + .update(update) + .toString(); + sqles.push(sql); } @@ -876,9 +889,9 @@ Account.prototype.merge = function (address, diff, cb) { } } - var queries = sqles.concat(round).map(function (sql) { + var queries = sqles.join(';') + round.map(function (sql) { return pgp.as.format(sql.query, sql.values); - }).join(''); + }).join('').concat(round); if (!cb) { return queries; @@ -903,14 +916,12 @@ Account.prototype.merge = function (address, diff, cb) { * @return {setImmediateCallback} Data with address | Account#remove error. */ Account.prototype.remove = function (address, cb) { - var sql = jsonSql.build({ - type: 'remove', - table: this.table, - condition: { - address: address - } - }); - this.scope.db.none(sql.query, sql.values).then(function () { + const sql = knex(this.table) + .where({ address }) + .del() + .toString(); + + this.scope.db.none(sql).then(function () { return setImmediate(cb, null, address); }).catch(function (err) { library.logger.error(err.stack); diff --git a/modules/accounts.js b/modules/accounts.js index 39d12d1b..59715cbe 100644 --- a/modules/accounts.js +++ b/modules/accounts.js @@ -668,7 +668,8 @@ Accounts.prototype.internal = { top: function (query, cb) { self.getAccounts({ sort: { - balance: -1 + column: 'balance', + order: 'desc' }, offset: query.offset, limit: (query.limit || 100) diff --git a/modules/delegates.js b/modules/delegates.js index 3c801579..c0d0a9c2 100644 --- a/modules/delegates.js +++ b/modules/delegates.js @@ -10,6 +10,9 @@ var jobsQueue = require('../helpers/jobsQueue.js'); var crypto = require('crypto'); var Delegate = require('../logic/delegate.js'); var extend = require('extend'); +var knex = require('knex')({ + client: 'pg' +}); var OrderBy = require('../helpers/orderBy.js'); var sandboxHelper = require('../helpers/sandbox.js'); var schema = require('../schema/delegates.js'); @@ -80,7 +83,13 @@ function Delegates (cb, scope) { __private.getKeysSortByVote = function (cb) { modules.accounts.getAccounts({ isDelegate: 1, - sort: { 'vote': -1, 'publicKey': 1 }, + sort: [{ + column: 'vote', + order: 'desc' + }, { + column: 'publicKey', + order: 'asc' + }], limit: slots.delegates }, ['publicKey'], function (err, rows) { if (err) { @@ -101,7 +110,13 @@ __private.getKeysSortByVote = function (cb) { __private.getKeysSortByVotesWeight = function (cb) { modules.accounts.getAccounts({ isDelegate: 1, - sort: { 'votesWeight': -1, 'publicKey': 1 }, + sort: [{ + column: 'votesWeight', + order: 'desc' + }, { + column: 'publicKey', + order: 'asc' + }], limit: slots.delegates }, ['publicKey'], function (err, rows) { if (err) { @@ -425,10 +440,23 @@ Delegates.prototype.getDelegates = function (query, cb) { if (!query) { throw 'Missing query argument'; } - var sortFilter = { 'vote': -1, 'publicKey': 1 }; + var sortFilter = [{ + column: 'vote', + order: 'desc' + }]; + if (modules.blocks.lastBlock.get().height > constants.fairSystemActivateBlock) { - sortFilter = { 'votesWeight': -1, 'publicKey': 1 }; + sortFilter = [{ + column: 'votesWeight', + order: 'desc' + }]; } + + sortFilter.push({ + column: 'publicKey', + order: 'asc' + }); + modules.accounts.getAccounts({ isDelegate: 1, sort: sortFilter @@ -856,7 +884,7 @@ Delegates.prototype.shared = { var addresses = (row.accountIds) ? row.accountIds : []; modules.accounts.getAccounts({ - address: { $in: addresses }, + raw: knex.raw('?? in (??)', ['address', addresses]).toString(), sort: 'balance' }, ['address', 'balance', 'username', 'publicKey'], function (err, rows) { if (err) { diff --git a/modules/multisignatures.js b/modules/multisignatures.js index 4eb23a64..20cb59a4 100644 --- a/modules/multisignatures.js +++ b/modules/multisignatures.js @@ -9,7 +9,9 @@ var sandboxHelper = require('../helpers/sandbox.js'); var schema = require('../schema/multisignatures.js'); var sql = require('../sql/multisignatures.js'); var transactionTypes = require('../helpers/transactionTypes.js'); - +var knex = require('knex')({ + client: 'pg' +}); // Private fields var modules, library, self, __private = {}, shared = {}; @@ -233,7 +235,7 @@ Multisignatures.prototype.shared = { }, getAccounts: function (seriesCb) { modules.accounts.getAccounts({ - address: { $in: scope.accountIds }, + raw: knex.raw('?? in (??)', ['address', scope.accountIds]).toString(), sort: 'balance' }, ['address', 'balance', 'multisignatures', 'multilifetime', 'multimin'], function (err, accounts) { if (err) { @@ -253,7 +255,7 @@ Multisignatures.prototype.shared = { } modules.accounts.getAccounts({ - address: { $in: addresses } + raw: knex.raw('?? in (??)', ['address', addresses]).toString() }, ['address', 'publicKey', 'balance'], function (err, multisigaccounts) { if (err) { return setImmediate(eachSeriesCb, err); diff --git a/modules/sql.js b/modules/sql.js index e4a3a957..8da60a61 100644 --- a/modules/sql.js +++ b/modules/sql.js @@ -2,8 +2,10 @@ var async = require('async'); var extend = require('extend'); -var jsonSql = require('json-sql')(); -jsonSql.setDialect('postgresql'); +var knex = require('knex')({ + client: 'pg' +}); + var sandboxHelper = require('../helpers/sandbox.js'); // Private fields @@ -118,7 +120,7 @@ __private.pass = function (obj, dappid) { * @implements {jsonSql.build} * @implements {library.db.query} * @implements {async.until} - * @param {string} action + * @param {string} action - should be `batch` or sql query * @param {Object} config * @param {function} cb * @return {setImmediateCallback} cb, err, data @@ -135,22 +137,11 @@ __private.query = function (action, config, cb) { } if (action !== 'batch') { - __private.pass(config, config.dappid); - - var defaultConfig = { - type: action - }; - - try { - sql = jsonSql.build(extend({}, config, defaultConfig)); - library.logger.trace('sql.query:', sql); - } catch (e) { - return done(e); - } + const sql = action; - // console.log(sql.query, sql.values); + library.logger.trace('sql.query:', sql); - library.db.query(sql.query, sql.values).then(function (rows) { + library.db.query(sql).then(function (rows) { return done(null, rows); }).catch(function (err) { library.logger.error(err.stack); @@ -206,6 +197,9 @@ Sql.prototype.createTables = function (dappid, config, cb) { var sqles = []; for (var i = 0; i < config.length; i++) { config[i].table = 'dapp_' + dappid + '_' + config[i].table; + + let sql; + if (config[i].type === 'table') { config[i].type = 'create'; if (config[i].foreignKeys) { @@ -213,17 +207,31 @@ Sql.prototype.createTables = function (dappid, config, cb) { config[i].foreignKeys[n].table = 'dapp_' + dappid + '_' + config[i].foreignKeys[n].table; } } + + try { + sql = knex.schema.createTableIfNotExists('user', function (table) { + for (let j = 0; j < config[i].tableFields.length; j++) { + const field = config[i].tableFields[j]; + + table[field.type](field.name); + } + }).toString(); + } catch (err) { + return setImmediate(cb, 'An error occurred while creating table: ' + err); + } } else if (config[i].type === 'index') { - config[i].type = 'index'; + sql = knex.raw( + `create index if not exists ? ON ?(?)`, + [config[i].name, config[i].table, config[i].indexOn] + ).toString(); } else { return setImmediate(cb, 'Unknown table type: ' + config[i].type); } - var sql = jsonSql.build(config[i]); - sqles.push(sql.query); + sqles.push(sql); } - async.eachSeries(sqles, function (command, cb) { + async.eachSeries(sqles.join('; '), function (command, cb) { library.db.none(command).then(function () { return setImmediate(cb); }).catch(function (err) { @@ -308,7 +316,35 @@ Sql.prototype.onBlockchainReady = function () { */ shared.select = function (req, cb) { var config = extend({}, req.body, { dappid: req.dappid }); - __private.query.call(this, 'select', config, cb); + + __private.pass(config, config.dappid); + + const table = config.alias ? + ({ [config.alias]: config.table }) : + config.table; + + const sql = knex(table) + .select(config.fields) + .modify(function (queryBuilder) { + if (config.limit) { + queryBuilder.limit(config.limit); + } + + if (config.offset) { + queryBuilder.offset(config.offset); + } + + if (config.condition) { + queryBuilder.where(config.condition); + } + + if (config.sort) { + queryBuilder.orderBy(config.sort); + } + }) + .toString(); + + __private.query.call(this, sql, config, cb); }; /** @@ -328,7 +364,19 @@ shared.batch = function (req, cb) { */ shared.insert = function (req, cb) { var config = extend({}, req.body, { dappid: req.dappid }); - __private.query.call(this, 'insert', config, cb); + + __private.pass(config, config.dappid); + + const sql = knex(config.table) + .insert(config.values) + .modify(function (queryBuilder) { + if (config.condition) { + queryBuilder.where(config.condition); + } + }) + .toString(); + + __private.query.call(this, sql, config, cb); }; /** @@ -338,7 +386,19 @@ shared.insert = function (req, cb) { */ shared.update = function (req, cb) { var config = extend({}, req.body, { dappid: req.dappid }); - __private.query.call(this, 'update', config, cb); + + __private.pass(config, config.dappid); + + const sql = knex(config.table) + .update(config.modifier) + .modify(function (queryBuilder) { + if (config.condition) { + queryBuilder.where(config.condition); + } + }) + .toString(); + + __private.query.call(this, sql, config, cb); }; /** @@ -348,7 +408,18 @@ shared.update = function (req, cb) { */ shared.remove = function (req, cb) { var config = extend({}, req.body, { dappid: req.dappid }); - __private.query.call(this, 'remove', config, cb); + __private.pass(config, config.dappid); + + const sql = knex(config.table) + .modify(function (queryBuilder) { + if (config.condition) { + queryBuilder.where(config.condition); + } + }) + .del() + .toString(); + + __private.query.call(this, sql, config, cb); }; // Export From c360227b4b88ef252b2556bf9cb55edb132c3c29 Mon Sep 17 00:00:00 2001 From: martiliones Date: Sun, 31 Jul 2022 07:44:52 +0600 Subject: [PATCH 23/36] fix: add knex and remove json-sql from depends --- package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/package.json b/package.json index 2d2158e3..d3dbb506 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "ip": "=1.1.8", "js-nacl": "^1.4.0", "json-schema": "=0.4.0", - "json-sql": "LiskHQ/json-sql#27e1ed1", + "knex": "^2.2.0", "lodash": "=4.17.21", "method-override": "=3.0.0", "npm": "=8.13.1", @@ -98,9 +98,6 @@ "overrides": { "grunt-contrib-obfuscator": { "javascript-obfuscator": "^4.0.0" - }, - "json-sql": { - "underscore": "^1.13.4" } }, "config": { From 0f46a98be1aa8088bbbd3bb9a54276800d9ffc4f Mon Sep 17 00:00:00 2001 From: martiliones Date: Sun, 31 Jul 2022 07:57:36 +0600 Subject: [PATCH 24/36] chore: replace decompress-zip with unzipper --- modules/dapps.js | 73 +++++++++++++++++++++++++++++++----------------- package.json | 2 +- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/modules/dapps.js b/modules/dapps.js index aebfaac4..ab5854df 100644 --- a/modules/dapps.js +++ b/modules/dapps.js @@ -5,7 +5,7 @@ var crypto = require('crypto'); var DApp = require('../logic/dapp.js'); var dappCategories = require('../helpers/dappCategories.js'); var dappTypes = require('../helpers/dappTypes.js'); -var DecompressZip = require('decompress-zip'); +var unzipper = require('unzipper'); var extend = require('extend'); var fs = require('fs'); var ip = require('ip'); @@ -380,7 +380,7 @@ __private.removeDApp = function (dapp, cb) { * Creates a temp dir, downloads the dapp as stream and decompress it. * @private * @implements {axios} - * @implements {DecompressZip} + * @implements {unzipper} * @param {dapp} dapp * @param {string} dappPath * @param {function} cb @@ -442,34 +442,57 @@ __private.downloadLink = function (dapp, dappPath, cb) { .catch(cleanup); }, decompressZip: function (serialCb) { - var unzipper = new DecompressZip(tmpPath); - library.logger.info(dapp.transactionId, 'Decompressing zip file'); - unzipper.on('error', function (err) { - library.logger.error(dapp.transactionId, 'Decompression failed: ' + err.message); - fs.exists(tmpPath, function (exists) { - if (exists) { fs.unlink(tmpPath); } - return setImmediate(serialCb, 'Failed to decompress zip file'); - }); - }); + fs.createReadStream(tmpPath) + .pipe(unzipper.Parse()) + .on('entry', function (entry) { + const fileName = entry.path; + const strippedPath = fileName + .split(path.sep) + .splice(1) + .join(path.sep); + + if (!strippedPath) { + return entry.autodrain(); + } - unzipper.on('extract', function (log) { - library.logger.info(dapp.transactionId, 'Finished extracting'); - fs.exists(tmpPath, function (exists) { - if (exists) { fs.unlink(tmpPath); } - return setImmediate(serialCb, null); - }); - }); + if (entry.type === 'File') { + const dirName = path.dirname(strippedPath); - unzipper.on('progress', function (fileIndex, fileCount) { - library.logger.info(dapp.transactionId, ['Extracted file', (fileIndex + 1), 'of', fileCount].join(' ')); - }); + if (dirName) { + const dirPath = path.resolve(dappPath, dirName); - unzipper.extract({ - path: dappPath, - strip: 1 - }); + fs.exists(dirPath, function (exists) { + if (!exists) { + fs.mkdirSync(dirPath, { + recursive: true + }); + } + }); + } + + library.logger.info(dapp.transactionId, `Extracted file: ${strippedPath}`); + entry.pipe(fs.createWriteStream(path.resolve(dappPath, strippedPath))); + } else { + entry.autodrain(); + } + }) + .promise() + .then(() => { + library.logger.info(dapp.transactionId, 'Finished extracting'); + fs.exists(tmpPath, function (exists) { + if (exists) { fs.unlink(tmpPath); } + return setImmediate(serialCb, null); + }); + }) + .catch((error) => { + library.logger.error(dapp.transactionId, 'Decompression failed: ' + error); + fs.exists(tmpPath, function (exists) { + if (exists) { fs.unlink(tmpPath); } + return setImmediate(serialCb, 'Failed to decompress zip file'); + }); + }); } }, function (err) { diff --git a/package.json b/package.json index d3dbb506..dae07ecf 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "commander": "=9.3.0", "compression": "=1.7.4", "cors": "=2.8.5", - "decompress-zip": "LiskHQ/decompress-zip#f46b0a3", "ejs": "=3.1.8", "execa": "^5.1.1", "express": "=4.18.1", @@ -72,6 +71,7 @@ "sodium": "^3.0.2", "sodium-browserify-tweetnacl": "*", "strftime": "=0.10.1", + "unzipper": "^0.10.11", "valid-url": "=1.0.9", "validator.js": "=2.0.4", "z-schema": "=5.0.3" From 79f2cda1f24ca95b8087546919e0fa8321fdc396 Mon Sep 17 00:00:00 2001 From: martiliones Date: Sun, 31 Jul 2022 07:59:30 +0600 Subject: [PATCH 25/36] chore: update dependencies --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index dae07ecf..66e424d4 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,12 @@ "async": "=3.2.4", "axios": "^0.27.2", "bignumber.js": "=9.0.2", - "bitcore-mnemonic": "=8.25.30", + "bitcore-mnemonic": "=8.25.34", "body-parser": "=1.20.0", "bytebuffer": "=5.0.1", "change-case": "=4.1.2", "colors": "=1.4.0", - "commander": "=9.3.0", + "commander": "=9.4.0", "compression": "=1.7.4", "cors": "=2.8.5", "ejs": "=3.1.8", @@ -50,7 +50,7 @@ "express": "=4.18.1", "express-domain-middleware": "=0.1.0", "express-query-int": "=3.0.0", - "express-rate-limit": "=6.4.0", + "express-rate-limit": "=6.5.1", "express-slow-down": "1.4.0", "extend": "=3.0.2", "ip": "=1.1.8", @@ -59,12 +59,12 @@ "knex": "^2.2.0", "lodash": "=4.17.21", "method-override": "=3.0.0", - "npm": "=8.13.1", + "npm": "=8.15.1", "pg-monitor": "=1.4.1", "pg-native": "=3.0.0", "pg-promise": "=10.11.1", "randomstring": "=1.2.2", - "redis": "=4.1.0", + "redis": "=4.2.0", "rimraf": "=3.0.2", "semver": "=7.3.7", "socket.io": "^4.5.1", @@ -87,13 +87,13 @@ "grunt-contrib-obfuscator": "^8.0.0", "grunt-eslint": "^24.0.0", "grunt-exec": "^3.0.0", - "jsdoc": "^3.6.10", + "jsdoc": "^3.6.11", "mocha": "^10.0.0", - "moment": "^2.29.3", + "moment": "^2.29.4", "nyc": "^15.1.0", "nyc-middleware": "^1.0.4", "sinon": "^14.0.0", - "supertest": "^6.2.3" + "supertest": "^6.2.4" }, "overrides": { "grunt-contrib-obfuscator": { From 671ea57ba9b765e0edd1ee028537ca9bd3ccace7 Mon Sep 17 00:00:00 2001 From: martiliones Date: Sun, 31 Jul 2022 08:41:24 +0600 Subject: [PATCH 26/36] fix: add backward compatibility for old js api --- modules/peers.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/peers.js b/modules/peers.js index fb5ac5de..988c85ec 100644 --- a/modules/peers.js +++ b/modules/peers.js @@ -413,11 +413,14 @@ Peers.prototype.acceptable = function (peers) { return (a.ip + a.port) === (b.ip + b.port); }) .filter(function (peer) { - // Removing peers with private address or nonce equal to self + // Removing peers with private address or nonce equal to self + const isJsAPI = peer.os === 'adm-js-api' || peer.os === 'lisk-js-api'; + if ((process.env['NODE_ENV'] || '').toUpperCase() === 'TEST') { - return peer.nonce !== modules.system.getNonce() && (peer.os !== 'adm-js-api'); + return peer.nonce !== modules.system.getNonce() && !isJsAPI; } - return !ip.isPrivate(peer.ip) && peer.nonce !== modules.system.getNonce() && (peer.os !== 'adm-js-api'); + + return !ip.isPrivate(peer.ip) && peer.nonce !== modules.system.getNonce() && !isJsAPI; }).value(); }; From ed9cea4f7442d81e70b38b943f62d7001a9454b4 Mon Sep 17 00:00:00 2001 From: martiliones Date: Sun, 31 Jul 2022 09:32:14 +0600 Subject: [PATCH 27/36] feat: add jsdoc plugin for ESLint --- .eslintrc.json | 58 ++++++++++++++++++++++++++++++++++++++++++++++++-- package.json | 6 ++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 3ecd48f8..d99a9226 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -43,7 +43,59 @@ "named": "always", "asyncArrow": "ignore" }], - "space-infix-ops": ["error", { "int32Hint": true }] + "space-infix-ops": ["error", { "int32Hint": true }], + "jsdoc/check-access": 1, + "jsdoc/check-alignment": 1, + "jsdoc/check-line-alignment": 1, + "jsdoc/check-param-names": 1, + "jsdoc/check-property-names": 1, + "jsdoc/check-tag-names": 1, + "jsdoc/check-types": 1, + "jsdoc/check-values": 1, + "jsdoc/empty-tags": 1, + "jsdoc/multiline-blocks": 1, + "jsdoc/newline-after-description": 1, + "jsdoc/no-multi-asterisks": 1, + "jsdoc/require-param": 1, + "jsdoc/require-param-name": 1, + "jsdoc/require-param-type": 1, + "jsdoc/require-property": 1, + "jsdoc/require-property-name": 1, + "jsdoc/require-property-type": 1, + "jsdoc/require-returns-type": 1, + "jsdoc/require-yields": 1, + "jsdoc/require-yields-check": 1, + "jsdoc/tag-lines": 1, + "jsdoc/valid-types": 1, + "jsdoc/sort-tags": [ + "warn", + { + "tagSequence": [ + "global", + "typedef", + "var", + "name", + "namespace", + "constructor", + "callback", + "event", + "function", + "augments", + "lends", + "type", + "prop", + "param", + "throws", + "fires", + "listens", + "ingroup", + "deprecated", + "see", + "todo", + "ignore" + ] + } + ] }, "globals": { "PR": true, @@ -54,5 +106,7 @@ "after": true, "afterEach": true }, - "plugins": [] + "plugins": [ + "jsdoc" + ] } diff --git a/package.json b/package.json index 6efe68fc..3a552cd9 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "encryption", "crypto", "cryptocurrency" - ], + ], "author": "ADAMANT Tech Labs , Lisk Foundation , lightcurve GmbH ", "license": "GPL-3.0", "dependencies": { @@ -80,12 +80,14 @@ "chai": "^4.3.4", "chai-bignumber": "^3.0.0", "eslint-config-google": "^0.14.0", + "eslint-formatter-codeframe": "^7.32.1", + "eslint-plugin-jsdoc": "^39.3.4", "grunt": "^1.4.1", "grunt-cli": "^1.4.3", "grunt-contrib-compress": "^2.0.0", + "grunt-contrib-obfuscator": "^8.0.0", "grunt-eslint": "^23.0.0", "grunt-exec": "^3.0.0", - "grunt-contrib-obfuscator": "^8.0.0", "jsdoc": "^3.6.7", "mocha": "^9.1.1", "moment": "^2.29.1", From 410c8668096f7c867c1a3498bffc5a32c6ca2f7e Mon Sep 17 00:00:00 2001 From: martiliones Date: Sun, 31 Jul 2022 09:32:25 +0600 Subject: [PATCH 28/36] chore: remove directory --- docs/adm-nodes.jpeg | Bin 241965 -> 0 bytes docs/conf.json | 28 ---- docs/intro.md | 210 ------------------------ docs/typedef/components.js | 19 --- docs/typedef/crypto.js | 20 --- docs/typedef/modules/accounts.js | 5 - docs/typedef/modules/blocks.js | 5 - docs/typedef/modules/dapps.js | 5 - docs/typedef/modules/delegates.js | 5 - docs/typedef/modules/helpers.js | 5 - docs/typedef/modules/loader.js | 5 - docs/typedef/modules/multisignatures.js | 5 - docs/typedef/modules/peers.js | 5 - docs/typedef/modules/rounds.js | 5 - docs/typedef/modules/server.js | 5 - docs/typedef/modules/signatures.js | 5 - docs/typedef/modules/transactions.js | 5 - docs/typedef/modules/transport.js | 5 - docs/typedef/namespace.js | 5 - 19 files changed, 347 deletions(-) delete mode 100644 docs/adm-nodes.jpeg delete mode 100644 docs/conf.json delete mode 100644 docs/intro.md delete mode 100644 docs/typedef/components.js delete mode 100644 docs/typedef/crypto.js delete mode 100644 docs/typedef/modules/accounts.js delete mode 100644 docs/typedef/modules/blocks.js delete mode 100644 docs/typedef/modules/dapps.js delete mode 100644 docs/typedef/modules/delegates.js delete mode 100644 docs/typedef/modules/helpers.js delete mode 100644 docs/typedef/modules/loader.js delete mode 100644 docs/typedef/modules/multisignatures.js delete mode 100644 docs/typedef/modules/peers.js delete mode 100644 docs/typedef/modules/rounds.js delete mode 100644 docs/typedef/modules/server.js delete mode 100644 docs/typedef/modules/signatures.js delete mode 100644 docs/typedef/modules/transactions.js delete mode 100644 docs/typedef/modules/transport.js delete mode 100644 docs/typedef/namespace.js diff --git a/docs/adm-nodes.jpeg b/docs/adm-nodes.jpeg deleted file mode 100644 index c8f3f8e76618e582988ee140fb443ae473ccf240..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 241965 zcmeFYXH-*L z0Y$oi6h%M@TiA&1ec11N?m2gyd&mF1_kOw`&dgYIWR1*Z%wJZ& zjQ^C2XUt5D%>SJH^CAC!D~OhchL({ILievG|F5dQ`vC0pAW<4~8lVUO#15ok2mU<> zxC#IOY5&zdAmG0jAczJ`3!%H{2Gd1@HnxlI)6iUWn)af{7p)&$ybb`f({cza=s`HG z-RMMuxfEkFp3#fyH}!Md%zTF_xrg8x7=oRpkG%FN0p=j7()mzI@RR902jJa2Ak zZEL4=baoBA9vm7T866v+oqP9w{=>rJ(#Gbet?ixN&tLYAsK3* z$GWKh9qfO}#eR_s^v^Y*`zIF=6mfCTu!CuZ6(AgX)^u*coFa;`^j!KG&zkxfM3rp5 zbGwJkF!I2ZH^h$qiT1B#|IdQq|34-BAHn`-t|b674e;XP(Xaz<0DiAEzmscVCR<6h zvbpG!2iD#;*G0Rz6Usr4Stmws)sE3%{Y1%8M1zg)#ojjB(Z=Hne1zW((&))Uj7z0@e!%3~}E6vcp3-5XLu+0So*iT#lBr-|#_7(@_`< zrap}zJQ!SU-VLIbZ5v&}bzc8YPosW@_oFzdwLdFs=}Zio$t&LBPAfm-Zq^+gqw^cl zuTzv~u}8WYz-`1{>O-pL3LoZ{;zsMaSw{NGQdqMP|Qf-EHGHo36D!?j&x_8byOIGrpGhl>OYwU zCg#2!ZIR}OQRfX_jjqtqIw8SouxKh(%9TvDo?p@!sEv9mESku)@+5~q;Jm%I zbgX)GYsn&fh(tp0wLFp7nci_>7X0Q-Bo3iyU?8W5LshTxf}c-G!@qE)iH(VCT*`XJ zD6Vrl9&(>o;;@OUbZ5c+dru2fPBM8@!m*5b&Cx7oqUXvbhc}a*+ESSM70t9i!hhm{ zksimeX0q%s#F`W11OVVB&xg4^tPvzna0eXDG8^! z=@N~F3XBD+viX?}JZqR8X-7Xn@wWK@mi4Ej_#YdjE$OKF3Oy;gE#Y=Ksw$SQh9-m* zd~Ao&`Sq1YHKMC%!mt?)%N!7u5g0c!OYr=IHHcRtd{bk|$eebl8ExuLyfxmQBd@1n z1f*=LGOMzI()JtyyugkT=bzCFj_46==18ERxB9+kE8J+DJJtZB6;~s6^O2bClUxpS zfT_p5S{GDcwf^yxm$FuT1jA*u$;Mxc)(cTpf}SJ=TpQN+;T*@U2l4Rc9?R`1Y@OBN z&12W!+f45`@xa#@{;4??t@HNt80Tm>X^g$cQPke|7U?5>$n)T~^iRiZwWo=oG(J!A z&MZ@^zXmk6RR676Or@?#AzKO2))OzOILoc1# z%K~kDlFpnm)HDV?aC$2iKyV>8)7UguB9_#Lt~>e5!%#Jjm?8b`R!JhC40S`7MwT`lGk)~G(6+9W+`BvGV^k$AcVr2b{Q2c*08ABh4|KexSevK0RZ+bg|Fi@7xe3e z;$`n~!o|oAVRuwjsK@8Mi=#K=1s(=qR+!W@Zx`GXR45(1c^Z7yoTdA!%ii{8Y+M$N z>vDHG19SpBEyA1@)-+&(bpwVX7O3W}p`7 z$AS%^(xG+@mL^*91znzj;F6t{&H{G-t*lid8-8>n#o=(Zlkj85O>V) zNVJgGeZzg5#kyX&{ao9zn`kILR|+fF&9zHs+_|2z9)A|XM^X&oFJEg75AXAvo$!IN zM_t2U350Te8WJd@EidWJVH=@^pauBJHltT%^n45KdF^FD_%dlwttUp%hfiZk<4jj& z+fPLe?H86?ygcdT?0AT@`s^MQ+Q_pv$lg!wgLx)AtiqlGm`>6})aZ|*s8DoqC5aA2ifA$i{p{GS+g?WwuRd zunwVorWGTON3C>4TIQZLx2J`rdA_L{flG?C8eQwdN%>ef2H2pk+gKh}*YoEcR`_&x zMcy=Je?DRG!3H%$9|QQGv^IM-G>Js^gcM+q{=a}-bPI{Us| z5oBw)zu#j_qd(s87qWs_c{vN z1l>>vq^~`Yzp?1?Qg#200H^E>M@uYEpFz<|qM>V_0$ly9*|W7PW!|{Sbae*(aA&(b zjSW`dv4Be}!G0{H6_q6rXq7Qn4rfpH1dKS9d9C|lPr&K@jOak&iNYXM|8x-Fu0PMK zDCKbSJmoaRgvF(WO}$Fz~O)cBg&v$;f_5RP~|GJ1y6;+FS~LI|F# zI}qlOkcZSCR!mDCdx{IMeQFoUxw`e&`x)&P^+`y+rJ)H|;PB*6Mv@9oF;=#Wy=?$D z1di^tvqAB%tm>{mc-!O33CW9!oNixA%nZt;AG1H#r2nKPx{}gYNN6{%G5*@><*b!- zPaRtK4P{OXye6J%m1*uD<<20~LmcZq#kzX~ z`5l;u^Qy6KzXEy4|t4o@^ERcgHq6fSa%4U)=Z0b|!{_@T#n`M`f>gNW_8jGNXMb6y-h_@LJK1RZ$BIxnAwZUIH+Z?I%mV`@ zD(KP<^6;vj%u~AJlexPQVzJxIhT}(Fbj01A=^4Uy&*aa`*e8JzKG&b?lzvDGIPw>( zGa-VP8S(nRUK%w2ICH|S+y1ofK6h#QI4Rd4yMVxgt*b3p5hm*jqM<@7pXMrO-Te9C zj!TQn6GF}Amk0I9Mg%@R>$cQ%0t8g@nim{12^5j)Vq2ZKCo-jc+rvrr^8)1aDYrDx zwfq4q^%4t?oI}=(54z)IDfZ4J*4oWQs7};*3Z-pSSJA% z+}n!RSdgu=fyk+rsDdRG_jtKKsZJp!O)GuJB!_Z)WAj%jl65a;c=%#d7Uek1hP5eY zk0+TfRe+q#r*%eBn7) z7cK)+S}LzswhwUZ9R=0E(Lr8tru(cV&)k9cm+kS<>N}eGQ8$$_KJaDhDnk5(hc?+~ zc_zwLUR|6?R)24O%nMYYM>{ALAbY?hJ<`r2Ij$=iwBM7k)I(I=X~-ijqtBrn{D=0a zOzCbAH0_d^=dTHT$bCX=tkcPVgcn^Q!InwlW0HDv}X31vPoHL=Y%|UV*PkR z)so3sp{d7O<*?YzcY2mjMv%zgPPz9+sMF;L=P%A;gV8@>2kS(OxX+@>tWMdlwkVD3 z=@ID005701b-@$^^RCX9Z7f1XlS+w%$y50{jKpX?34%asS!i;*W#D{)9wamx?eF11 zA1rv66WiBQeP(U2E@v470!-_;(N@1q=v_Uz);tytrZo0!WCpVf(>A@v@!MV-Ul{xP znuE`3f-Bj7dn%Nf5|U?{RCxym_iO=82QjwOeJ7Pa&6SCq6WFL!R)miyqO{wH->7Q9 zetR1r%b13l)E6AXdg~NH0<%?Jc|-P4j7i>#t=*DYZ^;y*0b_p~Yju11%ZJmWy-N)v zJ2?iPp0*0P6MYN1#&1YTG=zy8)R=K*R+(tPE zmb;~66EB&2{!wb|v&+-2L0kKi4|$Z7!`aYwcos>1s4(h=YJ}pt>u$7ie4;dxU(tE> zb@fN%z-lOO;tfSpDfbmuFLu@UL;yYWC~8-%11blgNtkLJ+KBSzy~Zg!Xwih=?`h0E z%Zb#!^5SV;NTiL+(wCF?2i2yc-8Ceee(m8)mlHA1tlGEDdryt!I?=)lshuyDe&;ojOb=rYCr?pkw`ULH~^m+OMz0Ms1JlP4G6iMhyaP zZ3V1cvVm;FKJAnt$Y&K)Z-dJNvCwoa)pRvWX+^%oF&n-{ri@`daE@;ZW zaK%^)S+%fw_X7udkFDp}$((gVXaJFR48Ngj8-B%agdaw5l{Kn*nUK&E{zW_dXxJlt z*i?Ve<(VUw>Rg8=$xe-Ghdhgg&4o=_{B_X#k*9b9)@&HaL;pUX*OM|J_`DF~(eZ0g z`u7H|gvR8*<{!+|kJ&fPAIwn#;jqDvn$J90o@P};l~k2TY#tg~_WWX%-bLNd3mSI^QL*E@H2!hZUd@yCx(jp^BSYBcR8iemLFM$Pp7*tc$2-XsH#J^ z_Bw2~2p~l*){biBuugm=Kb*GfP2i5Qfv*MwN5hXLdN=$lIWBTQ&1qv(9K&If9W0l5 zZN{h_d;)k&0NJ+pY4MHd zBFE8V!^)+ohRb@I55K5=*TO?hWwY-aUQLLad@! z{k5>%!GkSOssAG|s&b|Nbs2)>g7R-SZ^-CAJnwYxw$KFxsTOr8_A4dDiQa+-WpKdf zC(VZNZqtZQI3#Qxgbq3j)n)fFox_$TDm$h;JiMW5lf-*!GM*A7AA$=IhzRb7aqx?_ zl9(Rcu#L{cdN@JFtOuKDE-NP_t`17kg>OC#(G39F&NE={3F*8q=V)2t1=ejbf>fI0 zwhsqmj{ATN)9RQH`#K-0{rttwO`0_lQE!(M*yR}~p9_X`zR`er;zA$*=&Sde`qz|q z+B;>ngOg)dw})WFM5{hmj;oR2tzYF3A@0Ki_pCd&Z?BO3GGw30?`N?8B7XAY3+|}# z9+~K=hNk$hd|4XGP8e*QY<$CE=?~j(zqDpJ`s8sxv8X$=khmJfyr6<;OEWK-V{E`3+a1wC4`UDtR+8M%hATy zD}|pV8hpQLJ?}=?vvB!q7dPZPJ62V&Aj5V%L!Ip50X@$L_VQ;3m__Z8Gms!nwoMr= z^2RkAfCeDuzXYFZ%q>^%dQ|nn$fX zEY~X&?4aj{3d>{s2$XwB3L^jkgzN4_j*j+0IabE}uT*h&=`81Ykc`e2WT4W5gyU0V^8@h0RICBxg(T5Vy{HIK|K zaFsUc)AfFsl&BBKmtfh7=f)nPoM#y_F1O*r9#5GrZvpzevSKRqtosfU9rEW<`@eME zyeK4yArl;p$}a8q=%(%HTXJM$Wi`PAZ;tcisM-hnM`K&h@3m!A0%ZF>V()0Yne4;S zNnHCwF4`c;LoaM2(}@LQA26|~c;5!?Z> zqRpmu_({6g3C?Zm;RgkbXpC-0Jr57BCcWIjZz7A{t*9*}@rkT34=1To$mQvM_s(2! zK<&nrkLZDRxxG|nS-v^k= zep)^87E-~JH2B9^;``dvlZj;7vNs`LxKjTj%|xLa*?Zj?$=XfhxTjGjM{y3=EDHM` z`9981bwA(Ko|g3l=gK3-CU2{yb7Vj6`J+u5jBA{~BKAJzH|Iu47v* z&E$#EYSCpg_LqJGGOt6org3!heezA4pJRjTBWbXr4fMeG_dT@(Bm5&Ufl{sSgvQihfArP_8ll|~iyYN@J z&5+jO@2ALD(P7Ag(X2PE{6)_4^?WXp0xJZzs_+%&dw|YL11@`ZWwo`)4DR097Ny!> zx#MK-zCOBdSIW0Ys5G5^_$kl)d~wL~ipU$Rg+3Cbh_PP>RNUGAjL?Pqk@j*FRI+X^ zYmRBtZnuj$Wj!_ux%joI701!N-~;qmhz*xjE}*n;drdeGP)%WpNScQS9khRW#AXL~ zfEcDxD?pJi2qi(DJ+Bs-SK~zpQBQ?^_rbswkODt5YTD7wNIN&IHWFgXMKAbQGU0s9*{j;3VU(eOpTXBP*Xw=t$_De(YL0^(%l8%$GIhd*pSn4Gb zy(Hj#p?{h&k*PJ-syw`_eye%;qw-sqX^Nt=mqBfHyJtq|doBQ)?$7Uv!47sTX}?Z} zSMnMfY#g)Vz3i+xI8r-QXFi0hLkl0Qj%WIv>V#31$B)dC!!p}Dp3Mh@=}A$^&tEArnWI}uNQV+8t0|$yF3P)W_4ZJ9p1n24 zx3Yti-!YlKA)R1(e)qm7SvUCWx!Zc5RxJ%_i{Da4VcWL@FS|6Veie`saKNrx)(o$+ z?l`{;ZFqgM4j*jp8qYA+X3t6BUb-T{3Ka7KtGV*wf(!sgF!HIIKjsn4Mo;3KcjVi_ zCKnl9cMclzF$1((?4gU4{rX(%*&mc>|68hG-u8eDwB2abnJ#^_TNkRs{dy%`%otpt z>Wld!i*%IM(@dW>7!jcpKEDwTfw3$n0tIRVg-Hd_G zl`}t}6=dt|p$AC-f`dIdIIR)d&rw9Hib_m9b{}O(OG%oD>24gS0qvpszjVQ*s#?1D ztTO~_X$%6h*jvt|<Qa7Ut!JIX641Txpq{(a=~@|hP&1!!Y0hsFkz7GbAjW1~|(4HaU8R35yj zvy{EOdb#~H34@pnv6-)}6ma9?yKf$j~Qu$IZKhPS<8FxUcfm;j+@^W z9sr;=lXsQ4=;d04{CDV4^)|Fjx;ZJK-yp=!-L#>H=2>Y3!u8OF>Ay-8} zsofM$Zt=1<+h*T2)6e5yj(+2ju21fM%Nj$}xWMMPpR-KAh6U@aqlAx*8+p&i5W zY6V&*;zunWSG0!93tdaZ-KCoXXr%JjXjD<%LA)A#&6+4^e44Al7_=x#(%Ht-y zVKB`0luPy9BY|8&&bJbEJ&a9bjG!7?t2mwhH_+_bMi;XK!KSqd_+hmNC661%{_xx6 zr7PRCohufK-QVeqe7&C-ylieMI6yzkdzQb1suZqdJYpNl3*jTiVG)~%C=E!VQDrSA zux_Mz-&t|lRO~gXEf^p@xxhR$$`6Q3A0+DX%xb`2e2noeerR5IiOst;HGpvjS5Hk_ zobDTCy%^j1HZZC_sB1pDZ%qZmdD)&PZ6_ymb8@Udxi-0=M3$N63r=|AlxgK@t~&i? zaKk%0S+ZGDp5yaSk{y$<7(gqfV>-0!_lk3XOrTHn{o2c}BBmP0PT3!>YG}BkeWAO- zUT<937*8U?|MZ#VCrNsy2xGWz2L#kLc>pcPRVjSZ5T}$z{%dHhPAAX&eYqbW#p+k| z5i86-kH!~lx*cOm$`23yy<_!OZ#q3+NE|skjN#F1z^=1<|3Za$m+5=YD3XW?NwqUOQKF?B!*?M^q?Xt zBNvCnvd-2;cosJMf#!dHJ%DJ{`U!za{b9~h4Y@FhN$EPSPmBamMqxToI8DH&{eAsG zTP^*TZx60z>5#T8ibG;=VO?5xl;Jd ztODePk+!v7&FWTvx}(7W0W9+=&AI*5`ki7^|J;W&DlO18gArkAHKFJCi5B~;G@cwl zXNT5K3JR-&SgyH+UR&;Ql8!ZFWaGr!wzCGQhxT1}FcW#t5!J?Dyt8iv|LEno8fdX# zU8&2JaaGrnk9p+kz4(I~r!hcxv4&e~e&pm$gI0()bpSs)c_&o$Vxtitu8+2~cwn*3 zF`?{Hr9LPscy(X0yTto1=Fw+%A(k!&H=?%z`H*mH-dq*#PX{65^LDvWUY>wghr_JD zOTtHY1-^bsIhhasa{6I@Ex?j-W!0P^P}P-)bD})>OZV0}S`2B!yK_s)y1A9od8}^~ zj&(!Nkaawo^~)PhR-Y6>klFcCo>!st%dvet}};A2^Ld z>4{PVwX7Qve*Z`1F*OgprRoJ8^Lh+g9SHC{!cCmf10 z3VM2I+-sS--ovZf>@w*^VVyJ8b_07>uE#Xmbg}0;dW`)_XL|FR|x*!)tgODbM7ve4ccwK>SCxm@1;jvQ|k-7~SZ zDmM>542;)iWjQLt5a%KjRj{VQ!K2}QLtTT8PC`4wRA`@wuKy8#)Z@^+{IdT4fE{f5 ztyoO?;jTau9~aY$Nhnv9XP(GtjgI+^UedE~|<_^0CsD1-)Hwk^$= zP8T2#wr_dn`%Cx32^Bt>B7V+K;8Ywa^H^^Z^yz>lzyda}NjkeMIt|qybj=;i&m(dH z6q*N}F}pXEKmftQS8v}_{sS(37DFA%)*Y6TWy-Ku=mk!Lv+uykj1^6-M1V)(R!GlV zuquO4QJtoFVnr}V|K;JgZ!T*t)3QH2STts}U5^mGtd%eR5_ZS3=2j#YyNd`iCE_E$!o*x1Z%pkZVVi0wiqi)=wv=dHGC-6ou?gIg!d*!JsIXczf-%%biQ1+YLLno=Tn%W^W1N zZi@>^ub_=W8b<=P?N1KiA#T>~l4hCb!@Izrpr9z0eDXY8^s$-gmT{p$ST{yH8Ri&6 z)YGL~8)gl%mA_Ta*v9qUul;C1#BqPqGf|SBengl{@$gYLVpEmKSLo^^uuvTkxZAF} zHFYn9G*~Vw{XB`7b*sNUy}PL&{ORxzP@b{0M^iu6u*?3-lNDw&Thw292b#G-4UP%7HWV9 z#kaII9^Gp8KNEg54}wHi^`xOrJ3es%1qRup(tTd3ARVh;UbCpc(D3%tH-bcD%wiGS zV3S2XFbTI+zk zJN)%LQfk=i0(6pvwk|-YT@*IK`ku%sz|BrRi*U=r(!R@#n+tRHy!_zhhs?@5rd#Ic z?-%-#LB@=`q1)xOx&oQoXX(g*%f;{KrG(WKO}P>;U}vZ6e}NrIP*=d*V$l!D(RAC` zXOwaJ#hz?F1o8Qk0G~@#Dk{9Q-T(7OSa_z3M zDE?b#!YFV+{}jMiw#Ry~{hSGY7gR@ZX=cZO zkd?1Wkv#GQ)pF7j#ulpln9CtB&(H~gs*u1`&iFfrDRhK2CTPPtTQ{vyy9cCJY4I;i z;#y9PQ(JsS+J|SBnjIBL$O=1&j?phtKkRH>aR}c}D}QgUIxz3SvOVv?%lL0n8sEU2 z!3Ucptvm_Ljk4p%75{*av42SQoGEV}2dQL`OWYu4-zhd=%QFo82OMbs-e&5P-?cKC zs*w|Z5&ZSqUQ1Xl_qwgBIjgmJ=5rLzw#@?f*cW4xlhb)5qz5Vp2R=oM+$66tDeC$#vTCg}0#lP{nF5 z{~tM?Df}Wy=po($0U5Ys-%V;-{V-!J*}V>8o?89KXZy8T>XE(j@E|#Kr$wexCuGsB z`@d7tUSj~r_MDI|l<9r#=r-7F2jEyhd^8rs&G6U)9~BO z;E+guk?X?Gw)))V@xn)_q?#l^FpeN4Oocd?%>0mf;LL>a9?-aaL)bn#06)`{%=sfA zhz+k#WWCR`iQ^GSNY;l_LQ!{*bca{I*}us`^?RQoT?czza&u;)QMR@4IBk%b zG3Q**b#BSm&gN39flyCImM2+4vZMs;$}z}VMIaDBK<#h-S1P!>G|>OD-nkH6D-HGa z>~UamGzh4IKEE-_6kOgf)*^#=aaU9ybTNkm1py34MuRley+NgoUHLx_ul!u{NbMLm z0B>UEh7si{%KKN@i zwhoH+yX*>DVL918COz~912nh)N~~%`?jJmxty2+5BGb>SCi>3IuSlkz28^qRRMG5c z$PHqFy7>$?eh2%9r6ie5$e*r(tIzDH6Y_SlY2`Q?!CldeDchDd!@R>kBsdUDVLY@OWv(Z4tTrMr8yln#N3k#v4<_`Kb3XScQH z537eZbEnKNt;po&Z7^Hm_dI~j%Mr}35ZVZi3EPE#r-%yJ{+XWT01BFDY_Qj9UWy}@%x>_ZI@M= z()K^YM@_cm6a^R(v^>?-r%*a=UXWVu`V}f?Q1`Q(N9;v!pO-MIxwfXBzPyvA zB$QaA3CqynW$n9Wib;o}ByVeh02>g#9N1dZ*Ee>3th)T&p}W6&Rl64{l4&Bqq{)V# z`+^9Ad;Z|N9@z&Wh@cOy4Eqk;xA~{H_}6jMs8KNa#NO(6 zu_=qdZ)U@kFZY;xJ>h0rAUrE50eplTWq>9szl%Niq~NkR#?sM|I<7AYYovl0%sSW3 zzz}^szaC(&}EvL%zF2^TUMJ4_}q&?{OGa!K8NAbXfJqN5oD5?zQ zwWBh=KQ28KR62W8Q)z_ZEU(vRWUMnU1nBa~pW|F3y_D1XuMg>NS=7UNHhjugfdI(} zSG6@*;q%6=2Ycz))hzI90%@9iYxB{8;tVmZ)c4KqlL3Sf{uy>#1vroYVY zeG}Li!3B#}V%5XECk=c>`CS^HdPywq^yG&DkMBmj?ITOS zQ2FC#zhJ`p;%?>VUt<}1=Ow7IX&_iI7XR(inD5C1SMWeHjFD0b3tN;R$Kx7kf+{qr z5a6!zXua&;fYzsGeiDV+#rLKr+s}=QF28QswJKA8?E&WC^U^;kL%k%eO;1lj6Yuw!=tCY z4sZrqvI4bp%KXv-hGXf2ov-Ync7g(WFs99_f@?V5V_VJ&13MfQOqY6brHZCZhe%MY0nSK%}ZF*|;VozHr! z!~OOiNLg7~jB)|5DJNGXv`dEu!F2!_4eAkE0LbyVZkYxLN4VX{xmR;Qn0%f&9ZT^X zj(unq&*6j{UT5;mz-0^Czw)D`_#JxWUF_%#L9TWTe!19$D z!KDLFM52@?Fu?&QJl8IhZ}RjRPqlJz0@n9$?2~H(cHU#K`fGk)b}ZYL)Y%Qw5)#y2 zeX~TsS(#X#mqqAqg`b?Ax+}S50J7jW+CVA^3hz{ZVho4B&QDF{UY3{u* zi9I(F6Co0TYX_2p&CJq8Gk*vIP<$&!0Q))dPIj2)t$m8>3_Q4%qIGM_ritV8__$Cw zSuEXRM}2f4C(Xt`*~@h3P75mNMb(P2+`D^&Xg$wSv7%+h;2!1>*1NZ^pybw^R-(*- zwrIV9;Yyf$g5K6H(GkM0&ZptNjEby(FV<7+Cju0U=*YHP2oTp*yP{)JH0Jg+uNla! z_%*`0U<4Bst_QWx*R30*jU8wPbu-05HCzZ=7%*u0rTqg0+Rei==CM(9TSx4~@~Z-- z0N2OcK@Ar4P!CuFouVJvfqcdO34GRz?NJrD50lI%UAALuf7YxR?|ML&^ZYG((bSF8 zkU4k}g<&aJS&Bav5$75HAdGScj9S?xa){_mPMc(g4WriP{_1I#2Ox={9!aZF)#bN7)e6dS_qz7p z=FiD6*KfZ4JB2#!KnX`(t0XJ?iFV$^dVu?fLqZ}hZM^x}D1A__HWns()lyz0u3#f- zhoo;z{q-sM&2)M3qljWAW6OzMRh0B!*m3?8ezAa&*6~3E$%8NAsq(M^I$@`qRAL{X#5|OBSF`J7z9CSN2)*S?4H? z^4If;w)V^Ut-N1B$z^HDtl`6xc0qPIp|o;J6c|YHuB^zZgKAC9THO=y1e-!%t<)n0 zcZm63_yI!E#}`0o6!fYyFD*zaQT+bHla`C}e*CeD-qGWe>q|s2m zczI@DJ9W7x&Mj0aPC!P%b+G88l<=y<3Nsy{D3FG_+ba z77T8pU*VBYj8Re&e{aRej1SHvG7!H8xje|{O^5o}kyj`CMnt@|yV`HWkAvb`Nt(=Y zx-4x?=v67@#TRxvt)!JZCN!qWoSp>W!=Rv^zX5HUnir>zhcfFJ9#U)m{iw@|gM zBI+-c6r;wH4`PwVWWDK?tf0OKn!;mUdQ9(}#1iI@LA3*I%g_#_YfO|_5gNB(XH}JM zb#$7by0-eMBk|S^ljz7R>^lKwip&FymES=KF1B&~SED8kQMEif(|<{Bz1>YaNFRac zq{pfwj2MAS;76(})f)X*HH6{Al_vv;x%{DwXIH(Q-v4?(zysTtcv5uzBr}6J4n!MJ z<(!pt^8N-~ISQCv(eEH}3Nkxp{hkqSO6!SvDi`GE_NeWnVNMO`wqmlXrN9KSS5FLf zX3H{UJV7NdEsT08>V=9|K-)5HCn&u3}$6ctPb)|KZ9bqZtZUj%`gxKy9 zICxkM{=6nqp5SeRD-8Pcc3NgJy@*zU^|KZMC%)9YxiD(`G4B*JI%Kz~FC?@hgUI(? z7v{fuSH^e!CiXPZH$0)wNf@Fa+OP|IQq-w_9dd~__b2&JmhEzqM1eMFlQ-we?UU3H zO75#}*E$8heywDhP)~Hn+@e_AZ`D9??uF;g8s1_#em<+@<7$WW$aDT3@jw1JJOZT?=)v#^?Uydtdn?>;#C<2rUOH2Di3CI zdawaCjT{>`f57u)NAhQVq4iykQq4j448ChQtiN)mEvRL9y@47H7E7`veEF`Y1y(%u zE?Tzc9wlu}75+^PW1?8Yh|BNmjyE@S-oQeQ1e;nrGKBIuLUZQe8HAGiNAJ}+ zoA(xZJpW=nq0;PJ|1#fzA441;r78NT3b2Fimca13B@9sVPr766?$GNKpDT1o1((P+MTMrjKN16geHIa&n$(jmqGpHy6=p`GP+6Nwo(>&Njjn}doua!W2XPu(bLb?@Gnp`AXv!)40 z0}s6e^Ny%kZx@_{`qJR*s-a*vf8TleXDx|biPM`qY+{~)!nPrw5`o9Ws*ncuq_gcO z=5r;K#7`Dol(ypKG5j4_?Y4m276DPg#4tlUW>bLfPQ9LgR8XqQIVzbm8i8eOYl*4y z76quU)RQbj%A^MYj599C4-3u38{&CnRQ+L*6SOZr%ClxA-S)ehYfF;`*f!4go&6@Q zFE}GgK!yEs+L6T+59GZ!6tHgz2lP+Vi;?Y1r*}TS-n37bglc)Rv{g~(Wg8sXM);+5 z$1`5D_o-U^SH#oDwbk|VVo9@N)F&MopIm_JtB%x@wzy*5ltZ4gAoS2eOAH_-^dcZt z5Cc*INbdp)gbtxsv4kq!5Fm5}=~W0Ay3%`5nsjLaWdSNEim27@wMGuy9cFHsZL8LRi*__P0@5m99vd z4%3g|#McwjXI+YJLnzu9`!fF+fzlMru?+n1?rRO}#T=U0(MD$u)bAwBfh?`<_X>;e ze}}?2gfk+Ax(f^QG{CRkCz5F$*e9X1;H2bZsw0!!?v z6}*3NhdNEy@(xsWsH^C4n+SmG<;ahRrBVZImle1Cf8So@)V`(7OJ|Bh-Jx?z9l&V@ zK(!2PTE;t}>$LaFi&VoM?zdZ(ild>s3TV78Jpg*X=wM(J+NNj~uZ`b7Qf>Fu-%>cv z<1-ID!PoYvOu-AlX8osdEz*nm+YFIU_-|Q#zAR-SrY&0nJu}FJ>!}iH1d{;q4D5Pp z6>kz#I=WMu9<%wF(*D}MalgR^!XSwz2jxYpgh(h5T^bp~1rs@ekO)sY?-1NjC8Q=L z<0P2xXxp*Y?|-8v;5RO=P`0zM+eM2LW7TT!;i0!YxX82`LOEbx07(x@hh){k-M)Q$ z*;7%PAT0VxBltdbir6v`w;8V52(Xvw9Z6!Jk(ReH-7yx9z%)n|^$l*DjX5vUo{^$8 z9l3cP|H9A9{}VO!>H7J($_IHU$zJcCbNxLEN{((%1XUMNAiSfw8je&1una0M?Cfx@ zdG{hR4nxyGrh-mRh9A#o-y~B;_j^^Xy;cn#ZP3zV%8rzpY%$=*NKL(sV6W5akRv{J zC{ILZw5Q3*R7`Y8tm!n}y)$x0tkkugt@UGQrn4I7dr0Pjnme70l%mo! zCK3O@Jd_F~{Aarwg9x=5EeidQ0phidi?^`8YWE9O@%=Zhx?PtWJMJHQL(aSPjQPt( z4r7xbmBszBq<4xsCMi6{$6tUv3$+1N<35s-Pw@LsLR(o>BEwFcFexlQ8#qRLL0}hL z@$Qc4(*A7=cO%qfdw4nN|HnJ9)6)gBMJ;D)<%6;p@smdw&4_e12DCS>tO?*ED&9}u ze#e*8BA|0_FCH_6s_-ah-Wl1I`5SuDfKb@T2sME(zo{ulOpf72 z;(!;vxeSGlDrDLC4>bWx4RY*PdeL6sYc2*p&Ye#voH}SAqEPcVtKOde=afl++{yZ1 z1Lo^XBNOcTD$Z#pni*^uoFlsssj%|9#a?P(ZNGZ}Hw)!64W2ne3S)BV8H}2{AOs!P z*E+#9-Y<77zJZ^ZIz0{Z_!jVT>Gfpe)32_pSeXHSqtI{k^t<_ZPy0gn5)5-T; zCwlzovxfm0PYdo!fs_6$0@XJnNbN!vcl#Hc#a)hN&ffJIu&1O*p@yJ&;Gfu5) zW=mk4fsJ!sj}`{c3c%Usm%sy%j2=z~omZNUygTh42|*-6ZEg#^&UPK8gfgV4{N{hm zS1&hT#u@OTBPrQ6szf(Z*k&s+draM0S`U2KLIIRWPt)+Mt2>f%y=GYS?EfUDuDwRz zVb4m%+?{cziqnG=gtHP!iCLVSCxen29VWmpU`GU$fn=P0{v0f8?=b#9c!~aRUdoTN z=T3Y4QMm9yL@=q}oRR8B&oh_QH0~`$=h`vikz@H#=rIr`f395l@{MDAvYF7HO+}vl z4n#2n+hBQ97j zI_6Z1KU}H=mC?rkL`w#i=m78PI(=#BLtcXfp#+_crr4{Sxw(opSQ8hvlmaY@sXPf#NEJmU|}#Yt>!;Z2f~?#pJ!`3K!R~ewbWBB8;VV0Iv|- zSFS(w`8p}Ku+erde}x-4RV&q*8Y8H)%3yB zL$hDc-WlF)ZTWn?*H5PB`?0~bR$yaIi7|J{_D3_%|DBc0R`ON9muB27QX;8x6YpFD zZNk_^X*^het^^kEf$r~MX{8S8O3u{?my~`$6FFw~VfRODazd#<+*I^*+dv#d1MFE% zxiaM3-i(I_1aM`>>gYM`fFQX+-N&@x*l1wy6;j<{)`DuSYCwqZza03kj;(so(B)D^ zJ?GG`XHz_IF9Q(UHF~X0RKNwSAsvf-pRUwJk3k9v9}MI1O16 z^^TBZO%x0grk+85#dHn=(G(p07|)+goNpy*(J5JcuBgp~4HgvZAm zlB;bC2NIe(pNTr;no&CLl-$s98jJ9e4 zQ{1e;i`L&W4;0FZ{Es8vDnV8bd4s)~rVB$;wi#1cP);~5%q7F^m8GSnGMVw;2}RcU z_Z2QcV7lo;lUFJTihvfh%HQt0;8Xqlh5L5l-1@Kk&c1NpLqEabq^48d*{?A7X!P#t zt;^1f1a14jQ=ESnq#vHh7rT|QpGW=7k#KaOzTrZDsqdBh;I#ePewy%Mk&+|2!FpZe zix3wYuOT$Q8Wy;kHQ-H_M^=#cgsiba1xm`$Vi0|@H)nnr8h(>Gvsu@Vtzq=vF;E1N z#txAE*2K+;!bf0AGhiX!VrVAe#b>PNLl4jFaVW{ACs}D;9uNCH2n!vxH~st_&DmKm zOIdsVCjEG6NZTXIh9?-E#f-ca+&MfEVP0-s))p8|;MN|(BkZV~t! z1_A{71dOGeqmw21jSEQf)CRB1N8E;*Xf+LTJ(2(yhr_St9Kbf-S5Q!IA zO@fuK!V>Id*7eiqKs@!YF9pBtz}wryeEH4geWL9!oq}R;Iq*i~)Mgu2LsE9ovS6)0 z3>;=9icV%-nUp^5x#D+BIt3Qs2NZWzG*TzqQ53hAZC1tit(if2F2}l31*!ZHrEzA0 zJ@Lp*y3oM=h=Tc3>*Vrh1tN0)4yzv`+|(tmc;IpeqOd3XkJh93u@ft)!{Hkonr$~F<9*ie>Xj$&P;eq$!qseJ8C6YoY>&t~F3o7OB@T2%f%;hgRYEml3-_Y?wL!8? ziJ!PBblFJ5FE{X^4?3EQMr{!7`s+EHmr}i-;s=PGYL3+B?$Cif`ptM)h8kN`D`T6B z2Ab#awBko~raXVowFh&l9gtDq(Jt<|bp7_8qivi`9w5WK`(><#SNZy;TNM3jjc>MG ze}_p;o2uoCWEqw3a5P-)#EaSMlh^oZ_$I~aCJ$Ol49}IS0(01C9bZ+ZsjBd34f{yH zxMuvT1ZI=xoiAnz@_|rj9`X?(ha$>n;mbFreQh1=rCzlBNZo0w!%;%)k*vvzJ)Fkb zyiU&k)TkB(> z$~px%uHB~6AP*&&VnM?_?~zVzJKNAR8(;nXgc@ITyT9E*C-=|NQPA=IF2){6`}t%` z%qz*(mJH(;c7Jf>iAoo7=FQBr4!CwzeX8REa+vJ#&x`Y@4iEQd%~Qr z<}Y@w! zp<+Q^Gj*fcva0vlpMZzG_bU_wC5#KB^2hl&c9E)a(<1Q{#9p6o3mKl5p3x*R#tZW9 zt#nWTkiZmnhBn6H2y8l%O9)kIIU};b>)p1Kn5-)x89OvKkOgoIERVmY7d2-t#W-ug zm$ZE-XZLXb<7AsyV^&If9I_!GdllKMszb@GzHgU*U8CHatB8*7ZVQKf53JNIiJeni z&kgZqX&TB;2)MiEX%)s5w3_X~O4fZm79liNK#V!|XENb=B})^g31si+Zuhuz{6|RL zMCC+W0eCg@)iLT9r%TjXFMvsjRJidC#Xpc@G9R=M2`D>bT@K%RHXB$?*_2vY1cAA)zELN?)6j{o57wl^L*E7>cNS{}9WhaQlYHG9h zNpZT86J=FC8;&kY6eZ1#iku4_1*di&!2!emq{d&neo|p%tUvYO$jF&B?xMTbsBbs2 z+&uU&2tbD*q&<@vC}nxmq^fu4X?UCn4Z+w*?91A zouW9SKrbytd|;V)d!6WYEv~CJLQby&Q1(PJ2ge;JD4;v?N8ukMfUSuzE>``8Z)|7Z zw0n=OWUy8Au-8heK%b|%32P~P0Mi#mG**Xq_uh`(`k=eiNka8t$%{QaAD^K#68o90;W(2aSv!Y&LWO)s}*7}zsrYpOwOLA6Hci>40!oUPn&GG4V>{cD7+$jP!3$>*_U>vKXg92RgTLZilzX@#3PdwQbe};lS)3ODgR}y-hVNtyzxb`Wx zLhf&uTtW%e3MINExqb=?Qwz`fOZ! zx{Z3*o+W~hY=vbrn4ckU%%Fd*?e~<)mz2RR(QYv3%c`!X*}&#e^E9`lhjyl|=eN=g z$7##;1pUw1<#=B&&tP>c`CGdzyW(|nwaRZHgP(p6}ztJO<__RzvQg=ne&z-ZSaGU&^BZp z-6|bi6Kk}=zJ=wR{g^Uzo%h50SuC__ouz`9+EzH z?|6n$B5Y33x~Yk36hJff=zFMe@Ah%eFsRE@6YiZgJy@qMyPG&2%t?08x9V|}S*$!EZ-PN)Ud43V+P)ynng*7m#DpgWI}tHDQ1d+V9I4ytJW?Ge(HzJN7-ylTD`qJ2UR)XW zlg!qGU-OAwoL96n@D`QikUw`%ku**BZI{9Zb{1lX#rNgfO|3AO4u%Cv?`Tly zU6QjOe5sL@vU%|vrIK|<+xX#o+wwrpr=S4OiWSYbgviHun6wLl?0V%=Ld?d-@f&PV z8j&(R-F(se66U9_1*zkTU$<$j%+qzXgp`J!hK*3-b^+j>l5tk{H_}XmwXy#PwvJzU z!Rv3PJ5Z!Q0kx_|TbikBfx}cv6oiGVYYq+7knZj|8$AYbHaX(JV3{Si3SG0xMxe4F zfRWg&emUg#n^(bf^EZ2An=D?9k4h=T)mP2oqko)k)L0)nJWk{0~A7@*|9n(KCQB= zi!n)4DQ}OzV1eV6HeZw0yA#;XKaMk+JswBgOCYrb zz`8?F3m)qV+Ckj`vdjc46Ik@#T*#o3VbM1;gX9dM0nu5V5h2nF@#-bj;whz+BX|M` z(!(SQ6q#$t=D+nY5LF!jGV{|0zd52B`43Xng>xJnO8T;fh^^DfkM5+|sqktd{0NsN z+g(>D9Ig?jS3#FIG5rpIQB^3dnWQd8t#=W7=NelJP+v7W^JNpq@#JV>^ zhehVjWy)%3w2Lg@fIl~_j|)M;tmHkgFiKe+uHBWssg381VR`r-A4YMv;avw@V$vr2 zHp9c2>nR@~Y%K25MUJ7dVnD2MH$Qh_zb8H2D&?WboODY*M&AKZKat+Qm-03iADk#vIKlaSz0)};&c7c#T13~g62B@35K8;;&aAn$ zNQ}X54b=7`iSxLeHMMMq^DJ9rcSxDvKWOrS(diFXs+#P`xIBg3>w|U0_u4VI8W2}l`a)z6Y^qTk5fukh_Sb3T-ppjW8gWElYIp@ zGPKVUD6~dBwTWoh#tTbvClJ}-5)cbu)EEK!L72wWVu8W^WlKJ4o<+^XM;7``eeE?1 z1pw?=?9hXuoB$=Yhe%Bp70cx89x@yYQ7!yBRDRFng}l+#f&3 zr=U^CbEJP@s+)GkZC%$ABUconckSg<*>4TqB(6eAe!FTKE%a?*#@VkUIsXx=Jj0l> zQQUQ%^#$V*EH^$3`|#c303zuSF2@upJe}22=2o*`-ot~l&2H}4tt!mt%2fri;Nlra zD1&6Fh8`vF@U3>`MT!ZgW41|Y=v-GwN+ibu;SQHQ_OITYWm{+Zs0 z2{pkPO5L~o*1Tx^!DgEB5$D3?u1Rvk&qgM|Vms`z_%R)T{x+tfggTpXV0o}?J<{&l zx`CLF?s;4~OB>>&tF!7+$aNzmXvQm>&Ff9h)A7V-K}tDMMUdO?SV%M@c9H&i`*39d zD4qY+EAL5NYdTkbgZWUp%Ire!{^Zqkt}5Jt9e$uj?jvM*qXpy)39q%6}%bkHIWG?(|av8wY6 z3On1~i)R81>8YHZ$?5tDh!Y9?o{>i3cGzdoQvj@4qT| zx~2*Aq_OH5nbKpTRb~f@v3QvcM^s2zIQS>ljpOFp7}?tZYK1h@9NLo_GG{hI#lz{7c-j!t{P>)Pg; z)LPs`WdSx0%NO9gFQvJ@VQ+_Wc_eEIF&s3L8)05kBT=$UFwv>SO(Dh9j=hxV9EGVr z+)*N(>!-iu@Zi-69s2#XpGJtUtc}T)T*CJa$j1F zKhtZc+0vqI!?cBY;TF-;Asa`eeoX>wewDO^9Q!$FQ+-2lgUxrrK}Vv>S@E}zwc^LC zhNoBI*%U(Xlc=`nJt~d9s3(~_6xVNZPcE>WqD`zgARgfk6Hs+E#su*ED>Pe z+%ebC;FRmrAXQOxSAIi(bAihzBP{sIeuz1KM+d8(rRfzgkAw^JX_0C*j zYw?daN$xp|_E(`JO0zOQ1{7ZjyM!vWlBfs;@Gx8i{#&L6M>|1Q0}_5LnY%Z&Euwb65S{573% zax)Xn1UrCBXmkiAPs|UueG&fE(`sg)3|0IR9O@^<qF_y*UqFT zrVtRz8N?3RNgsPFZhCC2hb@cpanFv~qQhO|2Yx=wJ|t%eE66g!HW4a$t8@a&?du8* ztdIZD$_^l$Hrrk&;X>$kaGDkC@WdYH^qgL|V0(NQIq+sSUtuLV_(#^vj@XkaJq~HU zB*XV5dDnZ|8RFriroDVIFP;E{mp+Cd+S=vpHBkGo^Mx;)_ zPxGN0alO;BwziCSgy*jCuKSGOZCCsKQvV={C^JC;6S*4&fHu{5Ek)V&cV3tpSX8Vs zb>F_#8l6k4%Q};0$6+AgUlRRcd1NF@^n6&?ACk1=7m?9)x4#2fBNO^q#Sv+B5YFFh ztnicYoVw@oI|5KrhHb}L+m&_ZtN;8y$S}mo0lr7Sgt7Ogt=FtP+SU^7p+Ij0^U16R2Bh4tS3-Ct)V!5lZIe26 zXn1J46CI&`hkAp~{@3oZ#=D%uL&WSM;m4p`61+05qUD8F08HH?-0VW3TkKJm%7Ppn zDIBB~T2kdZ|5)NZy0}xFp1Y=(indyuVjlmGLXTHnabK|2F6@%eVh+&~b`UyA;|0 zyLA3SF1>u~h=x+UF#7MfCMOQZ z9^Z+I4o#s}elU@(crPlet|;6^=;50eK>)YZP*KyiYSo;KS$8$Zcw6lUA?3EyVF7{m zJ*nbq>gHWf#l}T%h}_JixhzX_ofZ`6bxJPr?yO&E=1_OJrFj-Rz@lSKwe9@tq3cdG z^2{rBA!akm9fSD{_)sfpT7}Jzf^h33og?X@vOY{vt6UDYo!#ramUh0P9e)uQpZuS3 zaa2@Ss`d5rk1gM&+>6#drAoEZzdpD!g;~Gt+sg#pfuP)B%G2XW$e`lWFvxxd2x9c9 zbEVxZWx~0iGD|NYwm)@%m;Gsn<4o|G)UzkI?L06=+FIudBcf6R$rMB}dnUCmU@M^q z3Zp*YRGhhMH`Z}2VP+|=ZXk>G%F$SLB6c}2cS!y2l_uqjzwV0;lHDJUuC^R5O7wr^ zM;)^YO201qSz6$VJPRl=p%@yx;@V1_-AQ)IA%t0`sTd8M5vh0iji{-w!o`L-lf_`@ zlwWw8+sgAo@gaC?u;$N_GbWh`N7M)+Bfe^{`9(W^(l2Dl$=YLET=jSf2XX-*Q- z2N2L~8c_PT$EHi8eC{&+Rrc_3Ntai#f-T0L4}BFApYfuUR}T&hRDn+&jVBn4oZ?R` z+q-5cw67P`Q|;x_4wMW?4+|C?m;k>H1+trq6tO#~qY=;DUOPgiTP0NRd}R|840|G! z`0cPqSy;iOclXyFiYl7=+0$v_N0BF}WBMZ>#(z6smQ)jbq;cMm<~hv(P~lT@Dj z^5q$XxlM>`IU6ns?wW9e0ZeatRmMDBJP4$Fx1Ni3MtYpub)`!8JX2ngtkE5BbHUbx zL6GE_L%!TE85r3lgT?Zgb0bvf?^A7B+m~i-8%%A=lw_wkK01u-EP}HmMR*yy=1h9j zLW86qNU8yVk)eE?qZhrsGVBjZB%Qb}U61QxfjOmauxyOB4JAFiImD74eIqV{UQVkk zYL_6TsT#q0l!d?ig1Vn7dDmoMhuSeHR%Ka}Oq^@r>((o7Bg}J|yran75iRwM^poKb z@jysZzq?AM!wmrfTD{RB?)O{ogcsi7vSu0Rkrm)LF}T`lSUCz97XES*p+}$0Jr0D#7KF*ogh!%=1eDcu89D)RcP5oxm?BHvD3c(@Dl`1@IVd{h9}Mv;o}Vq4;oM4HR=&x3GJ>`}V->`h>IIK;KQp%}b7) zAnZgfY=f;;4Z;Yxsx_0Zj|%>HjfXgZh`21^=8`7bE3U_lQ4xfmg-_`bE_%v{sfWRO zn=#*nRdqt*w(sKN=)2h|1~UuD^qW!a+m-#kO~el~bzVptj5`oL)Po!~t5N(l*oUj+ z%ZQ%!^!rt*uyaZ*AIY^+F-7nlu?CAr+)dZGcn(ycU=-+?rH5R?u8NHuv$$ml>~8KT zjHZC)UR2DRn&Qq@zb`jl7}F01K+5O)QCbCB$o2YYqfcE$Mn_QJiG@qSplA7h&uTmmU!baz%+3XTCB2ZAk}vR(_!?6)x|+H%O@5f>q63lnn^duruJPRkES zrs(4bnp@DXO0CfzXwx8%S#Vcz-x+P)JQ9_KOX&o|iHu{hQ|`k%R5whLyz8y(#31BdrSx%3;(`!sDRKgN@irhZsP%k2Yn4I& zE!)^*{Wn+i@XK0u?LtgQOW9`OU8_-#Rh)sl)Z9iEz;gEEc3S_fNQ2OLhJZbNAy)K!vd&CTq74 zq&Z>7k!snaZ6$%FZ@SyT1ORb^<(_okp4u`?~w*uh8T$s9srWp@Cp}KbxENY-^3K$#(eEtZK>tPALdVoyJ#>`QrH&|zz#?N%P{Cj>7ahAJ$0TH zcB|=nV+b=uRNM9V6JS{u7ttTI!d?wV3hKIixRR+qC=honFm{x3je>@cr(HE+2*nL> zH}<5O{}O6N>Zw6BgoyTpsxo#`xsA(80>}xf==c8hR`r;z*#oPots!_UFo- zc0~$4nRk`jFR;Ohd8fd|*CZ$fz_c}T6TI7IDAD(d-$=pqu81RO3sVbH0w}mIDbDG ztGZbKRm9`Vl6z#c#BhMZ*AdaGH)l6Qwa;TykJ!`WY4~EySBGoiXV0w-?aTWR)GFA} zxD>l%T5%n%*e}W&X?7tNvub)3;WY~@1J@q>w7w>>A(pO~>xyR67MZGjP1v~HrE^g= z<@=7IN?OQXJomYi$LfT|cCdHPx}0nqudY+maTq{BIyiYG{mzWaZnBpQuC@hy%Ws^w zayOND6<-SDGjskEVA>Yx-I*M?&`MjDVDHDL&F{#7Rm88Y!0s5qZ0q^D{aS362i~Yk ztjSB(5f$Gjz5GhmX1^^lVu~-O@b)0Ryv^zV*{1iR2FuQ_-OZGY)Y)b$*o>j<3>$lR zI&lQ(F8@^{5orKxu;{W%x+G>X^i5UM4S5wuZDf&xM=Em@yo@8-ZX>cml4F-FapEk9 z1VT_$JYOUID+$JaJQCd486N~o6%4o0xj8DH4$n#$;2A|r=BO~0#b8yGy_%_raCqe7 z=7=>uOWahF8##MdGBhf>GY2i=(K7L_Yrhd+C zGxujj1V^d5A3et>c14ZfjHPW5srs_kCyZ^@k?uCHYa<@HZ9;WAEqKZ`n?Sg5nlu2& zTanzFTle#iT7jRE!{QWtliY z$e>{bs5Q4y(+FuYmN`;O6@^^_gv`(A@^yJM7{CNIAY7(=?kT>DC_ZrXy$$MVL=5;^ z3h7wB>0&4mwduw*QST>6Ysr509ES?UlxV1deQ&51E%$AF_@TNYuXMxJwmuIfxCYTi z+%k!Sni=(Iejd`*FhYm@dMbW)eq?q849D1yT+LjkX!^A{WTYy#Hi}@Om72@&koubJ zr;eIm-)GyCkNL)r9TAGMr#N#c$;91DZ1HGV-&G>4CE41+LkL@%Gpo_*ia%poqQfY^ z5zYC&Rt0aPUD>?lM%5JGULE2)k^Z?Y{CzX!a%Q87BNw7+R1!+9;B6S0s6eS$;e2%( zhVRGnLP@&qC|B$AGm$4pdo&bSiRFrVUtIT55z91-_d9|2K6@`_UGtM%S@hPgPw1gm__9j7%KUHpOZbi2ez4-r`%!56Y4&jYOyE&4#J zmECa`n(29+&z-wT2I0aiEz36NQ2bTTOFpKM7$8uev;d5{5n()zZIHzQ1-kz#?Tjyy z$7_Jo(lz)c2yuDpc6^Epzge*w%Dqyhl#{`3=-@7uwM*$Q7Z0r#H#k8>7c$Bz<=6h? zOf2a~NWqVR|O7UpZD{DeW{{rU0V%6eUTT_ihlyE90~8dVEhx> zT`j#XDNX&9Ba{uae86VCfUrjlt81e^*e691NkcX{v=)F;<*AF7SFjChOiPDK%X!;d zD;Ylg=4Sd~LA9=%tWzf0%-%_6JAfum|c>W=>$_U^g6%)S-yC5!NGR$ zmalRt_JWS2SDDY;v5Lowl$3lQwDj+c`ngtiMG6fxchm)pg^En#?{tfMIm-!)bhxB$k+}q+b<0)HsZ&kbOX1g`TN8-wf8WQrUaEYPC zP;lT%CU1RV+0jrSP%1N}Y3Sjoq!hOozW>^&L_GgPlb?^FYK;gNh+e-1LkK%td0X0t zt{urGPi{?(M1Oz1%i$qB1$gSf|DBAUVp6B32AUgTr=XubO&l-Jx;n2LqT|H`m>Ka= zyXiX|tBdf!;?%et;tg{wC}Wyl`l&45y4gbp%w?BVFv?=)=08GvB20Z2VRZOKFL(^U*|zinkP6zXht;}DP) zK1e?iWugYqcx)7qF%{QbqTiavuH1yzSxui}-ve#ZEs?+krU8m2LC_901!0Ensh;92 zw5&VH=0?lTW516sCUN;>i8R0;F<5mgs99nJ<)q~8*wACaH3`HGFUEKVu8ESw5pd5Q zZBR?x#l8)5i%^Qdwr*o#=3Hru(B)ktIqxg9v0 zydcUh83*v^Y9^Mod&0Qm;~rXsA@KT}LmMeZdP1!Lha|n_8TfP3u==l@A`x}_-bSSq z>XkGUV4EgmN++anhxAS7_iorber*HQ@hn?=FsgLk`J&ZEkHf1ubZR3@-_2`@FP&a(sXj1zSi1U_)zV zMF1~AiKG>0lU+Z$GPzh2q?(edhz z`&C;YO$-^*dMbcNx21Lg#w+!ks)0@~3aFmIfs{h}RV&}P=rs$Xjc6$`WqIDB>u{1E$%jV6wYW( zYTqCwZS|&18lbFZiI&AeE;n{a)4CNTqJ{;41di5T zz5YF~Cc!bFpKZ;^4+&tUyhox_?K_T8ZFA~GaAoXsu?$ytcd^~Pl$@`V<4UIH8qr3- zCaEBzEq?Y!Nj0Xuwi&FoQ8V>yon#-Syv&cwN;K6lS~OF9+GT59eks=6!Hc+r%Dy7o zm1gH-)FhgEiP{;6?xM^ne6?=huwerv;2rGxslISYg{CEQD=vIz8HAtlwejobaP2zw zBKzfjcF_|%9{dbg^nBv!`?}Xx?SocbiHlB3)h)cg%wEB-*K{!QrL72)54VLr$l0Ni z!w@AS^c9LOt7A)2a6Fw4xuXEmERs$dJ28spcausQs@V{z<>;ZZdA>#ipkShg>8=Cf z$4SyczZ)j9NoONuch<`{yvdN-mP*r)=A{al$LDESW4Og3JCjQC?1U695yWS8_ z%g60d16rM^hO*gK4DO|i75%?-RLrnLP$l7PDE##VsV6Vg9hu8#IKXg}Zgu>mChIJl z%1F=oLKQvn!mZhzjq(-{+pXx)xuZD^{Oe2Ui76nbb|?*cnugHq>c=r*R8=+Nnt0eR z8lJ9MdnFr7{U#-53SP8WOn)t?kCg8Dww1wuMlw*3o}U94wdvh+BnN})`{POXl0OKw6~?j{=6FUnT+pd>y)kAp%XS+aAw<{2wFOos3lTfCA zQW-a13Gnc9F$2W;Ci~UqA(JQTH(x54XQ@W?2#Oq!{Ya|DR2c?i8tOP^+~eq)+HEPj zTm~E(r`QjzRBp(E!&O%6`F>B$eA)8*QjRhtcRjT5ub7recwx0#qe#jy zAZ8mBF-mV8iFjZ_Z)0#JqBSwXvd66|XRl)AqQ0d9EbVH&-%phbkxL_*bYEcUnRUR20tjF$SwC1 z+e#0sIr;VpxPL(|EdcxWCss-)cN+5A4PsH?AiyC5v~`EYA|;XTy?+chX&PqA| z6fE)}mD167A_sBsEd8ieqMOTkgcC2dsE?%+(hgk?1*88WYitL z3&Nww5q$^u3(pu!@8A4fabD|fOMn@Lz!(qQ;Di$a7Z=g;c)+$qbX6IVu^sSb;_ki@ zGbSv+_x8djdpbkv4sDqv(4Tt7t`jXW+And#;QV4jlW^6(I=r4;-mE(HC^U8g>&^!* zsD>f1{wnIW&+hYoHpcOHcBb)&ue;hsGDHr$TY2z}3JV0Ch`}B#n&Strz8s8?`^+>H zzSr_DV3$^{HsP!Cg#Re%hM3#C&ME)O$BZo-lx>>{G-dz5ME}4v0oP~-qf(CZK4OH04wd+vO&!r$r3u98kk-MLYa3Oj*P zS>C5ʕ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�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: -Can omit this tag if description is located at the beginning - -### Membership -`@namespace [[{}] ]`: -an object creates a namespace for its members. - -`@memberof `: -identifies a member symbol that belongs to a parent symbol. -`@memberof! `: - forces JSDoc to document a property of an object that is an instance member of a class. -[Examples](http://usejsdoc.org/tags-memberof.html#examples) - -### Relational -`@implements {typeExpression}`: -a symbol implements an interface. - -`@interface []`: -marks a symbol as an interface that other symbols can implement. - -`@external `: -identifies a class, namespace, or module that is defined outside of the current package. -`@see ` to add a link - -### Entities -`@class [ ]`: -marks a function as being a constructor, meant to be called with the new keyword to return an instance. - -`@function []`: -marks an object as being a function, even though it may not appear to be one to the parser - -`@module [[{}] module:]`: -marks the current file as being its own module. All symbols in the file are assumed to be members of the module unless documented otherwise. - -`@static`: -symbol is contained within a parent and can be accessed without instantiating the parent. - -#### Symbols, Parameters, Variables -`@type {typeName}`: -allows you to provide a type expression identifying the type of value that a symbol may contain, or the type of value returned by a function. -[examples](http://usejsdoc.org/tags-type.html) - -`@typedef [] `: -custom types, particularly if you wish to refer to them repeatedly. - -`@private | @public | @protected`: -* public: JSDoc treats all symbols as public -* proceted: a symbol is only available, or should only be used, within the current module. - -`@inner vs @global`: - -### Beavior -`@param {type} name - description`: -name, type, and description of a function parameter. -[examples](http://usejsdoc.org/tags-param.html) - -`@returns`: - -`@callback`: -callback function that can be passed to other functions. - -`@throws {} free-form description`: -an error that a function might throw. - -### Events -`@event #[event:]`: - -`@listens `: - indicates that a symbol listens for the specified event. - -`@fires #[event:]`: - -`@mixin`: -This provides methods used for event handling - -## Examples - -#### document namespaces - -``` js -/** @namespace */ -hgm = {}; - -/** @namespace */ -hgm.cookie = { - /** describe me */ - get: function (name) { }, - - /** describe me */ - set: function (name, value) { }, - - /** describe me */ - remove: function (name) { } -}; -``` - -#### A namespace with defaults and nested default properties -``` js -/** - * @namespace - * @property {object} defaults - The default values for parties. - * @property {number} defaults.players - The default number of players. - * @property {string} defaults.level - The default level for the party. - * @property {object} defaults.treasure - The default treasure. - * @property {number} defaults.treasure.gold - How much gold the party starts with. - */ -var config = { - defaults: { - players: 1, - level: 'beginner', - treasure: { - gold: 0 - } - } -}; -``` - -#### Documenting large apps, group modules into categories - -> This is to represent and explore functional behavior. - -Example: Account module is composed by: -* api -* modules -* logic -* schema -* helpers - -``` js -/**Parent module -* @module package-name -*/ - -/**Child of the parent module -* @namespace firstChild -* @memberof module:package-name -*/ -``` - -#### ToDO - -- [ ] JSDoc template -- [ ] Markdown plugin -- [ ] Use categories tag: `@categories` -- [ ] Patterns examples -- [ ] More Lisk examples: callback, throws, class, nested objects -- [ ] JSDoc tutorials for best practices -- [ ] Callback patterns - * node style: cb(err, data); - `app.js` - * setImmediate style: return setImmediate(cb, null, data); - `modules/blocks.js` - * next style: cb(); - `app.js` \ No newline at end of file diff --git a/docs/typedef/components.js b/docs/typedef/components.js deleted file mode 100644 index ceffce25..00000000 --- a/docs/typedef/components.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Handles module content - * @typedef {Object} modules - * @property {Accounts} accounts - * @property {Blocks} blocks - * @property {Crypto} crypto - * @property {DApps} dapps - * @property {Delegates} delegates - * @property {Loader} loader - * @property {Multisignatures} multisignatures - * @property {Peers} peers - * @property {Rounds} rounds - * @property {Server} server - * @property {Signatures} signatures - * @property {Sql} sql - * @property {System} system - * @property {Transactions} transactions - * @property {transport} Transport - */ diff --git a/docs/typedef/crypto.js b/docs/typedef/crypto.js deleted file mode 100644 index d97a2b00..00000000 --- a/docs/typedef/crypto.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Public Key obtained from sodium.crypto_sign_seed_keypair(hash) - * @typedef {string} publicKey - */ -/** - * Private Key obtained from sodium.crypto_sign_seed_keypair(hash) - * @typedef {string} privateKey - */ -/** - * Address obtained from publicKey using bignum function with 'U' in the beginning. - * @typedef {string} address - */ -/** - * Crypto hash hex - * @typedef {string} hash - */ -/** - * publicKey, privateKey - * @typedef {Object} keypair - */ diff --git a/docs/typedef/modules/accounts.js b/docs/typedef/modules/accounts.js deleted file mode 100644 index cb9f06c4..00000000 --- a/docs/typedef/modules/accounts.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `accounts` - * - * @module accounts - */ diff --git a/docs/typedef/modules/blocks.js b/docs/typedef/modules/blocks.js deleted file mode 100644 index 9e76b5f0..00000000 --- a/docs/typedef/modules/blocks.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `blocks` - * - * @module blocks - */ diff --git a/docs/typedef/modules/dapps.js b/docs/typedef/modules/dapps.js deleted file mode 100644 index 6e44a3bb..00000000 --- a/docs/typedef/modules/dapps.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `dapps` - * - * @module dapps - */ diff --git a/docs/typedef/modules/delegates.js b/docs/typedef/modules/delegates.js deleted file mode 100644 index 092e0dd6..00000000 --- a/docs/typedef/modules/delegates.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `delegates` - * - * @module delegates - */ diff --git a/docs/typedef/modules/helpers.js b/docs/typedef/modules/helpers.js deleted file mode 100644 index aaf05480..00000000 --- a/docs/typedef/modules/helpers.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Core Module `helpers` - * - * @module helpers - */ diff --git a/docs/typedef/modules/loader.js b/docs/typedef/modules/loader.js deleted file mode 100644 index d8afa350..00000000 --- a/docs/typedef/modules/loader.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `loader` - * - * @module loader - */ diff --git a/docs/typedef/modules/multisignatures.js b/docs/typedef/modules/multisignatures.js deleted file mode 100644 index 2bf920ba..00000000 --- a/docs/typedef/modules/multisignatures.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `multisignatures` - * - * @module multisignatures - */ diff --git a/docs/typedef/modules/peers.js b/docs/typedef/modules/peers.js deleted file mode 100644 index aab2edf5..00000000 --- a/docs/typedef/modules/peers.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `peers` - * - * @module peers - */ diff --git a/docs/typedef/modules/rounds.js b/docs/typedef/modules/rounds.js deleted file mode 100644 index d997442d..00000000 --- a/docs/typedef/modules/rounds.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Core Module `rounds` - * - * @module rounds - */ diff --git a/docs/typedef/modules/server.js b/docs/typedef/modules/server.js deleted file mode 100644 index c84af8c3..00000000 --- a/docs/typedef/modules/server.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `server` - * - * @module server - */ diff --git a/docs/typedef/modules/signatures.js b/docs/typedef/modules/signatures.js deleted file mode 100644 index c0224eee..00000000 --- a/docs/typedef/modules/signatures.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `signatures` - * - * @module signatures - */ diff --git a/docs/typedef/modules/transactions.js b/docs/typedef/modules/transactions.js deleted file mode 100644 index 90d356f8..00000000 --- a/docs/typedef/modules/transactions.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `transactions` - * - * @module transactions - */ diff --git a/docs/typedef/modules/transport.js b/docs/typedef/modules/transport.js deleted file mode 100644 index 543d205b..00000000 --- a/docs/typedef/modules/transport.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Functional Module `transport` - * - * @module transport - */ diff --git a/docs/typedef/namespace.js b/docs/typedef/namespace.js deleted file mode 100644 index 8f28110c..00000000 --- a/docs/typedef/namespace.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * General object to define private functions in different main functions. - * @namespace __private - * @private - */ From 0b08c73a7e4fdf7002d6f46d4f6fad2727406f9f Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 11 Aug 2022 13:01:36 +0600 Subject: [PATCH 29/36] Revert "fix: add knex and remove json-sql from depends" This reverts commit c360227b4b88ef252b2556bf9cb55edb132c3c29. --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 66e424d4..d0b109d3 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "ip": "=1.1.8", "js-nacl": "^1.4.0", "json-schema": "=0.4.0", - "knex": "^2.2.0", + "json-sql": "LiskHQ/json-sql#27e1ed1", "lodash": "=4.17.21", "method-override": "=3.0.0", "npm": "=8.15.1", @@ -98,6 +98,9 @@ "overrides": { "grunt-contrib-obfuscator": { "javascript-obfuscator": "^4.0.0" + }, + "json-sql": { + "underscore": "^1.13.4" } }, "config": { From e3eb3b35aa0b3a38b48a99e7e2291202326cf882 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 11 Aug 2022 13:03:40 +0600 Subject: [PATCH 30/36] Revert "chore: replace json-sql with knex.js" This reverts commit 94972606c2bd158c458f7b5ae9fefd487302bc08. --- logic/account.js | 159 +++++++++++++++++-------------------- modules/accounts.js | 3 +- modules/delegates.js | 38 ++------- modules/multisignatures.js | 8 +- modules/sql.js | 121 ++++++---------------------- 5 files changed, 108 insertions(+), 221 deletions(-) diff --git a/logic/account.js b/logic/account.js index bdbc46e3..3640730b 100644 --- a/logic/account.js +++ b/logic/account.js @@ -3,10 +3,8 @@ var async = require('async'); var pgp = require('pg-promise'); var path = require('path'); -var knex = require('knex')({ - client: 'pg' -}); - +var jsonSql = require('json-sql')(); +jsonSql.setDialect('postgresql'); var constants = require('../helpers/constants.js'); var slots = require('../helpers/slots.js'); @@ -458,14 +456,14 @@ Account.prototype.removeTables = function (cb) { 'mem_accounts2u_delegates', 'mem_accounts2multisignatures', 'mem_accounts2u_multisignatures'].forEach(function (table) { - const sql = knex.delete() - .from(table) - .toString(); - - sqles.push(sql); + sql = jsonSql.build({ + type: 'remove', + table: table + }); + sqles.push(sql.query); }); - this.scope.db.query(sqles.join('; ')).then(function () { + this.scope.db.query(sqles.join('')).then(function () { return setImmediate(cb); }).catch(function (err) { library.logger.error(err.stack); @@ -584,8 +582,6 @@ Account.prototype.getAll = function (filter, fields, cb) { }.bind(this)); var limit, offset, sort; - const rawFilters = filter.raw || []; - delete filter.raw; if (filter.limit > 0) { limit = filter.limit; @@ -603,39 +599,23 @@ Account.prototype.getAll = function (filter, fields, cb) { delete filter.sort; if (typeof filter.address === 'string') { - rawFilters.push( - knex.raw('address = UPPER(?)', filter.address) - ); - delete filter.address; + filter.address = { + $upper: ['address', filter.address] + }; } - const sql = knex(this.table) - .select(realFields) - .from({ a: this.table }) - .modify(function (queryBuilder) { - if (limit) { - queryBuilder.limit(limit); - } - - if (offset) { - queryBuilder.offset(offset); - } - - if (sort) { - queryBuilder.orderBy(Array.isArray(sort) ? sort : [sort]); - } - - if (Object.keys(filter).length >= 1) { - builder.where(filter); - } - - if (rawFilters.length >= 1) { - builder.where(rawFilters.join(' and ')); - } - }) - .toString(); + var sql = jsonSql.build({ + type: 'select', + table: this.table, + limit: limit, + offset: offset, + sort: sort, + alias: 'a', + condition: filter, + fields: realFields + }); - this.scope.db.query(sql).then(function (rows) { + this.scope.db.query(sql.query, sql.values).then(function (rows) { return setImmediate(cb, null, rows); }).catch(function (err) { library.logger.error(err.stack); @@ -658,13 +638,15 @@ Account.prototype.set = function (address, fields, cb) { address = String(address).toUpperCase(); fields.address = address; - const sql = knex(this.table) - .insert(this.toDB(fields)) - .onConflict(['address']) - .merge(this.toDB(fields)) - .toString(); + var sql = jsonSql.build({ + type: 'insertorupdate', + table: this.table, + conflictFields: ['address'], + values: this.toDB(fields), + modifier: this.toDB(fields) + }); - this.scope.db.none(sql).then(function () { + this.scope.db.none(sql.query, sql.values).then(function () { return setImmediate(cb); }).catch(function (err) { library.logger.error(err.stack); @@ -815,16 +797,16 @@ Account.prototype.merge = function (address, diff, cb) { var sqles = []; - const tableName = `${self.table}2${el}`; - if (Object.keys(remove).length) { Object.keys(remove).forEach(function (el) { - const sql = knex(tableName) - .whereIn('dependentId', remove[el]) - .andWhere('accountId', address) - .del() - .toString(); - + var sql = jsonSql.build({ + type: 'remove', + table: self.table + '2' + el, + condition: { + dependentId: { $in: remove[el] }, + accountId: address + } + }); sqles.push(sql); }); } @@ -832,13 +814,14 @@ Account.prototype.merge = function (address, diff, cb) { if (Object.keys(insert).length) { Object.keys(insert).forEach(function (el) { for (var i = 0; i < insert[el].length; i++) { - const sql = knex(tableName) - .insert({ - accountId: address, - dependentId: insert[el][i] - }) - .toString(); - + var sql = jsonSql.build({ + type: 'insert', + table: self.table + '2' + el, + values: { + accountId: address, + dependentId: insert[el][i] + } + }); sqles.push(sql); } }); @@ -847,11 +830,11 @@ Account.prototype.merge = function (address, diff, cb) { if (Object.keys(remove_object).length) { Object.keys(remove_object).forEach(function (el) { remove_object[el].accountId = address; - const sql = knex(tableName) - .where(remove_object[el]) - .del() - .toString(); - + var sql = jsonSql.build({ + type: 'remove', + table: self.table + '2' + el, + condition: remove_object[el] + }); sqles.push(sql); }); } @@ -860,21 +843,25 @@ Account.prototype.merge = function (address, diff, cb) { Object.keys(insert_object).forEach(function (el) { insert_object[el].accountId = address; for (var i = 0; i < insert_object[el].length; i++) { - const sql = knex(tableName) - .insert(insert_object[el]) - .toString(); - + var sql = jsonSql.build({ + type: 'insert', + table: self.table + '2' + el, + values: insert_object[el] + }); sqles.push(sql); } }); } if (Object.keys(update).length) { - const sql = knex(this.table) - .where({ address }) - .update(update) - .toString(); - + var sql = jsonSql.build({ + type: 'update', + table: this.table, + modifier: update, + condition: { + address: address + } + }); sqles.push(sql); } @@ -889,9 +876,9 @@ Account.prototype.merge = function (address, diff, cb) { } } - var queries = sqles.join(';') + round.map(function (sql) { + var queries = sqles.concat(round).map(function (sql) { return pgp.as.format(sql.query, sql.values); - }).join('').concat(round); + }).join(''); if (!cb) { return queries; @@ -916,12 +903,14 @@ Account.prototype.merge = function (address, diff, cb) { * @return {setImmediateCallback} Data with address | Account#remove error. */ Account.prototype.remove = function (address, cb) { - const sql = knex(this.table) - .where({ address }) - .del() - .toString(); - - this.scope.db.none(sql).then(function () { + var sql = jsonSql.build({ + type: 'remove', + table: this.table, + condition: { + address: address + } + }); + this.scope.db.none(sql.query, sql.values).then(function () { return setImmediate(cb, null, address); }).catch(function (err) { library.logger.error(err.stack); diff --git a/modules/accounts.js b/modules/accounts.js index 59715cbe..39d12d1b 100644 --- a/modules/accounts.js +++ b/modules/accounts.js @@ -668,8 +668,7 @@ Accounts.prototype.internal = { top: function (query, cb) { self.getAccounts({ sort: { - column: 'balance', - order: 'desc' + balance: -1 }, offset: query.offset, limit: (query.limit || 100) diff --git a/modules/delegates.js b/modules/delegates.js index c0d0a9c2..3c801579 100644 --- a/modules/delegates.js +++ b/modules/delegates.js @@ -10,9 +10,6 @@ var jobsQueue = require('../helpers/jobsQueue.js'); var crypto = require('crypto'); var Delegate = require('../logic/delegate.js'); var extend = require('extend'); -var knex = require('knex')({ - client: 'pg' -}); var OrderBy = require('../helpers/orderBy.js'); var sandboxHelper = require('../helpers/sandbox.js'); var schema = require('../schema/delegates.js'); @@ -83,13 +80,7 @@ function Delegates (cb, scope) { __private.getKeysSortByVote = function (cb) { modules.accounts.getAccounts({ isDelegate: 1, - sort: [{ - column: 'vote', - order: 'desc' - }, { - column: 'publicKey', - order: 'asc' - }], + sort: { 'vote': -1, 'publicKey': 1 }, limit: slots.delegates }, ['publicKey'], function (err, rows) { if (err) { @@ -110,13 +101,7 @@ __private.getKeysSortByVote = function (cb) { __private.getKeysSortByVotesWeight = function (cb) { modules.accounts.getAccounts({ isDelegate: 1, - sort: [{ - column: 'votesWeight', - order: 'desc' - }, { - column: 'publicKey', - order: 'asc' - }], + sort: { 'votesWeight': -1, 'publicKey': 1 }, limit: slots.delegates }, ['publicKey'], function (err, rows) { if (err) { @@ -440,23 +425,10 @@ Delegates.prototype.getDelegates = function (query, cb) { if (!query) { throw 'Missing query argument'; } - var sortFilter = [{ - column: 'vote', - order: 'desc' - }]; - + var sortFilter = { 'vote': -1, 'publicKey': 1 }; if (modules.blocks.lastBlock.get().height > constants.fairSystemActivateBlock) { - sortFilter = [{ - column: 'votesWeight', - order: 'desc' - }]; + sortFilter = { 'votesWeight': -1, 'publicKey': 1 }; } - - sortFilter.push({ - column: 'publicKey', - order: 'asc' - }); - modules.accounts.getAccounts({ isDelegate: 1, sort: sortFilter @@ -884,7 +856,7 @@ Delegates.prototype.shared = { var addresses = (row.accountIds) ? row.accountIds : []; modules.accounts.getAccounts({ - raw: knex.raw('?? in (??)', ['address', addresses]).toString(), + address: { $in: addresses }, sort: 'balance' }, ['address', 'balance', 'username', 'publicKey'], function (err, rows) { if (err) { diff --git a/modules/multisignatures.js b/modules/multisignatures.js index 20cb59a4..4eb23a64 100644 --- a/modules/multisignatures.js +++ b/modules/multisignatures.js @@ -9,9 +9,7 @@ var sandboxHelper = require('../helpers/sandbox.js'); var schema = require('../schema/multisignatures.js'); var sql = require('../sql/multisignatures.js'); var transactionTypes = require('../helpers/transactionTypes.js'); -var knex = require('knex')({ - client: 'pg' -}); + // Private fields var modules, library, self, __private = {}, shared = {}; @@ -235,7 +233,7 @@ Multisignatures.prototype.shared = { }, getAccounts: function (seriesCb) { modules.accounts.getAccounts({ - raw: knex.raw('?? in (??)', ['address', scope.accountIds]).toString(), + address: { $in: scope.accountIds }, sort: 'balance' }, ['address', 'balance', 'multisignatures', 'multilifetime', 'multimin'], function (err, accounts) { if (err) { @@ -255,7 +253,7 @@ Multisignatures.prototype.shared = { } modules.accounts.getAccounts({ - raw: knex.raw('?? in (??)', ['address', addresses]).toString() + address: { $in: addresses } }, ['address', 'publicKey', 'balance'], function (err, multisigaccounts) { if (err) { return setImmediate(eachSeriesCb, err); diff --git a/modules/sql.js b/modules/sql.js index 8da60a61..e4a3a957 100644 --- a/modules/sql.js +++ b/modules/sql.js @@ -2,10 +2,8 @@ var async = require('async'); var extend = require('extend'); -var knex = require('knex')({ - client: 'pg' -}); - +var jsonSql = require('json-sql')(); +jsonSql.setDialect('postgresql'); var sandboxHelper = require('../helpers/sandbox.js'); // Private fields @@ -120,7 +118,7 @@ __private.pass = function (obj, dappid) { * @implements {jsonSql.build} * @implements {library.db.query} * @implements {async.until} - * @param {string} action - should be `batch` or sql query + * @param {string} action * @param {Object} config * @param {function} cb * @return {setImmediateCallback} cb, err, data @@ -137,11 +135,22 @@ __private.query = function (action, config, cb) { } if (action !== 'batch') { - const sql = action; + __private.pass(config, config.dappid); + + var defaultConfig = { + type: action + }; + + try { + sql = jsonSql.build(extend({}, config, defaultConfig)); + library.logger.trace('sql.query:', sql); + } catch (e) { + return done(e); + } - library.logger.trace('sql.query:', sql); + // console.log(sql.query, sql.values); - library.db.query(sql).then(function (rows) { + library.db.query(sql.query, sql.values).then(function (rows) { return done(null, rows); }).catch(function (err) { library.logger.error(err.stack); @@ -197,9 +206,6 @@ Sql.prototype.createTables = function (dappid, config, cb) { var sqles = []; for (var i = 0; i < config.length; i++) { config[i].table = 'dapp_' + dappid + '_' + config[i].table; - - let sql; - if (config[i].type === 'table') { config[i].type = 'create'; if (config[i].foreignKeys) { @@ -207,31 +213,17 @@ Sql.prototype.createTables = function (dappid, config, cb) { config[i].foreignKeys[n].table = 'dapp_' + dappid + '_' + config[i].foreignKeys[n].table; } } - - try { - sql = knex.schema.createTableIfNotExists('user', function (table) { - for (let j = 0; j < config[i].tableFields.length; j++) { - const field = config[i].tableFields[j]; - - table[field.type](field.name); - } - }).toString(); - } catch (err) { - return setImmediate(cb, 'An error occurred while creating table: ' + err); - } } else if (config[i].type === 'index') { - sql = knex.raw( - `create index if not exists ? ON ?(?)`, - [config[i].name, config[i].table, config[i].indexOn] - ).toString(); + config[i].type = 'index'; } else { return setImmediate(cb, 'Unknown table type: ' + config[i].type); } - sqles.push(sql); + var sql = jsonSql.build(config[i]); + sqles.push(sql.query); } - async.eachSeries(sqles.join('; '), function (command, cb) { + async.eachSeries(sqles, function (command, cb) { library.db.none(command).then(function () { return setImmediate(cb); }).catch(function (err) { @@ -316,35 +308,7 @@ Sql.prototype.onBlockchainReady = function () { */ shared.select = function (req, cb) { var config = extend({}, req.body, { dappid: req.dappid }); - - __private.pass(config, config.dappid); - - const table = config.alias ? - ({ [config.alias]: config.table }) : - config.table; - - const sql = knex(table) - .select(config.fields) - .modify(function (queryBuilder) { - if (config.limit) { - queryBuilder.limit(config.limit); - } - - if (config.offset) { - queryBuilder.offset(config.offset); - } - - if (config.condition) { - queryBuilder.where(config.condition); - } - - if (config.sort) { - queryBuilder.orderBy(config.sort); - } - }) - .toString(); - - __private.query.call(this, sql, config, cb); + __private.query.call(this, 'select', config, cb); }; /** @@ -364,19 +328,7 @@ shared.batch = function (req, cb) { */ shared.insert = function (req, cb) { var config = extend({}, req.body, { dappid: req.dappid }); - - __private.pass(config, config.dappid); - - const sql = knex(config.table) - .insert(config.values) - .modify(function (queryBuilder) { - if (config.condition) { - queryBuilder.where(config.condition); - } - }) - .toString(); - - __private.query.call(this, sql, config, cb); + __private.query.call(this, 'insert', config, cb); }; /** @@ -386,19 +338,7 @@ shared.insert = function (req, cb) { */ shared.update = function (req, cb) { var config = extend({}, req.body, { dappid: req.dappid }); - - __private.pass(config, config.dappid); - - const sql = knex(config.table) - .update(config.modifier) - .modify(function (queryBuilder) { - if (config.condition) { - queryBuilder.where(config.condition); - } - }) - .toString(); - - __private.query.call(this, sql, config, cb); + __private.query.call(this, 'update', config, cb); }; /** @@ -408,18 +348,7 @@ shared.update = function (req, cb) { */ shared.remove = function (req, cb) { var config = extend({}, req.body, { dappid: req.dappid }); - __private.pass(config, config.dappid); - - const sql = knex(config.table) - .modify(function (queryBuilder) { - if (config.condition) { - queryBuilder.where(config.condition); - } - }) - .del() - .toString(); - - __private.query.call(this, sql, config, cb); + __private.query.call(this, 'remove', config, cb); }; // Export From 03d10afd9afc8d7488d7884d3ed8228f2fa9e7af Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 11 Aug 2022 13:42:16 +0600 Subject: [PATCH 31/36] fix: move json-sql to legacy directory --- legacy/json-sql | 1 + package.json | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) create mode 160000 legacy/json-sql diff --git a/legacy/json-sql b/legacy/json-sql new file mode 160000 index 00000000..6aa525c5 --- /dev/null +++ b/legacy/json-sql @@ -0,0 +1 @@ +Subproject commit 6aa525c580f49c9a00e1814721e76764a4c9e0bb diff --git a/package.json b/package.json index d0b109d3..1fa1e249 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "ip": "=1.1.8", "js-nacl": "^1.4.0", "json-schema": "=0.4.0", - "json-sql": "LiskHQ/json-sql#27e1ed1", + "json-sql": "./legacy/json-sql", "lodash": "=4.17.21", "method-override": "=3.0.0", "npm": "=8.15.1", @@ -98,9 +98,6 @@ "overrides": { "grunt-contrib-obfuscator": { "javascript-obfuscator": "^4.0.0" - }, - "json-sql": { - "underscore": "^1.13.4" } }, "config": { From 2177a984f55dabde9f3b203eb2431be4be7bb325 Mon Sep 17 00:00:00 2001 From: martiliones Date: Thu, 11 Aug 2022 13:43:08 +0600 Subject: [PATCH 32/36] chore: update dependencies with minor changes --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1fa1e249..5b282bcb 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,8 @@ "dependencies": { "async": "=3.2.4", "axios": "^0.27.2", - "bignumber.js": "=9.0.2", - "bitcore-mnemonic": "=8.25.34", + "bignumber.js": "=9.1.0", + "bitcore-mnemonic": "=8.25.36", "body-parser": "=1.20.0", "bytebuffer": "=5.0.1", "change-case": "=4.1.2", @@ -59,7 +59,7 @@ "json-sql": "./legacy/json-sql", "lodash": "=4.17.21", "method-override": "=3.0.0", - "npm": "=8.15.1", + "npm": "=8.17.0", "pg-monitor": "=1.4.1", "pg-native": "=3.0.0", "pg-promise": "=10.11.1", From c38eff435f0b618a4f81c2d6aa252fc55f1226c4 Mon Sep 17 00:00:00 2001 From: martiliones Date: Wed, 17 Aug 2022 19:53:05 +0600 Subject: [PATCH 33/36] fix(tests): axios syntax error --- test/node.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/node.js b/test/node.js index e8474825..477ba05b 100644 --- a/test/node.js +++ b/test/node.js @@ -578,7 +578,8 @@ node.addPeers = function (numOfPeers, ip, cb) { os = operatingSystems[node.randomizeSelection(operatingSystems.length)]; version = node.version; - node.axios.get({ + node.axios({ + method: 'get', url: node.baseUrl + '/peer/height', headers: { broadhash: node.config.nethash, From 7cc3e0c8cf4f4ea7cf80a36bfa58a1667ab94aeb Mon Sep 17 00:00:00 2001 From: martiliones Date: Wed, 17 Aug 2022 20:52:48 +0600 Subject: [PATCH 34/36] fix: add json-sql to legacy directory --- legacy/json-sql | 1 - legacy/json-sql/.gitignore | 1 + legacy/json-sql/.jshintrc | 19 + legacy/json-sql/.npmignore | 2 + legacy/json-sql/LICENSE | 22 + legacy/json-sql/README.md | 145 ++ legacy/json-sql/docs/README.md | 1180 ++++++++++++++++ legacy/json-sql/gulpfile.js | 27 + legacy/json-sql/lib/builder.js | 233 ++++ legacy/json-sql/lib/dialects/base/blocks.js | 441 ++++++ .../json-sql/lib/dialects/base/conditions.js | 122 ++ legacy/json-sql/lib/dialects/base/index.js | 58 + .../lib/dialects/base/logicalOperators.js | 37 + .../json-sql/lib/dialects/base/modifiers.js | 37 + legacy/json-sql/lib/dialects/base/scheme.js | 146 ++ .../json-sql/lib/dialects/base/templates.js | 220 +++ legacy/json-sql/lib/dialects/mssql/index.js | 13 + .../lib/dialects/postgresql/blocks.js | 33 + .../lib/dialects/postgresql/conditions.js | 87 ++ .../json-sql/lib/dialects/postgresql/index.js | 39 + .../lib/dialects/postgresql/templates.js | 269 ++++ legacy/json-sql/lib/dialects/sqlite/blocks.js | 23 + legacy/json-sql/lib/dialects/sqlite/index.js | 17 + .../json-sql/lib/dialects/sqlite/templates.js | 236 ++++ legacy/json-sql/lib/index.js | 8 + legacy/json-sql/lib/valuesStore.js | 31 + legacy/json-sql/package.json | 44 + legacy/json-sql/tests/0_base.js | 304 ++++ legacy/json-sql/tests/1_select.js | 1222 +++++++++++++++++ legacy/json-sql/tests/2_insert.js | 73 + legacy/json-sql/tests/3_update.js | 123 ++ legacy/json-sql/tests/4_delete.js | 58 + legacy/json-sql/tests/5_union.js | 256 ++++ legacy/json-sql/tests/6_postgresDialect.js | 140 ++ legacy/json-sql/tests/7_create.js | 279 ++++ legacy/json-sql/tests/8_index.js | 45 + 36 files changed, 5990 insertions(+), 1 deletion(-) delete mode 160000 legacy/json-sql create mode 100644 legacy/json-sql/.gitignore create mode 100644 legacy/json-sql/.jshintrc create mode 100644 legacy/json-sql/.npmignore create mode 100644 legacy/json-sql/LICENSE create mode 100644 legacy/json-sql/README.md create mode 100644 legacy/json-sql/docs/README.md create mode 100644 legacy/json-sql/gulpfile.js create mode 100644 legacy/json-sql/lib/builder.js create mode 100644 legacy/json-sql/lib/dialects/base/blocks.js create mode 100644 legacy/json-sql/lib/dialects/base/conditions.js create mode 100644 legacy/json-sql/lib/dialects/base/index.js create mode 100644 legacy/json-sql/lib/dialects/base/logicalOperators.js create mode 100644 legacy/json-sql/lib/dialects/base/modifiers.js create mode 100644 legacy/json-sql/lib/dialects/base/scheme.js create mode 100644 legacy/json-sql/lib/dialects/base/templates.js create mode 100644 legacy/json-sql/lib/dialects/mssql/index.js create mode 100644 legacy/json-sql/lib/dialects/postgresql/blocks.js create mode 100644 legacy/json-sql/lib/dialects/postgresql/conditions.js create mode 100644 legacy/json-sql/lib/dialects/postgresql/index.js create mode 100644 legacy/json-sql/lib/dialects/postgresql/templates.js create mode 100644 legacy/json-sql/lib/dialects/sqlite/blocks.js create mode 100644 legacy/json-sql/lib/dialects/sqlite/index.js create mode 100644 legacy/json-sql/lib/dialects/sqlite/templates.js create mode 100644 legacy/json-sql/lib/index.js create mode 100644 legacy/json-sql/lib/valuesStore.js create mode 100644 legacy/json-sql/package.json create mode 100644 legacy/json-sql/tests/0_base.js create mode 100644 legacy/json-sql/tests/1_select.js create mode 100644 legacy/json-sql/tests/2_insert.js create mode 100644 legacy/json-sql/tests/3_update.js create mode 100644 legacy/json-sql/tests/4_delete.js create mode 100644 legacy/json-sql/tests/5_union.js create mode 100644 legacy/json-sql/tests/6_postgresDialect.js create mode 100644 legacy/json-sql/tests/7_create.js create mode 100644 legacy/json-sql/tests/8_index.js diff --git a/legacy/json-sql b/legacy/json-sql deleted file mode 160000 index 6aa525c5..00000000 --- a/legacy/json-sql +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6aa525c580f49c9a00e1814721e76764a4c9e0bb diff --git a/legacy/json-sql/.gitignore b/legacy/json-sql/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/legacy/json-sql/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/legacy/json-sql/.jshintrc b/legacy/json-sql/.jshintrc new file mode 100644 index 00000000..fe7c5b09 --- /dev/null +++ b/legacy/json-sql/.jshintrc @@ -0,0 +1,19 @@ +{ + "node": true, + "freeze": true, + "maxlen": 100, + "smarttabs": true, + "indent": 1, + "quotmark": "single", + "strict": true, + "globalstrict": true, + // use es3 to get 'extra comma' at object literal error + "es3": true, + "undef": true, + "unused": true, + "immed": true, + "eqeqeq": true, + "globals": { + "JSON": false + } +} diff --git a/legacy/json-sql/.npmignore b/legacy/json-sql/.npmignore new file mode 100644 index 00000000..3796175f --- /dev/null +++ b/legacy/json-sql/.npmignore @@ -0,0 +1,2 @@ +.npmignore +test \ No newline at end of file diff --git a/legacy/json-sql/LICENSE b/legacy/json-sql/LICENSE new file mode 100644 index 00000000..2ac98523 --- /dev/null +++ b/legacy/json-sql/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2013-2014 Oleg Korobenko + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/legacy/json-sql/README.md b/legacy/json-sql/README.md new file mode 100644 index 00000000..7891eaed --- /dev/null +++ b/legacy/json-sql/README.md @@ -0,0 +1,145 @@ +# JSON-SQL + +Library for mapping mongo-style query objects to SQL queries. + +## Quick Start + +Install it with NPM or add it to your package.json: + +``` bash +$ npm install json-sql +``` + +Then: + +``` js +var jsonSql = require('json-sql')(); + +var sql = jsonSql.build({ + type: 'select', + table: 'users', + fields: ['name', 'age'], + condition: {name: 'Max', id: 6} +}); + +sql.query +// sql string: +// select name, age from users where name = $p1 && id = 6; + +sql.values +// hash of values: +// { p1: 'Max' } +``` + +## Documentation + +Documentation is available at the [./docs directory](./docs). + +## Examples + +__Select with join:__ + +``` js +var sql = jsonSql.build({ + type: 'select', + table: 'users', + join: { + documents: { + on: {'user.id': 'documents.userId'} + } + } +}); + +sql.query +// select * from users join documents on user.id = documents.userId; + +sql.values +// {} +``` + +__Insert:__ + +``` js +var sql = jsonSql.build({ + type: 'insert', + table: 'users', + values: { + name: 'John', + lastname: 'Snow', + age: 24, + gender: 'male' + } +}); + +sql.query +// insert into users (name, lastname, age, gender) values ($p1, $p2, 24, $p3); + +sql.values +// { p1: 'John', p2: 'Snow', p3: 'male' } +``` + +__Update:__ + +``` js +var sql = jsonSql.build({ + type: 'update', + table: 'users', + condition: { + id: 5 + }, + modifier: { + role: 'admin' + age: 33 + } +}); + +sql.query +// update users set role = $p1, age = 33 where id = 5; + +sql.values +// { p1: 'admin' } +``` + +__Remove:__ + +``` js +var sql = jsonSql.build({ + type: 'remove', + table: 'users', + condition: { + id: 5 + } +}); + +sql.query +// delete from users where id = 5; + +sql.values +// {} +``` + +For more examples, take a look at the [./docs directory](./docs) or [./tests directory](./tests). + +## Tests + +Clone repository from github, `cd` into cloned dir and install dev dependencies: + +``` bash +$ npm install +``` + +Then run tests with command: + +``` bash +$ gulp test +``` + +Or run tests coverage with command: + +``` bash +$ gulp coverage +``` + +## License + +[MIT](./LICENSE) diff --git a/legacy/json-sql/docs/README.md b/legacy/json-sql/docs/README.md new file mode 100644 index 00000000..8752f703 --- /dev/null +++ b/legacy/json-sql/docs/README.md @@ -0,0 +1,1180 @@ +# Documentation + +## Table of contents + +* [API](#api) + - [Initialization](#initialization) + - __[build(query)](#buildquery)__ + - [configure(options)](#configureoptions) + - [setDialect(name)](#setdialectname) +* __[Queries](#queries)__ + - [type: 'create'](#type-create) + - [type: 'select'](#type-select) + - [type: 'insert'](#type-insert) + - [type: 'update'](#type-update) + - [type: 'remove'](#type-remove) + - [type: 'union' | 'intersect' | 'except'](#type-union--intersect--except) +* __[Blocks](#blocks)__ +* __[Condition operators](#condition-operators)__ + +--- + +## API + +### Initialization + +To create new instance of json-sql builder you can use factory function: + +``` js +var jsonSql = require('json-sql')(options); +``` + +or create instance by class constructor: + +``` js +var jsonSql = new (require('json-sql').Builder)(options); +``` + +`options` are similar to [configure method options](#available-options). + +--- + +### build(query) + +Create sql query from mongo-style query object. + +`query` is a json object that has required property `type` and a set of query-specific properties. `type` property determines the type of query. List of available values of `type` property you can see at [Queries section](#queries). + +Returns object with properties: + +| Property | Description | +| -------- | ----------- | +| `query` | SQL query string | +| `value` | Array or object with values.
Exists only if `separatedValues = true`. | +| `prefixValues()` | Method to get values with `valuesPrefix`.
Exists only if `separatedValues = true`. | +| `getValuesArray()` | Method to get values as array.
Exists only if `separatedValues = true`. | +| `getValuesObject()` | Method to get values as object.
Exists only if `separatedValues = true`. | + +--- + +### configure(options) + +Set options of json-sql builder instance. + +#### Available options + +| Option name | Default value | Description | +| ----------- | ------------- | ----------- | +| `separatedValues` | `true` | If `true` - create placeholder for each string value and put it value to result `values`.
If `false` - put string values into sql query without placeholder (potential threat of sql injection). | +| `namedValues` | `true` | If `true` - create hash of values with placeholders p1, p2, ...
If `false` - put all values into array.
Option is used if `separatedValues = true`. | +| `valuesPrefix` | `'$'` | Prefix for values placeholders
Option is used if `namedValues = true`. | +| `dialect` | `'base'` | Active dialect. See setDialect for dialects list. | +| `wrappedIdentifiers` | `true` | If `true` - wrap all identifiers with dialect wrapper (name -> "name"). | + +--- + +### setDialect(name) + +Set active dialect, name can has value `'base'`, `'mssql'`, `'mysql'`, `'postrgresql'` or `'sqlite'`. + +--- + +## Queries + +### type: 'create' + +>[ [tableFields](#tableFields) ]
+>[ [table](#table) ]
+>[ [foreignKeys](#foreignKeys) ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true, + unique: true, + default: "empty" + } + ] +}); + +sql.query +// create table if not exists "users"("name" varchar(16) NOT NULL default "empty" UNIQUE); +``` + +--- + +### type: 'select' + +>[ [with](#with-withrecursive) | [withRecursive](#with-withrecursive) ]
+>[ [distinct](#distinct) ]
+>[ [fields](#fields) ]
+>[table](#table) | [query](#query) | [select](#select) | [expression](#expression)
+>[ [alias](#alias) ]
+>[ [join](#join) ]
+>[ [condition](#condition) ]
+>[ [group](#group) ]
+>[ [sort](#sort) ]
+>[ [limit](#limit) ]
+>[ [offset](#offset) ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'select', + fields: ['a', 'b'] + table: 'table' +}); + +sql.query +// select "a", "b" from "table"; +``` + +If `fields` is not specified in query, result fields is `*` (all columns of the selected rows). + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'select', + table: 'table' +}); + +sql.query +// select * from "table"; +``` + +--- + +### type: 'insert' + +>[ [with](#with-withrecursive) | [withRecursive](#with-withrecursive) ]
+>[ [or](#or) ]
+>[table](#table)
+>[values](#values)
+>[ [condition](#condition) ]
+>[ [returning](#returning) ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'insert', + table: 'table', + values: {a: 4} +}); + +sql.query +// insert into "table" ("a") values (4); +``` + +--- + +### type: 'update' + +>[ [with](#with-withrecursive) | [withRecursive](#with-withrecursive) ]
+>[ [or](#or) ]
+>[table](#table)
+>[modifier](#modifier)
+>[ [condition](#condition) ]
+>[ [returning](#returning) ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'update', + table: 'table', + modifier: {a: 5} +}); + +sql.query +// update "table" set a = 5; +``` + +--- + +### type: 'remove' + +>[ [with](#with-withrecursive) | [withRecursive](#with-withrecursive) ]
+>[table](#table)
+>[ [condition](#condition) ]
+>[ [returning](#returning) ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'remove', + table: 'table' +}); + +sql.query +// delete from "table"; +``` + +--- + +### type: 'union' | 'intersect' | 'except' + +>[ [all](#all) ]
+>[ [with](#with-withrecursive) | [withRecursive](#with-withrecursive) ]
+>[queries](#queries)
+>[ [sort](#sort) ]
+>[ [limit](#limit) ]
+>[ [offset](#offset) ] + +__`type: 'union'` example:__ + +``` js +var sql = jsonSql.build({ + type: 'union', + queries: [ + {type: 'select', table: 'table1'}, + {type: 'select', table: 'table2'} + ] +}); + +sql.query +// (select * from "table1") union (select * from "table2"); +``` + +__`type: 'intersect'` example:__ + +``` js +var sql = jsonSql.build({ + type: 'intersect', + queries: [ + {type: 'select', table: 'table1'}, + {type: 'select', table: 'table2'} + ] +}); + +sql.query +// (select * from "table1") intersect (select * from "table2"); +``` + +__`type: 'except'` example:__ + +``` js +var sql = jsonSql.build({ + type: 'except', + queries: [ + {type: 'select', table: 'table1'}, + {type: 'select', table: 'table2'} + ] +}); + +sql.query +// (select * from "table1") except (select * from "table2"); +``` + +--- + +## Blocks + +Blocks are small chunks of query. + +### with, withRecursive + +Should be an `array` or an `object`. + +If value is an `array`, each item of array should be an `object` and should conform the scheme: + +>name
+>[ [fields](#fields) ]
+>[query](#query) | [select](#select) | [expression](#expression) + +__Example:__ + +``` js +var sql = jsonSql.build({ + 'with': [{ + name: 'table', + select: {table: 'withTable'} + }], + table: 'table' +}); + +sql.query +// with "table" as (select * from "withTable") select * from "table"; +``` + +If value is an `object`, keys of object interpret as names and each value should be an `object` and should conform the scheme: + +>[ name ]
+>[ [fields](#fields) ]
+>[query](#query) | [select](#select) | [expression](#expression) + +__Example:__ + +``` js +var sql = jsonSql.build({ + 'with': { + table: { + select: {table: 'withTable'} + } + }, + table: 'table' +}); + +sql.query +// with "table" as (select * from "withTable") select * from "table"; +``` + +--- + +### distinct + +Should be a `boolean`: + +``` +distinct: true +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + distinct: true, + table: 'table' +}); + +sql.query +// select distinct * from "table"; +``` + +--- + +### tableFields + +Should be an `array` + +Contains array of table`s fields + +--- + +### foreignKeys + +Should be an `array` + +__Example:__ + +``` js +var result = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true, + primary_key: true + }, + { + name: "age", + type: "Number", + not_null: true + } + ], + foreignKeys: [ + { + field: "name", + table: "person", + table_field: "id" + } + ] +}); + +sql.query +// create table "users"(name varchar(16) NOT NULL PRIMARY KEY,age int NOT NULL, FOREIGN KEY (name) REFERENCES person(id)); +``` + +--- + +### fields + +Should be an `array` or an `object`. + +If value is an `array`, each item interprets as [term block](#term). + +__Example:__ + +``` js +var sql = jsonSql.build({ + fields: [ + 'a', + {b: 'c'}, + {table: 'd', name: 'e', alias: 'f'}, + ['g'] + ], + table: 'table' +}); + +sql.query +// select "a", "b" as "c", "d"."e" as "f", "g" from "table"; +``` + +If value is an `object`, keys of object interpret as field names and each value should be an `object` and should conform the scheme: + +>[ name ]
+>[ [table](#table) ]
+>[ cast ]
+>[ [alias](#alias) ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + fields: { + a: 'b', + d: {table: 'c', alias: 'e'} + }, + table: 'table' +}); + +sql.query +// select "a" as "b", "c"."d" as "e" from "table"; +``` + +--- + +### term + +Should be: +* a `string` - interprets as field name; +* another simple type or an `array` - interprets as value; +* an `object` - should conform the scheme: + +>[query](#query) | [select](#select) | [field](#field) | [value](#value) | [func](#func) | [expression](#expression)
+>[ cast ]
+>[ [alias](#alias) ] + +--- + +### field + +Should be a `string` or an `object`. + +If value is a `string`: + +``` +field: 'fieldName' +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + fields: [{field: 'a'}], + table: 'table' +}); + +sql.query +// select "a" from "table"; +``` + +If value is an `object` it should conform the scheme: + +>name
+>[ [table](#table) ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + fields: [{field: {name: 'a', table: 'table'}}], + table: 'table' +}); + +sql.query +// select "table"."a" from "table"; +``` + +--- + +### value + +Can have any type. + +__Example:__ + +``` js +var sql = jsonSql.build({ + fields: [ + {value: 5}, + {value: 'test'} + ], + table: 'table' +}); + +sql.query +// select 5, $p1 from "table"; + +sql.values +// {p1: 'test'} +``` + +--- + +### table + +Should be a `string`: + +``` +table: 'tableName' +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table' +}); + +sql.query +// select * from "table"; +``` + +--- + +### query + +Should be an `object`. Value interprets as sub-query and process recursively with [build(query)](#buildquery) method. + +__Example:__ + +``` js +var sql = jsonSql.build({ + query: {type: 'select', table: 'table'} +}); + +sql.query +// select * from (select * from "table"); +``` + +--- + +### select + +Should be an `object`. Value interprets as sub-select and process recursively with [build(query)](#buildquery) method. + +__Example:__ + +``` js +var sql = jsonSql.build({ + select: {table: 'table'} +}); + +sql.query +// select * from (select * from "table"); +``` + +--- + +### func + +Should be a `string` or an `object`. + +If value is a `string`: + +``` +func: 'random' +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + fields: [{func: 'random'}], + table: 'table' +}); + +sql.query +// select random() from "table"; +``` + +If value is an `object` it should conform the scheme: + +>name
+>[ args ] + +where `name` is a `string` name of function, `args` is an `array` that contains it arguments. + +__Example:__ + +``` js +var sql = jsonSql.build({ + fields: [{ + func: { + name: 'sum', + args: [{field: 'a'}] + } + }], + table: 'table' +}); + +sql.query +// select sum("a") from table; +``` + +--- + +### expression + +Should be a `string` or an `object`. + +If value is a `string`: + +``` +expression: 'random()' +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + expression: 'generate_series(2, 4)' +}); + +sql.query +// select * from generate_series(2, 4); +``` + +If value is an `object` it should conform the scheme: + +>pattern
+>[ values ] + +where `pattern` is a `string` pattern with placeholders `{placeholderName}`, `values` is a hash that contains values for each `placeholderName`. + +__Example:__ + +``` js +var sql = jsonSql.build({ + expression: { + pattern: 'generate_series({start}, {stop})', + values: {start: 2, stop: 4} + } +}); + +sql.query +// select * from generate_series(2, 4); +``` + +--- + +### alias + +Should be a `string` or an `object`. + +If value is a `string`: + +``` +alias: 'aliasName' +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + alias: 'alias' +}); + +sql.query +// select * from "table" as "alias"; +``` + +If value is an `object` it should conform the scheme: + +>name
+>[ columns ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + alias: {name: 'alias'} +}); + +sql.query +// select * from "table" as "alias"; +``` + +--- + +### join + +Should be an `array` or an `object`. + +If value is an `array`, each item of array should be an `object` and should conform the scheme: + +>[ type ]
+>[table](#table) | [query](#query) | [select](#select) | [expression](#expression)
+>[ [alias](#alias) ]
+>[ on ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + join: [{ + type: 'right', + table: 'joinTable', + on: {'table.a': 'joinTable.b'} + }] +}); + +sql.query +// select * from "table" right join "joinTable" on "table"."a" = "joinTable"."b"; +``` + +If value is an `object`, keys of object interpret as table names and each value should be an `object` and should conform the scheme: + +>[ type ]
+>[ [table](#table) | [query](#query) | [select](#select) | [expression](#expression) ]
+>[ [alias](#alias) ]
+>[ on ] + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + join: { + joinTable: { + type: 'inner', + on: {'table.a': 'joinTable.b'} + } + }] +}); + +sql.query +// select * from "table" inner join "joinTable" on "table"."a" = "joinTable"."b"; +``` + +__Join with sub-select example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + join: [{ + select: {table: 'joinTable'}, + alias: 'joinTable', + on: {'table.a': 'joinTable.b'} + }] +}); + +sql.query +// select * from "table" join (select * from "joinTable") as "joinTable" on "table"."a" = "joinTable"."b"; +``` + +--- + +### condition + +Should be an `array` or an `object`. + +__`array` example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + condition: [ + {a: {$gt: 1}}, + {b: {$lt: 10}} + ] +}); + +sql.query +// select * from "table" where "a" > 1 and "b" < 10; +``` + +__`object` example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + condition: { + a: {$gt: 1}, + b: {$lt: 10} + } +}); + +sql.query +// select * from "table" where "a" > 1 and "b" < 10; +``` + +--- + +### group + +Should be a `string` or an `array`. + +If value is a `string`: + +``` +group: 'fieldName' +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + group: 'a' +}); + +sql.query +// select * from "table" group by "a"; +``` + +If value is an `array`: + +``` +group: ['fieldName1', 'fieldName2'] +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + group: ['a', 'b'] +}); + +sql.query +// select * from "table" group by "a", "b"; +``` + +--- + +### sort + +Should be a `string`, an `array` or an `object`. + +If value is a `string`: + +``` +sort: 'fieldName' +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + sort: 'a' +}); + +sql.query +// select * from "table" order by "a"; +``` + +If value is an `array`: + +``` +sort: ['fieldName1', 'fieldName2'] +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + sort: ['a', 'b'] +}); + +sql.query +// select * from "table" order by "a", "b"; +``` + +If value is an `object`: + +``` +sort: { + fieldName1: 1, + fieldName2: -1 +} +``` + +__Example__: + +``` js +var sql = jsonSql.build({ + table: 'table', + sort: {a: 1, b: -1} +}); + +sql.query +// select * from "table" order by "a" asc, "b" desc; +``` + +--- + +### limit + +Should be a `number`. + +``` +limit: limitValue +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + limit: 5 +}); + +sql.query +// select * from "table" limit 5; +``` + +--- + +### offset + +Should be a `number`. + +``` +offset: offsetValue +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + table: 'table', + offset: 5 +}); + +sql.query +// select * from "table" offset 5; +``` + +--- + +### or + +Should be a `string`. + +Available values: 'rollback', 'abort', 'replace', 'fail', 'ignore'. + +``` +or: 'orValue' +``` + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'insert', + or: 'replace', + table: 'table', + values: {a: 5} +}); + +sql.query +// insert or replace into "table" ("a") values (5); +``` + +--- + +### values + +Should be an `array` or an `object`. + +If value is an `array`, each item should be an `object` and interprets as single inserted row where keys are field names and corresponding values are field values. + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'insert', + table: 'table', + values: [ + {a: 5, b: 'text1'}, + {a: 6, b: 'text2'} + ] +}); + +sql.query +// insert into "table" ("a", "b") values (5, $p1), (6, $p2); + +sql.values +// {p1: 'text1', p2: 'text2'} +``` + +If value is an `object`, it interprets as single inserted row where keys are field names and corresponding values are field values. + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'insert', + table: 'table', + values: {a: 5, b: 'text'} +}); + +sql.query +// insert into "table" ("a", "b") values (5, $p1); + +sql.values +// {p1: 'text'} +``` + +Also you can specify fields array. If there no key in value object it value is `null`. + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'insert', + table: 'table', + fields: ['a', 'b', 'c'], + values: {c: 'text', b: 5} +}); + +sql.query +// insert into "table" ("a", "b", "c") values (null, 5, $p1); + +sql.values +// {p1: 'text'} +``` + +--- + +### modifier + +Should be an `object`. + +You can specify modifier operator. +Available operators: `$set`, `$inc`, `$dec`, `$mul`, `$div`, `$default`. + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'update', + table: 'table', + modifier: { + $set: {a: 5}, + $default: {b: true}, + $inc: {c: 10} + } +}); + +sql.query +// update "table" set "a" = 5, "b" = default, "c" = "c" + 10; +``` + +If modifier operator is not specified it uses default operator `$set`. + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'update', + table: 'table', + modifier: {a: 5} +}); + +sql.query +// update "table" set "a" = 5; +``` + +--- + +### returning + +Format is similar to [fields](#fields) block. + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'insert', + table: 'table', + values: {a: 5}, + returning: ['a'] +}); + +sql.query +// insert into "table" ("a") values (5) returning "a"; +``` + +--- + +### all + +Should be a `boolean`. + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'union', + all: true, + queries: [ + {type: 'select', table: 'table1'}, + {type: 'select', table: 'table2'} + ] +}); + +sql.query +// (select * from "table1") union all (select * from "table2"); +``` + +--- + +### queries + +Should be an `array` with minimum 2 items. Each item interprets as sub-query and process recursively with [build(query)](#buildquery) method. + +__Example:__ + +``` js +var sql = jsonSql.build({ + type: 'union', + queries: [ + {type: 'select', table: 'table1'}, + {type: 'select', table: 'table2'} + ] +}); + +sql.query +// (select * from "table1") union (select * from "table2"); + +//or for sqlite3 +jsonSql.setDialect("sqlite"); +var sql = jsonSql.build({ + type: 'union', + unionqueries: [ + {type: 'select', table: 'table1'}, + {type: 'select', table: 'table2'} + ] +}); + +sql.query +// select * from "table1" union select * from "table2"; +``` + +--- + +## Condition operators + +TODO: write this section \ No newline at end of file diff --git a/legacy/json-sql/gulpfile.js b/legacy/json-sql/gulpfile.js new file mode 100644 index 00000000..c7bbf59b --- /dev/null +++ b/legacy/json-sql/gulpfile.js @@ -0,0 +1,27 @@ +'use strict'; + +var gulp = require('gulp'); +var mocha = require('gulp-mocha'); +var jshint = require('gulp-jshint'); + +gulp.task('test', function() { + return gulp.src(['./tests/*.js'], {read: false}) + .pipe( + mocha({ + reporter: 'spec', + bail: true + }) + ); +}); + +gulp.task('lint', function() { + return gulp.src('./lib/**/*.js') + .pipe(jshint()) + .pipe(jshint.reporter('unix')); +}); + +gulp.task('lintTests', function() { + return gulp.src('./tests/*.js') + .pipe(jshint()) + .pipe(jshint.reporter('unix')); +}); \ No newline at end of file diff --git a/legacy/json-sql/lib/builder.js b/legacy/json-sql/lib/builder.js new file mode 100644 index 00000000..fa0d2a0d --- /dev/null +++ b/legacy/json-sql/lib/builder.js @@ -0,0 +1,233 @@ +'use strict'; + +var _ = require('underscore'); + +var dialects = { + base: require('./dialects/base'), + mssql: require('./dialects/mssql'), + postgresql: require('./dialects/postgresql'), + sqlite: require('./dialects/sqlite') +}; + +var blockRegExp = /\{([a-z0-9]+)\}(.|$)/ig; + +var Builder = module.exports = function (options) { + this.configure(options); +}; + +Builder.prototype._reset = function () { + if (this.options.separatedValues) { + this._placeholderId = 1; + this._values = this.options.namedValues ? {} : []; + } else { + delete this._placeholderId; + delete this._values; + } + + this._query = ''; +}; + +Builder.prototype._getPlaceholder = function () { + return (this.options.namedValues ? 'p' : '') + (this._placeholderId++); +}; + +Builder.prototype._wrapPlaceholder = function (name) { + return this.options.valuesPrefix + '{' + name + '}'; +}; + +Builder.prototype._pushValue = function (value) { + var valueType = typeof value; + + if (valueType === 'undefined') { + return 'null'; + } else if (value === null || valueType === 'number' || + valueType === 'boolean') { + return String(value); + } else if (valueType === 'string') { + if (this.options.separatedValues) { + var placeholder = this._getPlaceholder(); + if (this.options.namedValues) { + this._values[placeholder] = value; + } else { + this._values.push(value); + } + return this._wrapPlaceholder(placeholder); + } else { + return '\'' + value + '\''; + } + } else if (valueType == 'object') { + if (Object.prototype.toString.call(value) == "[object Array]") { + if (value.length > 0) { + return value.join(",") + } else { + return 'null'; + } + } + else if (Buffer.isBuffer(value)) { + var placeholder = this._getPlaceholder(); + this._values[placeholder] = value; + return this._wrapPlaceholder(placeholder); + //return "X'" + value.toString('hex') + "'"; + } else { + var placeholder = this._getPlaceholder(); + this._values[placeholder] = value; + return this._wrapPlaceholder(placeholder); + //return ("'" + JSON.stringify(value).replace(/'/g, "''") + "'"); + } + } else { + throw new Error('Wrong value type "' + valueType + '"'); + } +}; + +Builder.prototype.configure = function (options) { + options = options || {}; + + this.options = _(options).defaults({ + separatedValues: true, + namedValues: true, + valuesPrefix: '$', + dialect: 'base', + wrappedIdentifiers: true + }); + + this.setDialect(this.options.dialect); + + this._reset(); +}; + +Builder.prototype.buildCondition = function (params) { + var self = this; + var result = ''; + + var condition = params.condition; + var logicalOperator = params.logicalOperator || + this.dialect.config.defaultLogicalOperator; + + if (_.isObject(condition) && !_.isEmpty(condition)) { + // if condition is array: [{a: 1}, {b: 2}] + if (_.isArray(condition)) { + result = _(condition).map(function (item) { + return self.buildCondition({ + condition: item, + operator: params.operator + }); + }); + + // if condition is object + } else { + result = _(condition).map(function (value, field) { + // if field name is operator + if (field[0] === '$') { + // if field name is logical operator: {$logicalOperator: {a: 1}} + if (!self.dialect.logicalOperators.has(field)) { + throw new Error('Unknown logical operator "' + field + '"'); + } + + return self.buildCondition({ + condition: value, + logicalOperator: field, + operator: params.operator + }); + + // if condition item is object: {a: {$operator: 4}} + } else if (_.isObject(value)) { + var logicalOperatorFn = self.dialect.logicalOperators + .get(self.dialect.config.defaultLogicalOperator); + + return logicalOperatorFn(_(value).map(function (operatorValue, operator) { + var operatorFn = self.dialect.conditions.get(operator); + if (!operatorFn) { + throw new Error('Unknown operator "' + operator + '"'); + } + return operatorFn(field, operator, operatorValue); + })); + + // if condition item type is simple: {a: 1, b: 2} + } else { + var operatorFn = self.dialect.conditions.get(params.operator); + if (!operatorFn) { + throw new Error('Unknown operator "' + params.operator + '"'); + } + return operatorFn(field, params.operator, value); + } + }); + } + + result = this.dialect.logicalOperators + .get(logicalOperator)(_(result).compact()); + } + + return result; +}; + +Builder.prototype.buildBlock = function (block, params) { + var blockFn = this.dialect.blocks.get(block); + + if (!blockFn) { + throw new Error('Unknown block "' + block + '"'); + } + + return blockFn(params); +}; + +Builder.prototype.buildTemplate = function (type, params) { + var self = this; + + var template = this.dialect.templates.get(type); + if (!template) { + throw new Error('Unknown template type "' + type + '"'); + } + + params = _({}).defaults(params, template.defaults); + + if (template.validate) { + template.validate(type, params); + } + + return template.pattern.replace(blockRegExp, function (fullMatch, block, space) { + if (typeof params[block] !== 'undefined') { + return self.buildBlock(block, params) + space; + } else { + return ''; + } + }).trim(); +}; + +Builder.prototype.build = function (params) { + var builder = this; + + this._reset(); + + this._query = this.buildTemplate('query', {queryBody: params}) + ';'; + + if (this.options.separatedValues) { + return { + query: this._query, + values: this._values, + prefixValues: function () { + var values = {}; + _(this.getValuesObject()).each(function (value, name) { + values[builder._wrapPlaceholder(name)] = value; + }); + return values; + }, + getValuesArray: function () { + return _.isArray(this.values) ? this.values : _(this.values).values(); + }, + getValuesObject: function () { + return _.isArray(this.values) ? _(_.range(1, this.values.length + 1)).object(this.values) : + this.values; + } + }; + } else { + return {query: this._query}; + } +}; + +Builder.prototype.setDialect = function (name) { + if (!dialects[name]) { + throw new Error('Unknown dialect "' + name + '"'); + } + + this.dialect = new (dialects[name])(this); +}; diff --git a/legacy/json-sql/lib/dialects/base/blocks.js b/legacy/json-sql/lib/dialects/base/blocks.js new file mode 100644 index 00000000..809c5fd6 --- /dev/null +++ b/legacy/json-sql/lib/dialects/base/blocks.js @@ -0,0 +1,441 @@ +'use strict'; + +var _ = require('underscore'); +var scheme = require('./scheme.js'); + +function removeTopBrackets(condition) { + if (condition.length && condition[0] === '(' && + condition[condition.length - 1] === ')') { + condition = condition.slice(1, condition.length - 1); + } + + return condition; +} + +module.exports = function(dialect) { + dialect.blocks.add('distinct', function() { + return 'distinct'; + }); + + + dialect.blocks.add('field', function(params) { + var field = params.field || params.$field; + var expression = params.expression || params.$expression; + + if (!field && !expression) { + throw new Error('Neither `field` nor `expression` properties aren\'t set'); + } + + if (field) { + field = this.dialect._wrapIdentifier(field); + + var table = params.table || params.$table; + if (table) { + field = this.buildBlock('table', {table: table}) + '.' + field; + } + + if (expression) { + if (!_.isArray(expression)) expression = [expression]; + var exprSuffix = (new Array(expression.length + 1)).join(')'); + field = expression.join('(') + '(' + field + exprSuffix; + } + } else { + field = expression; + } + + var cast = params.cast || params.$cast; + if (cast) { + field = 'cast(' + field + ' as ' + cast + ')'; + } + + var alias = params.alias || params.$alias; + if (alias) { + field += ' ' + this.buildBlock('alias', {alias: alias}); + } + + return field; + }); + + + dialect.blocks.add('fields', function(params) { + var self = this; + + var fields = params.fields || {}; + var result = ''; + + if (!_.isObject(fields) && !_.isString(fields)) { + throw new Error('Invalid `fields` property type "' + (typeof fields) + '"'); + } + + if (_.isObject(fields)) { + if (_.isEmpty(fields)) { + result = '*'; + } else { + // If fields is array: ['a', {b: 'c'}, {field: '', table: 't', alias: 'r'}] + if (_.isArray(fields)) { + result = _(fields).map(function(item) { + + if (_.isObject(item)) { + // if field is field object: {field: '', table: 't', alias: 'r'} + if (item.field || item.expression) { + return self.buildBlock('field', item); + + // if field is non-field object: {b: 'c'} + } else { + return self.buildBlock('fields', {fields: item}); + } + + // if field is string: 'a' + } else if (_.isString(item)) { + return self.buildBlock('field', {field: item}); + } + }); + + // If fields is object: {a: 'u', b: {table: 't', alias: 'c'}} + } else { + // use keys as field names + result = _(fields).map(function(item, field) { + // b: {table: 't', alias: 'c'} + if (_.isObject(item)) { + if (!item.field) { + item = _.clone(item); + item.field = field; + } + + return self.buildBlock('field', item); + + // a: 'u' + } else if (_.isString(item)) { + return self.buildBlock('field', { + field: field, + alias: item + }); + } + + return ''; + }); + } + + result = result.join(', '); + } + } else { + result = fields; + } + + return result; + }); + + dialect.blocks.add('table', function(params) { + return this.dialect._wrapIdentifier(params.table); + }); + + dialect.blocks.add('expression', function(params) { + return params.expression; + }); + + dialect.blocks.add('name', function(params) { + return this.dialect._wrapIdentifier(params.name); + }); + + dialect.blocks.add('alias', function(params) { + var self = this; + + var result; + + if (_.isObject(params.alias)) { + if (!params.alias.name) { + throw new Error('Alias `name` property is required'); + } + + result = this.dialect._wrapIdentifier(params.alias.name); + + if (_.isArray(params.alias.columns)) { + result += '(' + _(params.alias.columns).map(function(column) { + return self.dialect._wrapIdentifier(column); + }).join(', ') + ')'; + } + } else { + result = this.dialect._wrapIdentifier(params.alias); + } + + return 'as ' + result; + }); + + dialect.blocks.add('condition', function(params) { + var condition = params.condition; + var result = ''; + + if (_.isObject(condition)) { + result = this.buildCondition({ + condition: condition, + operator: '$eq' + }); + } else if (_.isString(condition)) { + result = condition; + } + + if (result) { + result = 'where ' + removeTopBrackets(result); + } + + return result; + }); + + dialect.blocks.add('modifier', function(params) { + var self = this; + + var modifier = params.modifier; + var result = ''; + + // if modifier is object -> call method for each operator + if (_.isObject(modifier)) { + result = _(modifier).map(function(values, field) { + var modifierFn = self.dialect.modifiers.get(field); + var methodParams = values; + + if (!modifierFn) { + modifierFn = self.dialect.modifiers.get(self.dialect.config.defaultModifier); + methodParams = {}; + methodParams[field] = values; + } + + return modifierFn.call(self, methodParams); + }).join(', '); + + // if modifier is string -> not process it + } else if (_.isString(modifier)) { + result = modifier; + } + + if (result) { + result = 'set ' + result; + } + + return result; + }); + + dialect.blocks.add('join', function(params) { + var self = this; + + var join = params.join; + var result = ''; + + // if join is array -> make each joinItem + if (_.isArray(join)) { + result = _(join).map(function(joinItem) { + return self.buildTemplate('joinItem', joinItem); + }).join(' '); + + // if join is object -> set table name from key and make each joinItem + } else if (_.isObject(join)) { + result = _(join).map(function(joinItem, table) { + if (!joinItem.table && !joinItem.query && !joinItem.select) { + joinItem = _.clone(joinItem); + joinItem.table = table; + } + + return self.buildTemplate('joinItem', joinItem); + }).join(' '); + + // if join is string -> not process + } else if (_.isString(join)) { + result = join; + } + + return result; + }); + + dialect.blocks.add('type', function(params) { + return params.type.toLowerCase(); + }); + + + dialect.blocks.add('on', function(params) { + var on = params.on; + var result = ''; + + // `on` block is use `$field` as default compare operator + // because it most used case + if (_.isObject(on)) { + result = this.buildCondition({ + condition: on, + operator: '$field' + }); + } else if (_.isString(on)) { + result = on; + } + + if (result) { + result = 'on ' + removeTopBrackets(result); + } + + return result; + }); + + + dialect.blocks.add('group', function(params) { + var result = ''; + var group = params.group; + + if (_.isArray(group)) { + var self = this; + result = _(group).map(function(field) { + return self.dialect._wrapIdentifier(field); + }).join(', '); + } else if (_.isString(group)) { + result = this.dialect._wrapIdentifier(group); + } else if (_.isObject(group)) { + result = group.expression + (group.having ? " having " + this.buildCondition({ + condition: group.having + }) : ""); + } + + if (result) { + result = 'group by ' + result; + } + + return result; + }); + + + dialect.blocks.add('sort', function(params) { + var result = ''; + var sort = params.sort; + + // if sort is array -> field1, field2, ... + var self = this; + if (_.isArray(sort)) { + result = _(sort).map(function(sortField) { + return self.dialect._wrapIdentifier(sortField); + }).join(', '); + + // if sort is object -> field1 asc, field2 desc, ... + } else if (_.isObject(sort)) { + result = _(sort).map(function(direction, field) { + return self.dialect._wrapIdentifier(field) + ' ' + + (direction > 0 ? 'asc' : 'desc'); + }).join(', '); + + // if sort is string -> not process + } else if (_.isString(sort)) { + result = this.dialect._wrapIdentifier(sort); + } + + if (result) { + result = 'order by ' + result; + } + + return result; + }); + + + dialect.blocks.add('limit', function(params) { + return 'limit ' + this._pushValue(params.limit); + }); + + + dialect.blocks.add('offset', function(params) { + return 'offset ' + this._pushValue(params.offset); + }); + + + dialect.blocks.add('or', function(params) { + return 'or ' + params.or; + }); + + + dialect.blocks.add('values', function(params) { + var self = this; + + var fieldValues = params.values; + + if (!_.isArray(fieldValues)) { + fieldValues = [fieldValues]; + } + + var fields = params.fields || _(fieldValues).chain().map(function(values) { + return _(values).keys(); + }).flatten().uniq().value(); + + fieldValues = _(fieldValues).map(function(values) { + return _(fields).map(function(field) { + return self._pushValue(values[field]); + }); + }); + + return this.buildTemplate('insertValues', { + fields: fields, + fieldValues: fieldValues + }); + }); + + dialect.blocks.add('fieldValues', function(params) { + return _(params.fieldValues).map(function(values) { + return '(' + values.join(', ') + ')'; + }).join(', '); + }); + + dialect.blocks.add('queryBody', function(params) { + var query = params.queryBody || {}; + + return this.buildTemplate(query.type || 'select', query); + }); + + dialect.blocks.add('query', function(params) { + return this.buildTemplate('subQuery', {queryBody: params.query}); + }); + + dialect.blocks.add('select', function(params) { + return this.buildTemplate('subQuery', {queryBody: params.select}); + }); + + dialect.blocks.add('tableFields', function (params) { + return scheme.parse.call(this, params.tableFields, params.foreignKeys); + }); + + dialect.blocks.add('queries', function(params) { + var self = this; + + return _(params.queries).map(function(query) { + return self.buildTemplate('subQuery', {queryBody: query}); + }).join(' ' + params.type + (params.all ? ' all' : '') + ' '); + }); + + dialect.blocks.add('indexOn', function (params) { + return params.indexOn; + }); + + dialect.blocks.add('with', function(params) { + var self = this; + + var withList = params['with']; + var result = ''; + + // if with clause is array -> make each withItem + if (_.isArray(withList)) { + result = _(withList).map(function(withItem) { + return self.buildTemplate('withItem', withItem); + }).join(', '); + + // if with clause is object -> set name from key and make each withItem + } else if (_.isObject(withList)) { + result = _(withList).map(function(withItem, name) { + if (!withItem.name) { + withItem = _.clone(withItem); + withItem.name = name; + } + return self.buildTemplate('withItem', withItem); + }).join(', '); + + // if with clause is string -> not process + } else if (_.isString(withList)) { + result = withList; + } + + return 'with ' + result; + }); + + dialect.blocks.add('returning', function(params) { + return 'returning ' + this.buildBlock('fields', {fields: params.returning}); + }); +}; diff --git a/legacy/json-sql/lib/dialects/base/conditions.js b/legacy/json-sql/lib/dialects/base/conditions.js new file mode 100644 index 00000000..c46be4f3 --- /dev/null +++ b/legacy/json-sql/lib/dialects/base/conditions.js @@ -0,0 +1,122 @@ +'use strict'; + +var _ = require('underscore'); + +// Compare conditions (e.g. $eq, $gt) +function buildCompareCondition(builder, field, operator, value) { + var placeholder; + + // if value is object, than make field block from it + if (value && _.isObject(value)) { + placeholder = builder.buildBlock('field', value); + } else { + // if value is simple - create placeholder for it + placeholder = builder._pushValue(value); + } + + field = builder.dialect._wrapIdentifier(field); + return [field, operator, placeholder].join(' '); +} + +// Contain conditions ($in/$nin) +function buildContainsCondition(builder, field, operator, value) { + var newValue; + + if (_.isArray(value)) { + if (!value.length) value = [null]; + + newValue = '(' + _(value).map(function(item) { + return builder._pushValue(item); + }).join(', ') + ')'; + } else if (_.isObject(value)) { + newValue = builder.buildTemplate('subQuery', {queryBody: value}); + } else { + throw new Error('Invalid `' + operator + '` value type "' + (typeof value) + '"'); + } + + field = builder.dialect._wrapIdentifier(field); + return [field, operator, newValue].join(' '); +} + +module.exports = function(dialect) { + dialect.conditions.add('$eq', function(field, operator, value) { + return buildCompareCondition(this, field, '=', value); + }); + + dialect.conditions.add('$ne', function(field, operator, value) { + return buildCompareCondition(this, field, '!=', value); + }); + + dialect.conditions.add('$gt', function(field, operator, value) { + return buildCompareCondition(this, field, '>', value); + }); + + dialect.conditions.add('$lt', function(field, operator, value) { + return buildCompareCondition(this, field, '<', value); + }); + + dialect.conditions.add('$gte', function(field, operator, value) { + return buildCompareCondition(this, field, '>=', value); + }); + + dialect.conditions.add('$lte', function(field, operator, value) { + return buildCompareCondition(this, field, '<=', value); + }); + + dialect.conditions.add('$is', function(field, operator, value) { + return buildCompareCondition(this, field, 'is', value); + }); + + dialect.conditions.add('$isnot', function(field, operator, value) { + return buildCompareCondition(this, field, 'is not', value); + }); + + dialect.conditions.add('$like', function(field, operator, value) { + return buildCompareCondition(this, field, 'like', value); + }); + + dialect.conditions.add('$null', function(field, operator, value) { + return buildCompareCondition(this, field, 'is' + (value ? '' : ' not'), null); + }); + + dialect.conditions.add('$field', function(field, operator, value) { + var placeholder; + + // if value is object, than make field block from it + if (_.isObject(value)) { + placeholder = this.buildBlock('field', value); + } else { + // $field - special operator, that not make placeholder for value + placeholder = this.dialect._wrapIdentifier(value); + } + + return [this.dialect._wrapIdentifier(field), '=', placeholder].join(' '); + }); + + + dialect.conditions.add('$in', function(field, operator, value) { + return buildContainsCondition(this, field, 'in', value); + }); + + dialect.conditions.add('$nin', function(field, operator, value) { + return buildContainsCondition(this, field, 'not in', value); + }); + + dialect.conditions.add('$between', function(field, operator, value) { + if (!_.isArray(value)) { + throw new Error('Invalid `$between` value type "' + (typeof value) + '"'); + } + + if (value.length < 2) { + throw new Error('`$between` array length should be 2 or greater'); + } + + return [ + this.dialect._wrapIdentifier(field), + 'between', + this._pushValue(value[0]), + 'and', + this._pushValue(value[1]) + ].join(' '); + }); +}; diff --git a/legacy/json-sql/lib/dialects/base/index.js b/legacy/json-sql/lib/dialects/base/index.js new file mode 100644 index 00000000..2a0e2c7a --- /dev/null +++ b/legacy/json-sql/lib/dialects/base/index.js @@ -0,0 +1,58 @@ +'use strict'; + +var _ = require('underscore'); +var ValuesStore = require('../../valuesStore'); + +var templatesInit = require('./templates'); +var blocksInit = require('./blocks'); +var conditionsInit = require('./conditions'); +var logicalOperatorsInit = require('./logicalOperators'); +var modifiersInit = require('./modifiers'); + +var Dialect = module.exports = function(builder) { + this.builder = builder; + + this.blocks = new ValuesStore({context: builder}); + this.modifiers = new ValuesStore({context: builder}); + this.conditions = new ValuesStore({context: builder}); + this.logicalOperators = new ValuesStore({context: builder}); + this.templates = new ValuesStore({context: builder}); + + templatesInit(this); + blocksInit(this); + conditionsInit(this); + modifiersInit(this); + logicalOperatorsInit(this); + + this.identifierPartsRegexp = new RegExp( + '(\\' + this.config.identifierPrefix + '[^\\' + this.config.identifierSuffix + ']*\\' + + this.config.identifierSuffix + '|[^\\.]+)', 'g' + ); + this.wrappedIdentifierPartRegexp = new RegExp( + '^\\' + this.config.identifierPrefix + '.*\\' + this.config.identifierSuffix + '$' + ); +}; + +Dialect.prototype.config = { + identifierPrefix: '"', + identifierSuffix: '"', + defaultLogicalOperator: '$and', + defaultModifier: '$set' +}; + +Dialect.prototype._wrapIdentifier = function(name) { + if (this.builder.options.wrappedIdentifiers) { + var self = this; + var nameParts = name.match(this.identifierPartsRegexp); + + return _(nameParts).map(function(namePart) { + if (namePart !== '*' && !self.wrappedIdentifierPartRegexp.test(namePart)) { + namePart = self.config.identifierPrefix + namePart + self.config.identifierSuffix; + } + + return namePart; + }).join('.'); + } + + return name; +}; diff --git a/legacy/json-sql/lib/dialects/base/logicalOperators.js b/legacy/json-sql/lib/dialects/base/logicalOperators.js new file mode 100644 index 00000000..fb2879c1 --- /dev/null +++ b/legacy/json-sql/lib/dialects/base/logicalOperators.js @@ -0,0 +1,37 @@ +'use strict'; + +var _ = require('underscore'); + +function buildJoinOperator(conditions, operator) { + var isBracketsNeeded = conditions.length > 1; + var result = conditions.join(' ' + operator + ' '); + + if (result && isBracketsNeeded) result = '(' + result + ')'; + + return result; +} + +module.exports = function(dialect) { + dialect.logicalOperators.add('$and', function(conditions) { + return buildJoinOperator(conditions, 'and'); + }); + + dialect.logicalOperators.add('$or', function(conditions) { + return buildJoinOperator(conditions, 'or'); + }); + + dialect.logicalOperators.add('$not', function(conditions) { + var result = ''; + + if (_.isArray(conditions)) { + result = dialect.logicalOperators + .get(dialect.config.defaultLogicalOperator)(conditions); + } else if (_.isString(conditions)) { + result = conditions; + } + + if (result) result = 'not ' + result; + + return result; + }); +}; diff --git a/legacy/json-sql/lib/dialects/base/modifiers.js b/legacy/json-sql/lib/dialects/base/modifiers.js new file mode 100644 index 00000000..3be42aa6 --- /dev/null +++ b/legacy/json-sql/lib/dialects/base/modifiers.js @@ -0,0 +1,37 @@ +'use strict'; + +var _ = require('underscore'); + +module.exports = function(dialect) { + dialect.modifiers.add('$set', function(values) { + var self = this; + + return _(values).map(function(value, field) { + var placeholder = self._pushValue(value); + + return [self.dialect._wrapIdentifier(field), '=', placeholder].join(' '); + }).join(', '); + }); + + dialect.modifiers.add('$inc', function(values) { + var self = this; + + return _(values).map(function(value, field) { + var placeholder = self._pushValue(value); + field = self.dialect._wrapIdentifier(field); + + return [field, '=', field, '+', placeholder].join(' '); + }).join(', '); + }); + + dialect.modifiers.add('$dec', function(values) { + var self = this; + + return _(values).map(function(value, field) { + var placeholder = self._pushValue(value); + field = self.dialect._wrapIdentifier(field); + + return [field, '=', field, '-', placeholder].join(' '); + }).join(', '); + }); +}; diff --git a/legacy/json-sql/lib/dialects/base/scheme.js b/legacy/json-sql/lib/dialects/base/scheme.js new file mode 100644 index 00000000..686e16ed --- /dev/null +++ b/legacy/json-sql/lib/dialects/base/scheme.js @@ -0,0 +1,146 @@ +var SchemeParser = function () { +} + +var types = { + "Number": {syntax: "int"}, + "BigInt": {syntax: "bigint"}, + "SmallInt": {syntax: "smallint"}, + "String": {syntax: "varchar", length: true}, + "Text": {syntax: "text"}, + "Real": {syntax: "real"}, + "Boolean": {syntax: "boolean"}, + "Blob": {syntax: "blob"}, + "Binary": {syntax: "bytea"} +} + +// for future +var onDeleteTrigger = { + "set_null": "SET NULL", + "cascade": "CASCADE" +} + +function getType(field) { + var s = ""; + var type = types[field.type]; + + if (!type) { + throw new Error("Invalid type of field: " + field.type); + } + + s += type.syntax; + + if (type.length) { + if (!field.length || field.length <= 0) { + throw new Error("Field length can't be less or equal 0"); + } + + s += "(" + field.length + ")"; + } else if (type.default_length) { + s += "(" + type.default_length + ")"; + } + + return s; +} + +function foreignkeys(fields, keys) { + if (!keys || keys.length == 0) { + return ""; + } + + var s = ", "; + var names = []; + fields.forEach(function (field) { + names.push(field.name) + }); + + keys.forEach(function (key, i) { + if (!key.field) { + throw new Error("Provide field for foreign key"); + } + + if (names.indexOf(key.field) < 0) { + throw new Error("Not exists field to make foreign key: " + key.field); + } + + if (!key.table || key.table.trim().length == 0) { + throw new Error("Invalid reference table name"); + } + + if (!key.table_field || key.table_field.trim().length == 0) { + throw new Error("Invalid reference table filed"); + } + + s += "FOREIGN KEY (\"" + key.field + "\") REFERENCES " + key.table + "(\"" + key.table_field + "\")" + (key.on_delete ? " ON DELETE " + key.on_delete : ""); + if (i != keys.length - 1) { + s += ","; + } + }); + + return s; +} + +function parse(fields, fkeys) { + var sql_fields = ""; + var checkPrimaryKey = false; + var names = []; + + fields.forEach(function (field, i) { + if (!field.name) { + throw new Error("Name of field most be provided"); + } + + if (field.name.trim().length == 0) { + throw new Error("Name most contains characters"); + } + + if (names.indexOf(field.name) >= 0) { + throw new Error("Two parameters with same name: " + field.name); + } + + var line = this.dialect._wrapIdentifier(field.name) + " " + getType(field); + + if (field.not_null) { + line += " NOT NULL"; + } + + if (field.default !== undefined) { + var _type = typeof field.default; + var _default = field.default; + if (_type === "string") { + _default = "'" + field.default + "'"; + } + line += " default " + _default; + } + + if (field.unique) { + line += " UNIQUE"; + } + + + if (field.primary_key) { + if (checkPrimaryKey) { + throw new Error("Too much primary key '" + field.name + "' in table"); + } else { + checkPrimaryKey = true; + } + + line += " PRIMARY KEY"; + } + + sql_fields += line; + + names.push(field.name); + + if (i != fields.length - 1) { + sql_fields += ","; + } + }.bind(this)); + + sql_fields += foreignkeys(fields, fkeys); + return sql_fields; +} + +module.exports = { + parse: parse, + foreignkeys: foreignkeys +} diff --git a/legacy/json-sql/lib/dialects/base/templates.js b/legacy/json-sql/lib/dialects/base/templates.js new file mode 100644 index 00000000..81c60cdb --- /dev/null +++ b/legacy/json-sql/lib/dialects/base/templates.js @@ -0,0 +1,220 @@ +'use strict'; + +var _ = require('underscore'); + +module.exports = function(dialect) { + var availableSourceProps = ['table', 'query', 'select', 'expression']; + var availableJoinTypes = ['natural', 'cross', 'inner', 'outer', 'left', 'right', 'full', 'self']; + var orRegExp = /^(rollback|abort|replace|fail|ignore)$/i; + + // private templates + + dialect.templates.add('query', { + pattern: '{queryBody}', + validate: function(type, params) { + hasRequiredProp(type, params, 'queryBody'); + hasObjectProp(type, params, 'queryBody'); + } + }); + + + dialect.templates.add('subQuery', { + pattern: '({queryBody})', + validate: function(type, params) { + hasRequiredProp(type, params, 'queryBody'); + hasObjectProp(type, params, 'queryBody'); + } + }); + + dialect.templates.add('create', { + pattern: 'create table if not exists {table}({tableFields})', + validate: function (type, params) { + hasRequiredProp(type, params, 'table'); + hasRequiredProp(type, params, 'tableFields'); + hasArrayProp(type, params, 'tableFields'); + } + }); + + dialect.templates.add('index', { + pattern: 'create index if not exists {name} ON {table}({indexOn}) {condition}', + validate: function (type, params) { + hasRequiredProp(type, params, 'table'); + hasRequiredProp(type, params, 'name') + hasRequiredProp(type, params, 'indexOn'); + hasMinPropLength(type, params, 'name', 1); + hasMinPropLength(type, params, 'indexOn', 1); + } + }) + + + dialect.templates.add('queriesCombination', { + pattern: '{with} {queries} {sort} {limit} {offset}', + validate: function(type, params) { + hasRequiredProp(type, params, 'queries'); + hasArrayProp(type, params, 'queries'); + hasMinPropLength(type, params, 'queries', 2); + } + }); + + + dialect.templates.add('insertValues', { + pattern: '({fields}) values {fieldValues}', + validate: function(type, params) { + hasRequiredProp('values', params, 'fields'); + hasArrayProp('values', params, 'fields'); + hasMinPropLength('values', params, 'fields', 1); + + hasRequiredProp('values', params, 'fieldValues'); + hasArrayProp('values', params, 'fieldValues'); + hasMinPropLength('values', params, 'fieldValues', 1); + } + }); + + + dialect.templates.add('joinItem', { + pattern: '{type} join {table} {query} {select} {expression} {alias} {on}', + validate: function(type, params) { + hasOneOfProps('join', params, availableSourceProps); + + if (params.type) { + hasStringProp('join', params, 'type'); + + var splitType = _(params.type.toLowerCase().split(' ')).compact(); + if (_.difference(splitType, availableJoinTypes).length) { + throw new Error('Invalid `type` property value "' + params.type + '" in `join` clause'); + } + } + } + }); + + + dialect.templates.add('withItem', { + pattern: '{name} {fields} as {query} {select} {expression}', + validate: function(type, params) { + hasRequiredProp('with', params, 'name'); + hasOneOfProps('with', params, ['query', 'select', 'expression']); + } + }); + + + dialect.templates.add('fromItem', { + pattern: '{table} {query} {select} {expression}', + validate: function(type, params) { + hasOneOfProps('from', params, availableSourceProps); + } + }); + + + // public templates + + dialect.templates.add('select', { + pattern: '{with} select {distinct} {fields} ' + + 'from {from} {table} {query} {select} {expression} {alias} ' + + '{join} {condition} {group} {sort} {limit} {offset}', + defaults: { + fields: {} + }, + validate: function(type, params) { + hasOneOfProps(type, params, availableSourceProps); + } + }); + + + dialect.templates.add('insert', { + pattern: '{with} insert {or} into {table} {values} {returning} {condition}', + validate: function(type, params) { + hasRequiredProp(type, params, 'values'); + hasObjectProp(type, params, 'values'); + hasOneOfProps(type, params, availableSourceProps); + if (params.or) { + hasStringProp(type, params, 'or'); + matchesRegExpProp(type, params, 'or', orRegExp); + } + } + }); + + + dialect.templates.add('update', { + pattern: '{with} update {or} {table} {modifier} {condition} {returning}', + validate: function(type, params) { + hasRequiredProp(type, params, 'modifier'); + hasRequiredProp(type, params, 'table'); + if (params.or) { + hasStringProp(type, params, 'or'); + matchesRegExpProp(type, params, 'or', orRegExp); + } + } + }); + + + dialect.templates.add('remove', { + pattern: '{with} delete from {table} {condition} {returning}', + validate: function(type, params) { + hasRequiredProp(type, params, 'table'); + } + }); + + + dialect.templates.add('union', dialect.templates.get('queriesCombination')); + + + dialect.templates.add('intersect', dialect.templates.get('queriesCombination')); + + + dialect.templates.add('except', dialect.templates.get('queriesCombination')); + + + // validation helpers + + function hasRequiredProp(type, params, propName) { + if (!params[propName]) { + throw new Error('`' + propName + '` property is not set in `' + type + '` clause'); + } + } + + function hasObjectProp(type, params, propName) { + if (!_.isObject(params[propName])) { + throw new Error('`' + propName + '` property should be an object in `' + type + '` clause'); + } + } + + function hasArrayProp(type, params, propName) { + if (!_.isArray(params[propName])) { + throw new Error('`' + propName + '` property should be an array in `' + type + '` clause'); + } + } + + function hasStringProp(type, params, propName) { + if (!_.isString(params.type)) { + throw new Error('`' + propName + '` property should be a string in `' + type + '` clause'); + } + } + + function hasMinPropLength(type, params, propName, length) { + if (params[propName].length < length) { + throw new Error('`' + propName + '` property should not have length less than ' + length + + ' in `' + type + '` clause'); + } + } + + function hasOneOfProps(type, params, expectedPropNames) { + var propNames = _(params).chain().keys().intersection(expectedPropNames).value(); + + if (!propNames.length) { + throw new Error('Neither `' + expectedPropNames.join('`, `') + + '` properties are not set in `' + type + '` clause'); + } + + if (propNames.length > 1) { + throw new Error('Wrong using `' + propNames.join('`, `') + '` properties together in `' + + type + '` clause'); + } + } + + function matchesRegExpProp(type, params, propName, regExp) { + if (!params[propName].match(regExp)) { + throw new Error('Invalid `' + propName + '` property value "' + params[propName] + '" in `' + + type + '` clause'); + } + } +}; diff --git a/legacy/json-sql/lib/dialects/mssql/index.js b/legacy/json-sql/lib/dialects/mssql/index.js new file mode 100644 index 00000000..8374184f --- /dev/null +++ b/legacy/json-sql/lib/dialects/mssql/index.js @@ -0,0 +1,13 @@ +'use strict'; + +var BaseDialect = require('../base'); +var _ = require('underscore'); +var util = require('util'); + +var Dialect = module.exports = function(builder) { + BaseDialect.call(this, builder); +}; + +util.inherits(Dialect, BaseDialect); + +Dialect.prototype.config = _({}).extend(BaseDialect.prototype.config, {}); diff --git a/legacy/json-sql/lib/dialects/postgresql/blocks.js b/legacy/json-sql/lib/dialects/postgresql/blocks.js new file mode 100644 index 00000000..e6f0066d --- /dev/null +++ b/legacy/json-sql/lib/dialects/postgresql/blocks.js @@ -0,0 +1,33 @@ +'use strict'; + +var _ = require('underscore'); + +module.exports = function(dialect) { + dialect.blocks.add('offset', function(params) { + var limit = ''; + + if (typeof params.limit === 'undefined') { + limit = this.buildBlock('limit', {limit: -1}) + ' '; + } + + return limit + 'offset ' + this._pushValue(params.offset); + }); + + dialect.blocks.add('unionqueries', function(params) { + var self = this; + + return _(params.unionqueries).map(function(query) { + return self.buildTemplate('subUnionQuery', {queryBody: query}); + }).join(' ' + params.type + (params.all ? ' all' : '') + ' '); + }); + + dialect.blocks.add('conflictFields', function(params) { + var self = this; + + var fields = _(params.conflictFields).map(function(field) { + return self.dialect._wrapIdentifier(field); + }); + + return '(' + fields.join(',') + ')'; + }); +}; diff --git a/legacy/json-sql/lib/dialects/postgresql/conditions.js b/legacy/json-sql/lib/dialects/postgresql/conditions.js new file mode 100644 index 00000000..dddba22a --- /dev/null +++ b/legacy/json-sql/lib/dialects/postgresql/conditions.js @@ -0,0 +1,87 @@ +'use strict'; + +var _ = require('underscore'); + +var buildJsonInCondition = function(builder, field, operator, value) { + field = builder.dialect._wrapIdentifier(field); + + var placeholder; + try { + placeholder = builder.buildBlock('field', value); + } catch (e) { + placeholder = builder._pushValue(JSON.stringify(value)); + } + + return [field, operator, placeholder].join(' '); +}; + +var buildJsonHasCondition = function(builder, field, operator, value) { + field = builder.dialect._wrapIdentifier(field); + + var placeholder; + if (_(value).isArray()) { + placeholder = 'array[' + _(value).map(function(item) { + return builder._pushValue(item); + }).join(', ') + ']'; + } else { + placeholder = builder.buildBlock('field', value); + } + + return [field, operator, placeholder].join(' '); +}; + +module.exports = function(dialect) { + dialect.conditions.add('$jsonContains', function(field, operator, value) { + return buildJsonInCondition(this, field, '@>', value); + }); + + dialect.conditions.add('$jsonIn', function(field, operator, value) { + return buildJsonInCondition(this, field, '<@', value); + }); + + dialect.conditions.add('$jsonHas', function(field, operator, value) { + field = this.dialect._wrapIdentifier(field); + + var placeholder = value; + if (_(placeholder).isObject()) { + placeholder = this.buildBlock('field', placeholder); + } else { + placeholder = this._pushValue(value.toString()); + } + + return [field, '?', placeholder].join(' '); + }); + + dialect.conditions.add('$jsonHasAny', function(field, operator, value) { + return buildJsonHasCondition(this, field, '?|', value); + }); + + dialect.conditions.add('$jsonHasAll', function(field, operator, value) { + return buildJsonHasCondition(this, field, '?&', value); + }); + + dialect.conditions.add('$upper', function(field, operator, value) { + return [ + 'upper(' + this.dialect._wrapIdentifier(field) + ')', + '=', + 'upper(' + this._pushValue(value[1]) + ')' + ].join(' '); + }); + + dialect.conditions.add('$lower', function(field, operator, value) { + return [ + 'lower(' + this.dialect._wrapIdentifier(field) + ')', + '=', + 'lower(' + this._pushValue(value[1]) + ')' + ].join(' '); + }); + + dialect.conditions.add('$decode', function (field, operator, value) { + return [ + this.dialect._wrapIdentifier(field), + '=', + 'decode(' + this._pushValue(value[1]) + ',', + this._pushValue(value[2]) + ')' + ].join(' '); + }); +}; diff --git a/legacy/json-sql/lib/dialects/postgresql/index.js b/legacy/json-sql/lib/dialects/postgresql/index.js new file mode 100644 index 00000000..755dc00d --- /dev/null +++ b/legacy/json-sql/lib/dialects/postgresql/index.js @@ -0,0 +1,39 @@ +'use strict'; + +var BaseDialect = require('../base'); +var _ = require('underscore'); +var util = require('util'); +var blocksInit = require('./blocks'); +var conditionsInit = require('./conditions'); +var templatesInit = require('./templates'); + +var Dialect = module.exports = function (builder) { + BaseDialect.call(this, builder); + blocksInit(this) + conditionsInit(this); + templatesInit(this); +}; + +Dialect.prototype.config = _({}).extend(BaseDialect.prototype.config); + +util.inherits(Dialect, BaseDialect); + +Dialect.prototype.config = _({ + jsonSeparatorRegexp: /->>?/g +}).extend(BaseDialect.prototype.config); + +Dialect.prototype._wrapIdentifier = function(name) { + // split by json separator + var nameParts = name.split(this.config.jsonSeparatorRegexp); + var separators = name.match(this.config.jsonSeparatorRegexp); + + // wrap base identifier + var identifier = BaseDialect.prototype._wrapIdentifier.call(this, nameParts[0]); + + // wrap all json identifier and join them with separators + identifier += _(separators).reduce(function(memo, separator, index) { + return memo + separator + '\'' + nameParts[index + 1] + '\''; + }, ''); + + return identifier; +}; diff --git a/legacy/json-sql/lib/dialects/postgresql/templates.js b/legacy/json-sql/lib/dialects/postgresql/templates.js new file mode 100644 index 00000000..297ed8b0 --- /dev/null +++ b/legacy/json-sql/lib/dialects/postgresql/templates.js @@ -0,0 +1,269 @@ +'use strict'; + +var _ = require('underscore'); + +module.exports = function(dialect) { + var availableSourceProps = ['table', 'query', 'select', 'expression']; + var availableJoinTypes = ['natural', 'cross', 'inner', 'outer', 'left', 'right', 'full', 'self']; + var orRegExp = /^(rollback|abort|replace|fail|ignore)$/i; + + // private templates + + dialect.templates.add('query', { + pattern: '{queryBody}', + validate: function(type, params) { + hasRequiredProp(type, params, 'queryBody'); + hasObjectProp(type, params, 'queryBody'); + } + }); + + dialect.templates.add('subQuery', { + pattern: '({queryBody})', + validate: function(type, params) { + hasRequiredProp(type, params, 'queryBody'); + hasObjectProp(type, params, 'queryBody'); + } + }); + + dialect.templates.add('subUnionQuery', { + pattern: '{queryBody}', + validate: function(type, params) { + hasRequiredProp(type, params, 'queryBody'); + hasObjectProp(type, params, 'queryBody'); + } + }); + + dialect.templates.add('create', { + pattern: 'create table if not exists {table}({tableFields})', + validate: function (type, params) { + hasRequiredProp(type, params, 'table'); + hasRequiredProp(type, params, 'tableFields'); + hasArrayProp(type, params, 'tableFields'); + } + }); + + dialect.templates.add('index', { + pattern: 'create index if not exists {name} ON {table}({indexOn}) {condition}', + validate: function (type, params) { + hasRequiredProp(type, params, 'table'); + hasRequiredProp(type, params, 'name') + hasRequiredProp(type, params, 'indexOn'); + hasMinPropLength(type, params, 'name', 1); + hasMinPropLength(type, params, 'indexOn', 1); + } + }) + + + dialect.templates.add('queriesCombination', { + pattern: '{with} {queries} {sort} {limit} {offset}', + validate: function(type, params) { + hasRequiredProp(type, params, 'queries'); + hasArrayProp(type, params, 'queries'); + hasMinPropLength(type, params, 'queries', 2); + } + }); + + dialect.templates.add('queriesUnionCombination', { + pattern: '{with} {unionqueries} {sort} {limit} {offset}', + validate: function(type, params) { + hasRequiredProp(type, params, 'unionqueries'); + hasArrayProp(type, params, 'unionqueries'); + hasMinPropLength(type, params, 'unionqueries', 2); + } + }); + + + dialect.templates.add('insertValues', { + pattern: '({fields}) values {fieldValues}', + validate: function(type, params) { + hasRequiredProp('values', params, 'fields'); + hasArrayProp('values', params, 'fields'); + hasMinPropLength('values', params, 'fields', 1); + + hasRequiredProp('values', params, 'fieldValues'); + hasArrayProp('values', params, 'fieldValues'); + hasMinPropLength('values', params, 'fieldValues', 1); + } + }); + + + dialect.templates.add('joinItem', { + pattern: '{type} join {table} {query} {select} {expression} {alias} {on}', + validate: function(type, params) { + hasOneOfProps('join', params, availableSourceProps); + + if (params.type) { + hasStringProp('join', params, 'type'); + + var splitType = _(params.type.toLowerCase().split(' ')).compact(); + if (_.difference(splitType, availableJoinTypes).length) { + throw new Error('Invalid `type` property value "' + params.type + '" in `join` clause'); + } + } + } + }); + + + dialect.templates.add('withItem', { + pattern: '{name} {fields} as {query} {select} {expression}', + validate: function(type, params) { + hasRequiredProp('with', params, 'name'); + hasOneOfProps('with', params, ['query', 'select', 'expression']); + } + }); + + + dialect.templates.add('fromItem', { + pattern: '{table} {query} {select} {expression}', + validate: function(type, params) { + hasOneOfProps('from', params, availableSourceProps); + } + }); + + + // public templates + + dialect.templates.add('select', { + pattern: '{with} select {distinct} {fields} ' + + 'from {from} {table} {query} {select} {expression} {alias} ' + + '{join} {condition} {group} {sort} {limit} {offset}', + defaults: { + fields: {} + }, + validate: function(type, params) { + hasOneOfProps(type, params, availableSourceProps); + } + }); + + + dialect.templates.add('insert', { + pattern: '{with} insert {or} into {table} {values} {condition} {returning}', + validate: function(type, params) { + hasRequiredProp(type, params, 'values'); + hasObjectProp(type, params, 'values'); + hasOneOfProps(type, params, availableSourceProps); + if (params.or) { + hasStringProp(type, params, 'or'); + matchesRegExpProp(type, params, 'or', orRegExp); + } + } + }); + + + dialect.templates.add('insertornothing', { + pattern: '{with} insert {or} into {table} {values} on conflict do nothing {returning} {condition}', + validate: function(type, params) { + hasRequiredProp(type, params, 'values'); + hasObjectProp(type, params, 'values'); + hasOneOfProps(type, params, availableSourceProps); + if (params.or) { + hasStringProp(type, params, 'or'); + matchesRegExpProp(type, params, 'or', orRegExp); + } + } + }); + + + dialect.templates.add('insertorupdate', { + pattern: '{with} insert {or} into {table} {values} on conflict {conflictFields} do update {modifier} {condition} {returning}', + validate: function(type, params) { + hasRequiredProp(type, params, 'table'); + hasRequiredProp(type, params, 'values'); + hasObjectProp(type, params, 'values'); + hasRequiredProp('conflictFields', params, 'conflictFields'); + hasArrayProp('conflictFields', params, 'conflictFields'); + hasMinPropLength('conflictFields', params, 'conflictFields', 1); + hasRequiredProp(type, params, 'modifier'); + hasOneOfProps(type, params, availableSourceProps); + if (params.or) { + hasStringProp(type, params, 'or'); + matchesRegExpProp(type, params, 'or', orRegExp); + } + } + }); + + + dialect.templates.add('update', { + pattern: '{with} update {or} {table} {modifier} {condition} {returning}', + validate: function(type, params) { + hasRequiredProp(type, params, 'modifier'); + hasRequiredProp(type, params, 'table'); + if (params.or) { + hasStringProp(type, params, 'or'); + matchesRegExpProp(type, params, 'or', orRegExp); + } + } + }); + + + dialect.templates.add('remove', { + pattern: '{with} delete from {table} {condition} {returning}', + validate: function(type, params) { + hasRequiredProp(type, params, 'table'); + } + }); + + + dialect.templates.add('union', dialect.templates.get('queriesUnionCombination')); + + + dialect.templates.add('intersect', dialect.templates.get('queriesCombination')); + + + dialect.templates.add('except', dialect.templates.get('queriesCombination')); + + + // validation helpers + + function hasRequiredProp(type, params, propName) { + if (!params[propName]) { + throw new Error('`' + propName + '` property is not set in `' + type + '` clause'); + } + } + + function hasObjectProp(type, params, propName) { + if (!_.isObject(params[propName])) { + throw new Error('`' + propName + '` property should be an object in `' + type + '` clause'); + } + } + + function hasArrayProp(type, params, propName) { + if (!_.isArray(params[propName])) { + throw new Error('`' + propName + '` property should be an array in `' + type + '` clause'); + } + } + + function hasStringProp(type, params, propName) { + if (!_.isString(params.type)) { + throw new Error('`' + propName + '` property should be a string in `' + type + '` clause'); + } + } + + function hasMinPropLength(type, params, propName, length) { + if (params[propName].length < length) { + throw new Error('`' + propName + '` property should not have length less than ' + length + + ' in `' + type + '` clause'); + } + } + + function hasOneOfProps(type, params, expectedPropNames) { + var propNames = _(params).chain().keys().intersection(expectedPropNames).value(); + + if (!propNames.length) { + throw new Error('Neither `' + expectedPropNames.join('`, `') + + '` properties are not set in `' + type + '` clause'); + } + + if (propNames.length > 1) { + throw new Error('Wrong using `' + propNames.join('`, `') + '` properties together in `' + + type + '` clause'); + } + } + + function matchesRegExpProp(type, params, propName, regExp) { + if (!params[propName].match(regExp)) { + throw new Error('Invalid `' + propName + '` property value "' + params[propName] + '" in `' + + type + '` clause'); + } + } +}; diff --git a/legacy/json-sql/lib/dialects/sqlite/blocks.js b/legacy/json-sql/lib/dialects/sqlite/blocks.js new file mode 100644 index 00000000..c3531c2a --- /dev/null +++ b/legacy/json-sql/lib/dialects/sqlite/blocks.js @@ -0,0 +1,23 @@ +'use strict'; + +var _ = require('underscore'); + +module.exports = function(dialect) { + dialect.blocks.add('offset', function(params) { + var limit = ''; + + if (typeof params.limit === 'undefined') { + limit = this.buildBlock('limit', {limit: -1}) + ' '; + } + + return limit + 'offset ' + this._pushValue(params.offset); + }); + + dialect.blocks.add('unionqueries', function(params) { + var self = this; + + return _(params.unionqueries).map(function(query) { + return self.buildTemplate('subUnionQuery', {queryBody: query}); + }).join(' ' + params.type + (params.all ? ' all' : '') + ' '); + }); +}; diff --git a/legacy/json-sql/lib/dialects/sqlite/index.js b/legacy/json-sql/lib/dialects/sqlite/index.js new file mode 100644 index 00000000..72549400 --- /dev/null +++ b/legacy/json-sql/lib/dialects/sqlite/index.js @@ -0,0 +1,17 @@ +'use strict'; + +var BaseDialect = require('../base'); +var _ = require('underscore'); +var util = require('util'); +var blocksInit = require('./blocks'); +var templatesInit = require('./templates'); + +var Dialect = module.exports = function (builder) { + BaseDialect.call(this, builder); + blocksInit(this); + templatesInit(this); +}; + +util.inherits(Dialect, BaseDialect); + +Dialect.prototype.config = _({}).extend(BaseDialect.prototype.config); diff --git a/legacy/json-sql/lib/dialects/sqlite/templates.js b/legacy/json-sql/lib/dialects/sqlite/templates.js new file mode 100644 index 00000000..ac6e382f --- /dev/null +++ b/legacy/json-sql/lib/dialects/sqlite/templates.js @@ -0,0 +1,236 @@ +'use strict'; + +var _ = require('underscore'); + +module.exports = function(dialect) { + var availableSourceProps = ['table', 'query', 'select', 'expression']; + var availableJoinTypes = ['natural', 'cross', 'inner', 'outer', 'left', 'right', 'full', 'self']; + var orRegExp = /^(rollback|abort|replace|fail|ignore)$/i; + + // private templates + + dialect.templates.add('query', { + pattern: '{queryBody}', + validate: function(type, params) { + hasRequiredProp(type, params, 'queryBody'); + hasObjectProp(type, params, 'queryBody'); + } + }); + + dialect.templates.add('subQuery', { + pattern: '({queryBody})', + validate: function(type, params) { + hasRequiredProp(type, params, 'queryBody'); + hasObjectProp(type, params, 'queryBody'); + } + }); + + dialect.templates.add('subUnionQuery', { + pattern: '{queryBody}', + validate: function(type, params) { + hasRequiredProp(type, params, 'queryBody'); + hasObjectProp(type, params, 'queryBody'); + } + }); + + dialect.templates.add('create', { + pattern: 'create table if not exists {table}({tableFields})', + validate: function (type, params) { + hasRequiredProp(type, params, 'table'); + hasRequiredProp(type, params, 'tableFields'); + hasArrayProp(type, params, 'tableFields'); + } + }); + + dialect.templates.add('index', { + pattern: 'create index if not exists {name} ON {table}({indexOn}) {condition}', + validate: function (type, params) { + hasRequiredProp(type, params, 'table'); + hasRequiredProp(type, params, 'name') + hasRequiredProp(type, params, 'indexOn'); + hasMinPropLength(type, params, 'name', 1); + hasMinPropLength(type, params, 'indexOn', 1); + } + }) + + + dialect.templates.add('queriesCombination', { + pattern: '{with} {queries} {sort} {limit} {offset}', + validate: function(type, params) { + hasRequiredProp(type, params, 'queries'); + hasArrayProp(type, params, 'queries'); + hasMinPropLength(type, params, 'queries', 2); + } + }); + + dialect.templates.add('queriesUnionCombination', { + pattern: '{with} {unionqueries} {sort} {limit} {offset}', + validate: function(type, params) { + hasRequiredProp(type, params, 'unionqueries'); + hasArrayProp(type, params, 'unionqueries'); + hasMinPropLength(type, params, 'unionqueries', 2); + } + }); + + + dialect.templates.add('insertValues', { + pattern: '({fields}) values {fieldValues}', + validate: function(type, params) { + hasRequiredProp('values', params, 'fields'); + hasArrayProp('values', params, 'fields'); + hasMinPropLength('values', params, 'fields', 1); + + hasRequiredProp('values', params, 'fieldValues'); + hasArrayProp('values', params, 'fieldValues'); + hasMinPropLength('values', params, 'fieldValues', 1); + } + }); + + + dialect.templates.add('joinItem', { + pattern: '{type} join {table} {query} {select} {expression} {alias} {on}', + validate: function(type, params) { + hasOneOfProps('join', params, availableSourceProps); + + if (params.type) { + hasStringProp('join', params, 'type'); + + var splitType = _(params.type.toLowerCase().split(' ')).compact(); + if (_.difference(splitType, availableJoinTypes).length) { + throw new Error('Invalid `type` property value "' + params.type + '" in `join` clause'); + } + } + } + }); + + + dialect.templates.add('withItem', { + pattern: '{name} {fields} as {query} {select} {expression}', + validate: function(type, params) { + hasRequiredProp('with', params, 'name'); + hasOneOfProps('with', params, ['query', 'select', 'expression']); + } + }); + + + dialect.templates.add('fromItem', { + pattern: '{table} {query} {select} {expression}', + validate: function(type, params) { + hasOneOfProps('from', params, availableSourceProps); + } + }); + + + // public templates + + dialect.templates.add('select', { + pattern: '{with} select {distinct} {fields} ' + + 'from {from} {table} {query} {select} {expression} {alias} ' + + '{join} {condition} {group} {sort} {limit} {offset}', + defaults: { + fields: {} + }, + validate: function(type, params) { + hasOneOfProps(type, params, availableSourceProps); + } + }); + + + dialect.templates.add('insert', { + pattern: '{with} insert {or} into {table} {values} {condition} {returning}', + validate: function(type, params) { + hasRequiredProp(type, params, 'values'); + hasObjectProp(type, params, 'values'); + hasOneOfProps(type, params, availableSourceProps); + if (params.or) { + hasStringProp(type, params, 'or'); + matchesRegExpProp(type, params, 'or', orRegExp); + } + } + }); + + + dialect.templates.add('update', { + pattern: '{with} update {or} {table} {modifier} {condition} {returning}', + validate: function(type, params) { + hasRequiredProp(type, params, 'modifier'); + hasRequiredProp(type, params, 'table'); + if (params.or) { + hasStringProp(type, params, 'or'); + matchesRegExpProp(type, params, 'or', orRegExp); + } + } + }); + + + dialect.templates.add('remove', { + pattern: '{with} delete from {table} {condition} {returning}', + validate: function(type, params) { + hasRequiredProp(type, params, 'table'); + } + }); + + + dialect.templates.add('union', dialect.templates.get('queriesUnionCombination')); + + + dialect.templates.add('intersect', dialect.templates.get('queriesCombination')); + + + dialect.templates.add('except', dialect.templates.get('queriesCombination')); + + + // validation helpers + + function hasRequiredProp(type, params, propName) { + if (!params[propName]) { + throw new Error('`' + propName + '` property is not set in `' + type + '` clause'); + } + } + + function hasObjectProp(type, params, propName) { + if (!_.isObject(params[propName])) { + throw new Error('`' + propName + '` property should be an object in `' + type + '` clause'); + } + } + + function hasArrayProp(type, params, propName) { + if (!_.isArray(params[propName])) { + throw new Error('`' + propName + '` property should be an array in `' + type + '` clause'); + } + } + + function hasStringProp(type, params, propName) { + if (!_.isString(params.type)) { + throw new Error('`' + propName + '` property should be a string in `' + type + '` clause'); + } + } + + function hasMinPropLength(type, params, propName, length) { + if (params[propName].length < length) { + throw new Error('`' + propName + '` property should not have length less than ' + length + + ' in `' + type + '` clause'); + } + } + + function hasOneOfProps(type, params, expectedPropNames) { + var propNames = _(params).chain().keys().intersection(expectedPropNames).value(); + + if (!propNames.length) { + throw new Error('Neither `' + expectedPropNames.join('`, `') + + '` properties are not set in `' + type + '` clause'); + } + + if (propNames.length > 1) { + throw new Error('Wrong using `' + propNames.join('`, `') + '` properties together in `' + + type + '` clause'); + } + } + + function matchesRegExpProp(type, params, propName, regExp) { + if (!params[propName].match(regExp)) { + throw new Error('Invalid `' + propName + '` property value "' + params[propName] + '" in `' + + type + '` clause'); + } + } +}; diff --git a/legacy/json-sql/lib/index.js b/legacy/json-sql/lib/index.js new file mode 100644 index 00000000..55bbe02a --- /dev/null +++ b/legacy/json-sql/lib/index.js @@ -0,0 +1,8 @@ +'use strict'; + +var Builder = require('./builder'); + +module.exports = function(params) { + return new Builder(params); +}; +module.exports.Builder = Builder; diff --git a/legacy/json-sql/lib/valuesStore.js b/legacy/json-sql/lib/valuesStore.js new file mode 100644 index 00000000..29b7d395 --- /dev/null +++ b/legacy/json-sql/lib/valuesStore.js @@ -0,0 +1,31 @@ +'use strict'; + +var _ = require('underscore'); + +module.exports = ValuesStore; + +function ValuesStore(options) { + options = options || {}; + this.context = options.context || null; + this._values = options.values || {}; +} + +ValuesStore.prototype.add = ValuesStore.prototype.set = function(name, value) { + if (_.isFunction(value) && this.context) { + value = _(value).bind(this.context); + } + + this._values[name] = value; +}; + +ValuesStore.prototype.get = function(name) { + return this._values[name] || null; +}; + +ValuesStore.prototype.remove = function(name) { + delete this._values[name]; +}; + +ValuesStore.prototype.has = function(name) { + return this._values.hasOwnProperty(name); +}; diff --git a/legacy/json-sql/package.json b/legacy/json-sql/package.json new file mode 100644 index 00000000..73c407e3 --- /dev/null +++ b/legacy/json-sql/package.json @@ -0,0 +1,44 @@ +{ + "name": "json-sql", + "description": "Node.js json to sql query mapper", + "version": "0.2.6", + "author": { + "name": "Artem Zhukov", + "email": "artzhuchka@gmail.com" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/2do2go/json-sql.git" + }, + "keywords": [ + "json-sql", + "json", + "sql", + "odm", + "mapper", + "db", + "database" + ], + "dependencies": { + "underscore": "=1.8.3" + }, + "devDependencies": { + "jshint": "=2.9.2", + "chai": "=3.5.0", + "gulp": "=3.9.1", + "gulp-jshint": "=2.0.0", + "gulp-mocha": "=2.2.0" + }, + "main": "./lib/index", + "readme": "# JSON-SQL\n\nLibrary for mapping mongo-style query objects to SQL queries.\n\n## Quick Start\n\nInstall it with NPM or add it to your package.json:\n\n``` bash\n$ npm install json-sql\n```\n\nThen:\n\n``` js\nvar jsonSql = require('json-sql')();\n\nvar sql = jsonSql.build({\n\ttype: 'select',\n\ttable: 'users',\n\tfields: ['name', 'age'],\n\tcondition: {name: 'Max', id: 6}\n});\n\nsql.query\n// sql string:\n// select name, age from users where name = $p1 && id = 6;\n\nsql.values\n// hash of values:\n// { p1: 'Max' }\n```\n\n## Documentation\n\nDocumentation is available at the [./docs directory](./docs).\n\n## Examples\n\n__Select with join:__\n\n``` js\nvar sql = jsonSql.build({\n\ttype: 'select',\n\ttable: 'users',\n\tjoin: {\n\t\tdocuments: {\n\t\t\ton: {'user.id': 'documents.userId'}\n\t\t}\n\t}\n});\n\nsql.query\n// select * from users join documents on user.id = documents.userId;\n\nsql.values\n// {}\n```\n\n__Insert:__\n\n``` js\nvar sql = jsonSql.build({\n\ttype: 'insert',\n\ttable: 'users',\n\tvalues: {\n\t\tname: 'John',\n\t\tlastname: 'Snow',\n\t\tage: 24,\n\t\tgender: 'male'\n\t}\n});\n\nsql.query\n// insert into users (name, lastname, age, gender) values ($p1, $p2, 24, $p3);\n\nsql.values\n// { p1: 'John', p2: 'Snow', p3: 'male' }\n```\n\n__Update:__\n\n``` js\nvar sql = jsonSql.build({\n\ttype: 'update',\n\ttable: 'users',\n\tcondition: {\n\t\tid: 5\n\t},\n\tmodifier: {\n\t\trole: 'admin'\n\t\tage: 33\n\t}\n});\n\nsql.query\n// update users set role = $p1, age = 33 where id = 5;\n\nsql.values\n// { p1: 'admin' }\n```\n\n__Remove:__\n\n``` js\nvar sql = jsonSql.build({\n\ttype: 'remove',\n\ttable: 'users',\n\tcondition: {\n\t\tid: 5\n\t}\n});\n\nsql.query\n// delete from users where id = 5;\n\nsql.values\n// {}\n```\n\nFor more examples, take a look at the [./docs directory](./docs) or [./tests directory](./tests).\n\n## Tests\n\nClone repository from github, `cd` into cloned dir and install dev dependencies:\n\n``` bash\n$ npm install\n```\n\nThen run tests with command:\n\n``` bash\n$ gulp test\n```\n\nOr run tests coverage with command:\n\n``` bash\n$ gulp coverage\n```\n\n## License\n\n[MIT](./LICENSE)\n", + "readmeFilename": "README.md", + "bugs": { + "url": "https://github.com/2do2go/json-sql/issues" + }, + "homepage": "https://github.com/2do2go/json-sql#readme", + "_id": "json-sql@0.2.4", + "_shasum": "df8c5f345b72f421c6fc7da57116c47cae5b5216", + "_resolved": "https://github.com/LiskHQ/json-sql/tarball/master", + "_from": "https://github.com/LiskHQ/json-sql/tarball/master" +} diff --git a/legacy/json-sql/tests/0_base.js b/legacy/json-sql/tests/0_base.js new file mode 100644 index 00000000..b46774be --- /dev/null +++ b/legacy/json-sql/tests/0_base.js @@ -0,0 +1,304 @@ +'use strict'; + +var jsonSql = require('../lib')(); +var Builder = require('../lib').Builder; +var expect = require('chai').expect; + +describe('Builder', function() { + it('should have fields', function() { + expect(jsonSql).to.be.ok; + expect(jsonSql).to.be.an.instanceof(Builder); + + expect(jsonSql.dialect).to.be.ok; + + expect(jsonSql._query).to.be.equal(''); + expect(jsonSql._values).to.be.eql({}); + + expect(jsonSql.dialect.blocks).to.be.ok; + expect(jsonSql.dialect.templates).to.be.ok; + expect(jsonSql.dialect.conditions).to.be.ok; + expect(jsonSql.dialect.modifiers).to.be.ok; + expect(jsonSql.dialect.logicalOperators).to.be.ok; + }); + + it('should throw error with wrong `type` property', function() { + expect(function() { + jsonSql.build({ + type: 'wrong' + }); + }).to.throw('Unknown template type "wrong"'); + }); + + it('should throw error without `table`, `query` and `select` properties', function() { + expect(function() { + jsonSql.build({}); + }).to.throw('Neither `table`, `query`, `select`, `expression` properties ' + + 'are not set in `select` clause'); + }); + + it('should throw error with both `table` and `select` properties', function() { + expect(function() { + jsonSql.build({ + table: 'users', + select: {table: 'payments'} + }); + }).to.throw('Wrong using `table`, `select` properties together in `select` clause'); + }); + + it('should throw error with both `table` and `query` properties', function() { + expect(function() { + jsonSql.build({ + table: 'users', + query: {table: 'payments'} + }); + }).to.throw('Wrong using `table`, `query` properties together in `select` clause'); + }); + + it('should throw error with both `query` and `select` properties', function() { + expect(function() { + jsonSql.build({ + query: {table: 'payments'}, + select: {table: 'payments'} + }); + }).to.throw('Wrong using `query`, `select` properties together in `select` clause'); + }); + + it('should throw error without `name` property in `with` clause', function() { + expect(function() { + jsonSql.build({ + 'with': [{ + select: { + table: 'payments' + } + }], + table: 'users' + }); + }).to.throw('`name` property is not set in `with` clause'); + }); + + it('should throw error without `query` and `select` properties in `with` clause', function() { + expect(function() { + jsonSql.build({ + 'with': [{ + name: 'payments' + }], + table: 'users' + }); + }).to.throw('Neither `query`, `select`, `expression` properties are not set in `with` clause'); + }); + + it('should throw error with both `query` and `select` properties in `with` clause', function() { + expect(function() { + jsonSql.build({ + 'with': [{ + name: 'payments', + query: {table: 'table1'}, + select: {table: 'table2'} + }], + table: 'users' + }); + }).to.throw('Wrong using `query`, `select` properties together in `with` clause'); + }); + + it('should be ok with array in `with` clause', function() { + var result = jsonSql.build({ + 'with': [{ + name: 'payments', + select: { + table: 'payments' + } + }], + table: 'users' + }); + + expect(result.query).to.be.equal('with "payments" as (select * from "payments") select * from ' + + '"users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object in `with` clause', function() { + var result = jsonSql.build({ + 'with': { + payments: { + select: { + table: 'payments' + } + } + }, + table: 'users' + }); + + expect(result.query).to.be.equal('with "payments" as (select * from "payments") select * from ' + + '"users";'); + expect(result.values).to.be.eql({}); + }); + + it('should create array values with option `namedValues` = false', function() { + jsonSql.configure({ + namedValues: false + }); + + expect(jsonSql._values).to.be.eql([]); + + var result = jsonSql.build({ + table: 'users', + condition: {name: 'John'} + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = ${1};'); + expect(result.values).to.be.eql(['John']); + }); + + it('should use prefix `@` for values with option `valuesPrefix` = @', function() { + jsonSql.configure({ + valuesPrefix: '@' + }); + + var result = jsonSql.build({ + table: 'users', + condition: {name: 'John'} + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = @{p1};'); + expect(result.values).to.be.eql({p1: 'John'}); + }); + + it('should return prefixed values with method `prefixValues`', function() { + var result = jsonSql.build({ + table: 'users', + condition: {name: 'John'} + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = @{p1};'); + expect(result.values).to.be.eql({p1: 'John'}); + expect(result.prefixValues()).to.be.eql({'@{p1}': 'John'}); + }); + + it('should return array values with method `getValuesArray`', function() { + var result = jsonSql.build({ + table: 'users', + condition: {name: 'John'} + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = @{p1};'); + expect(result.values).to.be.eql({p1: 'John'}); + expect(result.getValuesArray()).to.be.eql(['John']); + }); + + it('should return object values with method `getValuesObject`', function() { + jsonSql.configure({ + valuesPrefix: '$', + namedValues: false + }); + + expect(jsonSql._values).to.be.eql([]); + + var result = jsonSql.build({ + table: 'users', + condition: {name: 'John'} + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = ${1};'); + expect(result.values).to.be.eql(['John']); + expect(result.prefixValues()).to.be.eql({'${1}': 'John'}); + expect(result.getValuesObject()).to.be.eql({1: 'John'}); + }); + + it('should create query without values with option `separatedValues` = false', function() { + jsonSql.configure({ + separatedValues: false + }); + + expect(jsonSql._values).to.not.be.ok; + expect(jsonSql._placeholderId).to.not.be.ok; + + var result = jsonSql.build({ + type: 'insert', + table: 'users', + values: {name: 'John', surname: 'Doe'} + }); + + expect(result.query).to.be.equal('insert into "users" ("name", "surname") values ' + + '(\'John\', \'Doe\');'); + expect(result.values).to.not.be.ok; + }); + + it('should create query without wrapping identifiers with option `wrappedIdentifiers` = false', + function() { + jsonSql.configure({ + wrappedIdentifiers: false + }); + + var result = jsonSql.build({ + type: 'insert', + table: 'users', + values: {name: 'John'} + }); + + expect(result.query).to.be.equal('insert into users (name) values (${p1});'); + } + ); + + it('shouldn\'t wrap identifiers that already wrapped', function() { + jsonSql.configure({ + wrappedIdentifiers: true + }); + + var result = jsonSql.build({ + type: 'insert', + table: '"users"', + values: { + '"name"': 'John', + '"users"."age"': 22 + } + }); + + expect(result.query).to.be.equal('insert into "users" ("name", "users"."age") values (${p1}, 22);'); + }); + + it('shouldn\'t split identifiers by dots inside quotes', function() { + jsonSql.configure({ + wrappedIdentifiers: true + }); + + var result = jsonSql.build({ + type: 'insert', + table: '"users"', + values: { + '"users.age"': 22 + } + }); + + expect(result.query).to.be.equal('insert into "users" ("users.age") values (22);'); + }); + + it('shouldn\'t wrap asterisk identifier parts', function() { + jsonSql.configure({ + wrappedIdentifiers: true + }); + + var result = jsonSql.build({ + fields: ['users.*'], + table: '"users"' + }); + + expect(result.query).to.be.equal('select "users".* from "users";'); + }); + + it('should split identifiers by dots and wrap each part', function() { + jsonSql.configure({ + wrappedIdentifiers: true + }); + + var result = jsonSql.build({ + type: 'insert', + table: '"users"', + values: { + 'name': 'John', + 'users.age': 22 + } + }); + + expect(result.query).to.be.equal('insert into "users" ("name", "users"."age") values (${p1}, 22);'); + }); +}); diff --git a/legacy/json-sql/tests/1_select.js b/legacy/json-sql/tests/1_select.js new file mode 100644 index 00000000..087a1afb --- /dev/null +++ b/legacy/json-sql/tests/1_select.js @@ -0,0 +1,1222 @@ +'use strict'; + +var jsonSql = require('../lib')(); +var expect = require('chai').expect; + +describe('Select', function() { + describe('type', function() { + it('should be ok without `type` property', function() { + var result = jsonSql.build({ + table: 'users' + }); + + expect(result.query).to.be.equal('select * from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with "select" value', function() { + var result = jsonSql.build({ + type: 'select', + table: 'users' + }); + + expect(result.query).to.be.equal('select * from "users";'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('distinct', function() { + it('should be ok with true value', function() { + var result = jsonSql.build({ + table: 'users', + distinct: true + }); + + expect(result.query).to.be.equal('select distinct * from "users";'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('fields', function() { + it('should be ok with string array', function() { + var result = jsonSql.build({ + table: 'users', + fields: ['name', 'type'] + }); + + expect(result.query).to.be.equal('select "name", "type" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`name`: `alias`, ...)', function() { + var result = jsonSql.build({ + table: 'users', + fields: {userAge: 'age', userScore: 'score'} + }); + + expect(result.query).to.be.equal('select "userAge" as "age", "userScore" as "score" from ' + + '"users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with array of objects(`name`: `alias`, ...)', function() { + var result = jsonSql.build({ + table: 'users', + fields: [{userAge: 'age'}] + }); + + expect(result.query).to.be.equal('select "userAge" as "age" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`field`) array', function() { + var result = jsonSql.build({ + table: 'users', + fields: [{field: 'address'}] + }); + + expect(result.query).to.be.equal('select "address" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`field`, `table`) array', function() { + var result = jsonSql.build({ + table: 'users', + fields: [{field: 'score', table: 'users'}] + }); + + expect(result.query).to.be.equal('select "users"."score" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`field`, `alias`) array', function() { + var result = jsonSql.build({ + table: 'users', + fields: [{field: 'zoneName', alias: 'zone'}] + }); + + expect(result.query).to.be.equal('select "zoneName" as "zone" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`field`, `table`, `alias`) array', function() { + var result = jsonSql.build({ + table: 'users', + fields: [{field: 'zoneName', table: 'users', alias: 'zone'}] + }); + + expect(result.query).to.be.equal('select "users"."zoneName" as "zone" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`table`)', function() { + var result = jsonSql.build({ + table: 'users', + fields: {score: {table: 'users'}} + }); + + expect(result.query).to.be.equal('select "users"."score" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`field`, `alias`)', function() { + var result = jsonSql.build({ + table: 'users', + fields: {zone: {field: 'zone_1', alias: 'zone'}} + }); + + expect(result.query).to.be.equal('select "zone_1" as "zone" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`table`, `alias`)', function() { + var result = jsonSql.build({ + table: 'users', + fields: {score: {table: 'users', alias: 's'}} + }); + + expect(result.query).to.be.equal('select "users"."score" as "s" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`field`, `table`, `alias`)', function() { + var result = jsonSql.build({ + table: 'users', + fields: {name: {field: 'name_1', table: 'users', alias: 'name_2'}} + }); + + expect(result.query).to.be.equal('select "users"."name_1" as "name_2" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`expression`)', function() { + var result = jsonSql.build({ + table: 'users', + fields: [{ + expression: 'count(*)' + }] + }); + + expect(result.query).to.be.equal('select count(*) from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`expression`, `alias`)', function() { + var result = jsonSql.build({ + table: 'users', + fields: [{ + expression: 'count(*)', + alias: 'count' + }] + }); + + expect(result.query).to.be.equal('select count(*) as "count" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`expression`, `field`, `alias`)', function() { + var result = jsonSql.build({ + table: 'users', + fields: [{ + expression: 'sum', + field: 'income', + alias: 'sum' + }] + }); + + expect(result.query).to.be.equal('select sum("income") as "sum" from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object(`expression`[], `field`, `alias`)', function() { + var result = jsonSql.build({ + table: 'users', + fields: [{ + expression: ['abs', 'sum'], + field: 'income', + alias: 'sum' + }] + }); + + expect(result.query).to.be.equal('select abs(sum("income")) as "sum" from "users";'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('alias', function() { + it('should be ok with string `alias` property', function() { + var result = jsonSql.build({ + table: 'users', + alias: 'u' + }); + + expect(result.query).to.be.equal('select * from "users" as "u";'); + expect(result.values).to.be.eql({}); + }); + + it('should throw error if object `alias` does not have `name` property', function() { + expect(function() { + jsonSql.build({ + table: 'users', + alias: {} + }); + }).to.throw('Alias `name` property is required'); + }); + + it('should be ok with object `alias`(`name`) property', function() { + var result = jsonSql.build({ + table: 'users', + alias: { + name: 'u' + } + }); + + expect(result.query).to.be.equal('select * from "users" as "u";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object `alias`(`name`, `columns`) property', function() { + var result = jsonSql.build({ + table: 'users', + alias: { + name: 'u', + columns: ['a', 'b'] + } + }); + + expect(result.query).to.be.equal('select * from "users" as "u"("a", "b");'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('query', function() { + it('should be ok with `query` property', function() { + var result = jsonSql.build({ + query: { + type: 'select', + table: 't' + } + }); + + expect(result.query).to.be.equal('select * from (select * from "t");'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('select', function() { + it('should be ok with `select` property', function() { + var result = jsonSql.build({ + select: { + table: 't' + } + }); + + expect(result.query).to.be.equal('select * from (select * from "t");'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('expression', function() { + it('should be ok with `expression` property', function() { + var result = jsonSql.build({ + expression: 'function()' + }); + + expect(result.query).to.be.equal('select * from function();'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('join', function() { + it('should throw error without `table`, `query` and `select` properties', + function() { + expect(function() { + jsonSql.build({ + table: 'users', + join: [{}] + }); + }).to.throw('Neither `table`, `query`, `select`, `expression` properties ' + + 'are not set in `join` clause'); + } + ); + + it('should throw error with both `table` and `select` properties', function() { + expect(function() { + jsonSql.build({ + table: 'users', + join: [{ + table: 'a', + select: {table: 'b'} + }] + }); + }).to.throw('Wrong using `table`, `select` properties together in `join` clause'); + }); + + it('should throw error with both `table` and `query` properties', function() { + expect(function() { + jsonSql.build({ + table: 'users', + join: [{ + table: 'a', + query: {table: 'b'} + }] + }); + }).to.throw('Wrong using `table`, `query` properties together in `join` clause'); + }); + + it('should throw error with both `query` and `select` properties', function() { + expect(function() { + jsonSql.build({ + table: 'users', + join: [{ + query: 'a', + select: {table: 'b'} + }] + }); + }).to.throw('Wrong using `query`, `select` properties together in `join` clause'); + }); + + it('should throw error with wrong `type` property', function() { + expect(function() { + jsonSql.build({ + table: 'users', + join: [{ + type: 'wrong', + table: 'payments' + }] + }); + }).to.throw('Invalid `type` property value "wrong" in `join` clause'); + }); + + it('should be ok with correct `type` property', function() { + var result = jsonSql.build({ + table: 'users', + join: [{ + type: 'left outer', + table: 'payments' + }] + }); + + expect(result.query).to.be.equal('select * from "users" left outer join "payments";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with array `join`', function() { + var result = jsonSql.build({ + table: 'users', + join: [{ + table: 'payments' + }] + }); + + expect(result.query).to.be.equal('select * from "users" join "payments";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object `join`', function() { + var result = jsonSql.build({ + table: 'users', + join: { + payments: {} + } + }); + + expect(result.query).to.be.equal('select * from "users" join "payments";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `on` property', function() { + var result = jsonSql.build({ + table: 'users', + join: { + payments: { + on: {'users.name': 'payments.name'} + } + } + }); + + expect(result.query).to.be.equal('select * from "users" join "payments" on "users"."name" = ' + + '"payments"."name";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `query` property', function() { + var result = jsonSql.build({ + table: 'users', + join: [{ + query: { + table: 'payments' + }, + on: {'users.name': 'payments.name'} + }] + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'join (select * from "payments") ' + + 'on "users"."name" = "payments"."name";' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `select` property', function() { + var result = jsonSql.build({ + table: 'users', + join: [{ + select: { + table: 'payments' + }, + on: {'users.name': 'payments.name'} + }] + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'join (select * from "payments") ' + + 'on "users"."name" = "payments"."name";' + ); + expect(result.values).to.be.eql({}); + }); + }); + + describe('condition', function() { + describe('compare operators', function() { + it('should throw error with wrong operator', function() { + expect(function() { + jsonSql.build({ + table: 'users', + condition: { + name: {$wrong: 'John'} + } + }); + }).to.throw('Unknown operator "$wrong"'); + }); + + it('should be ok with default operator(=)', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: 'John' + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = ${p1};'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with `$eq` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$eq: 'John'} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = ${p1};'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with `$ne` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$ne: 'John'} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" != ${p1};'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with `$gt` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$gt: 'John'} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" > ${p1};'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with `$lt` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$lt: 'John'} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" < ${p1};'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with `$gte` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$gte: 'John'} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" >= ${p1};'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with `$lte` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$lte: 'John'} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" <= ${p1};'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with `$is` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$is: null} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" is null;'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `$isnot` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$isnot: null} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" is not null;'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `$like` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$like: 'John%'} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" like ${p1};'); + expect(result.values).to.be.eql({ + p1: 'John%' + }); + }); + + it('should be ok with `$null`:true operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$null: true} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" is null;'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `$null`:false operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$null: false} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" is not null;'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `$field` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$field: 'name_2'} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = "name_2";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object `$field` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: {$field: {field: 'name_2'}} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = "name_2";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `$in` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + age: {$in: [12, 13, 14]} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "age" in (12, 13, 14);'); + expect(result.values).to.be.eql({}); + }); + + it('should add `null` value with empty array in `$in` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + age: {$in: []} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "age" in (null);'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `$nin` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + age: {$nin: [12, 13, 14]} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "age" not in (12, 13, 14);'); + expect(result.values).to.be.eql({}); + }); + + it('should add `null` value with empty array in `$nin` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + age: {$nin: []} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "age" not in (null);'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object subquery in `$in` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + age: {$in: { + table: 'test' + }} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "age" in (select * from ' + + '"test");'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `query` subquery in `$in` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + age: {$in: { + query: { + table: 'test' + } + }} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "age" in (select * from ' + + '(select * from "test"));'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `select` subquery in `$in` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + age: {$in: { + select: { + table: 'test' + } + }} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "age" in (select * from ' + + '(select * from "test"));'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `$between` operator', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + age: {$between: [12, 14]} + } + }); + + expect(result.query).to.be.equal('select * from "users" where "age" between 12 and 14;'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('logical operators', function() { + it('should throw error with wrong logical operator', function() { + expect(function() { + jsonSql.build({ + table: 'users', + condition: { + $wrong: [ + {name: 'John'}, + {age: 12} + ] + } + }); + }).to.throw('Unknown logical operator "$wrong"'); + }); + + it('should be ok with default logical operator(`$and`)', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + name: 'John', + age: 12 + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = ${p1} and "age" = 12;'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with default logical operator(`$and`) for one field', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + age: { + $gt: 5, + $lt: 15, + $ne: 10 + } + } + }); + + expect(result.query).to.be.equal('select * from "users" where "age" > 5 and "age" < 15 and ' + + '"age" != 10;'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with array `$and`', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $and: [ + {name: 'John'}, + {age: 12} + ] + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = ${p1} and "age" = 12;'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with object `$and`', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $and: { + name: 'John', + age: 12 + } + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = ${p1} and "age" = 12;'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with array `$or`', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $or: [ + {name: 'John'}, + {age: 12} + ] + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = ${p1} or "age" = 12;'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with object `$or`', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $or: { + name: 'John', + age: 12 + } + } + }); + + expect(result.query).to.be.equal('select * from "users" where "name" = ${p1} or "age" = 12;'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with array `$not`', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $not: [ + {name: 'John'}, + {age: 12} + ] + } + }); + + expect(result.query).to.be.equal('select * from "users" where not ("name" = ${p1} and ' + + '"age" = 12);'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with object `$not`', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $not: { + name: 'John', + age: 12 + } + } + }); + + expect(result.query).to.be.equal('select * from "users" where not ("name" = ${p1} and ' + + '"age" = 12);'); + expect(result.values).to.be.eql({ + p1: 'John' + }); + }); + + it('should be ok with object [`$or`, `$or`]', function() { + var result = jsonSql.build({ + table: 'users', + condition: [{ + $or: { + name: 'John', + age: 12 + } + }, { + $or: { + name: 'Mark', + age: 14 + } + }] + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'where ("name" = ${p1} or "age" = 12) and ' + + '("name" = ${p2} or "age" = 14);' + ); + expect(result.values).to.be.eql({ + p1: 'John', + p2: 'Mark' + }); + }); + + it('should be ok with object `$and`:[`$or`, `$or`]', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $and: [{ + $or: { + name: 'John', + age: 12 + } + }, { + $or: { + name: 'Mark', + age: 14 + } + }] + } + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'where ("name" = ${p1} or "age" = 12) and ' + + '("name" = ${p2} or "age" = 14);' + ); + expect(result.values).to.be.eql({ + p1: 'John', + p2: 'Mark' + }); + }); + + it('should be ok with object `$or`:[{},{}]', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $or: [{ + name: 'John', + age: 12 + }, { + name: 'Mark', + age: 14 + }] + } + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'where ("name" = ${p1} and "age" = 12) or ' + + '("name" = ${p2} and "age" = 14);' + ); + expect(result.values).to.be.eql({ + p1: 'John', + p2: 'Mark' + }); + }); + + it('should be ok with object `$or`:[`$and`, `$and`]', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $or: [{ + $and: { + name: 'John', + age: 12 + } + }, { + $and: { + name: 'Mark', + age: 14 + } + }] + } + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'where ("name" = ${p1} and "age" = 12) or ' + + '("name" = ${p2} and "age" = 14);' + ); + expect(result.values).to.be.eql({ + p1: 'John', + p2: 'Mark' + }); + }); + + it('should be ok with [{}, {}]', function() { + var result = jsonSql.build({ + table: 'users', + condition: [{ + name: 'John', + age: 12 + }, { + name: 'Mark', + age: 14 + }] + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'where ("name" = ${p1} and "age" = 12) and ' + + '("name" = ${p2} and "age" = 14);'); + expect(result.values).to.be.eql({ + p1: 'John', + p2: 'Mark' + }); + }); + + it('should be ok with `$and`:[{}, {}]', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $and: [{ + name: 'John', + age: 12 + }, { + name: 'Mark', + age: 14 + }] + } + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'where ("name" = ${p1} and "age" = 12) and ' + + '("name" = ${p2} and "age" = 14);' + ); + expect(result.values).to.be.eql({ + p1: 'John', + p2: 'Mark' + }); + }); + + it('should be ok with `$and`:[`$and`, `$and`]', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $and: [{ + $and: { + name: 'John', + age: 12 + } + }, { + $and: { + name: 'Mark', + age: 14 + } + }] + } + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'where ("name" = ${p1} and "age" = 12) and ' + + '("name" = ${p2} and "age" = 14);' + ); + expect(result.values).to.be.eql({ + p1: 'John', + p2: 'Mark' + }); + }); + + it('should be ok with `$or`:[`$or`, `$or`]', function() { + var result = jsonSql.build({ + table: 'users', + condition: { + $or: [{ + $or: { + name: 'John', + age: 12 + } + }, { + $or: { + name: 'Mark', + age: 14 + } + }] + } + }); + + expect(result.query).to.be.equal( + 'select * from "users" ' + + 'where ("name" = ${p1} or "age" = 12) or ' + + '("name" = ${p2} or "age" = 14);' + ); + expect(result.values).to.be.eql({ + p1: 'John', + p2: 'Mark' + }); + }); + }); + }); + + describe('group', function() { + it('should be ok with string value', function() { + var result = jsonSql.build({ + table: 'users', + group: 'age' + }); + + expect(result.query).to.be.equal( + 'select * from "users" group by "age";' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with array value', function() { + var result = jsonSql.build({ + table: 'users', + group: ['age', 'gender'] + }); + + expect(result.query).to.be.equal( + 'select * from "users" group by "age", "gender";' + ); + expect(result.values).to.be.eql({}); + }); + }); + + describe('sort', function() { + it('should be ok with string value', function() { + var result = jsonSql.build({ + table: 'users', + sort: 'age' + }); + + expect(result.query).to.be.equal( + 'select * from "users" order by "age";' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with array value', function() { + var result = jsonSql.build({ + table: 'users', + sort: ['age', 'gender'] + }); + + expect(result.query).to.be.equal( + 'select * from "users" order by "age", "gender";' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object value', function() { + var result = jsonSql.build({ + table: 'users', + sort: { + age: 1, + gender: -1 + } + }); + + expect(result.query).to.be.equal( + 'select * from "users" order by "age" asc, "gender" desc;' + ); + expect(result.values).to.be.eql({}); + }); + }); + + describe('limit, offset', function() { + it('should be ok with `limit` property', function() { + var result = jsonSql.build({ + table: 'users', + limit: 5 + }); + + expect(result.query).to.be.equal( + 'select * from "users" limit 5;' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `offset` property', function() { + var result = jsonSql.build({ + table: 'users', + offset: 5 + }); + + expect(result.query).to.be.equal( + 'select * from "users" offset 5;' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `limit` and `offset` properties', function() { + var result = jsonSql.build({ + table: 'users', + limit: 10, + offset: 20 + }); + + expect(result.query).to.be.equal( + 'select * from "users" limit 10 offset 20;' + ); + expect(result.values).to.be.eql({}); + }); + }); +}); diff --git a/legacy/json-sql/tests/2_insert.js b/legacy/json-sql/tests/2_insert.js new file mode 100644 index 00000000..fe3f1b9d --- /dev/null +++ b/legacy/json-sql/tests/2_insert.js @@ -0,0 +1,73 @@ +'use strict'; + +var jsonSql = require('../lib')(); +var expect = require('chai').expect; + +describe('Insert', function() { + it('should throw error without `values` property', function() { + expect(function() { + jsonSql.build({ + type: 'insert', + table: 'users' + }); + }).to.throw('`values` property is not set in `insert` clause'); + }); + + it('should be ok with `values` property', function() { + var result = jsonSql.build({ + type: 'insert', + table: 'users', + values: { + name: 'Max' + } + }); + + expect(result.query).to.be.equal('insert into "users" ("name") values (${p1});'); + expect(result.values).to.be.eql({p1: 'Max'}); + }); + + it('should be ok with `with` property', function() { + var result = jsonSql.build({ + 'with': [{ + name: 't_1', + select: { + table: 't_1' + } + }], + type: 'insert', + table: 'users', + values: { + name: 'Max', + age: 17, + lastVisit: null, + active: true + } + }); + + expect(result.query).to.be.equal( + 'with "t_1" as (select * from "t_1") insert into "users" ' + + '("name", "age", "lastVisit", "active") values (${p1}, 17, null, true);' + ); + expect(result.values).to.be.eql({p1: 'Max'}); + }); + + it('should be ok with `returning` property', function() { + var result = jsonSql.build({ + type: 'insert', + table: 'users', + values: { + name: 'Max', + age: 17, + lastVisit: null, + active: true + }, + returning: ['users.*'] + }); + + expect(result.query).to.be.equal( + 'insert into "users" ("name", "age", "lastVisit", "active") ' + + 'values (${p1}, 17, null, true) returning "users".*;' + ); + expect(result.values).to.be.eql({p1: 'Max'}); + }); +}); diff --git a/legacy/json-sql/tests/3_update.js b/legacy/json-sql/tests/3_update.js new file mode 100644 index 00000000..688b4c1d --- /dev/null +++ b/legacy/json-sql/tests/3_update.js @@ -0,0 +1,123 @@ +'use strict'; + +var jsonSql = require('../lib')(); +var expect = require('chai').expect; + +describe('Update', function() { + describe('modifier', function() { + it('should throw error without `modifier` property', function() { + expect(function() { + jsonSql.build({ + type: 'update', + table: 'users' + }); + }).to.throw('`modifier` property is not set in `update` clause'); + }); + + it('should be ok with default(`$set`)', function() { + var result = jsonSql.build({ + type: 'update', + table: 'users', + modifier: { + name: 'Max', + age: 16, + lastVisit: null, + active: false + } + }); + + expect(result.query).to.be.equal('update "users" set "name" = ${p1}, "age" = 16, ' + + '"lastVisit" = null, "active" = false;'); + expect(result.values).to.be.eql({p1: 'Max'}); + }); + + it('should be ok with `$set`', function() { + var result = jsonSql.build({ + type: 'update', + table: 'users', + modifier: { + $set: { + name: 'Max' + } + } + }); + + expect(result.query).to.be.equal('update "users" set "name" = ${p1};'); + expect(result.values).to.be.eql({p1: 'Max'}); + }); + + it('should be ok with `$inc`', function() { + var result = jsonSql.build({ + type: 'update', + table: 'users', + modifier: { + $inc: { + age: 4 + } + } + }); + + expect(result.query).to.be.equal('update "users" set "age" = "age" + 4;'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `$dec`', function() { + var result = jsonSql.build({ + type: 'update', + table: 'users', + modifier: { + $dec: { + age: 2 + } + } + }); + + expect(result.query).to.be.equal('update "users" set "age" = "age" - 2;'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('with', function() { + it('should be ok', function() { + var result = jsonSql.build({ + 'with': [{ + name: 't_1', + select: { + table: 't_1' + } + }], + type: 'update', + table: 'users', + modifier: { + $dec: { + age: 3 + } + } + }); + + expect(result.query).to.be.equal('with "t_1" as (select * from "t_1") update "users" ' + + 'set "age" = "age" - 3;'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('returning', function() { + it('should be ok', function() { + var result = jsonSql.build({ + type: 'update', + table: 'users', + modifier: { + $dec: { + age: 3 + } + }, + returning: ['users.*'] + }); + + expect(result.query).to.be.equal( + 'update "users" set "age" = "age" - 3 returning "users".*;' + ); + expect(result.values).to.be.eql({}); + }); + }); +}); diff --git a/legacy/json-sql/tests/4_delete.js b/legacy/json-sql/tests/4_delete.js new file mode 100644 index 00000000..c90f19f2 --- /dev/null +++ b/legacy/json-sql/tests/4_delete.js @@ -0,0 +1,58 @@ +'use strict'; + +var jsonSql = require('../lib')(); +var expect = require('chai').expect; + +describe('Delete', function() { + it('should be ok without `condition` property', function() { + var result = jsonSql.build({ + type: 'remove', + table: 'users' + }); + + expect(result.query).to.be.equal('delete from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `condition` property', function() { + var result = jsonSql.build({ + type: 'remove', + table: 'users', + condition: { + a: 5 + } + }); + + expect(result.query).to.be.equal('delete from "users" where "a" = 5;'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `with` property', function() { + var result = jsonSql.build({ + 'with': [{ + name: 't_1', + select: { + table: 't_1' + } + }], + type: 'remove', + table: 'users' + }); + + expect(result.query).to.be.equal('with "t_1" as (select * from "t_1") delete from "users";'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `returning` property', function() { + var result = jsonSql.build({ + type: 'remove', + table: 'users', + returning: ['users.*'] + }); + + expect(result.query).to.be.equal( + 'delete from "users" returning "users".*;' + ); + expect(result.values).to.be.eql({}); + }); +}); diff --git a/legacy/json-sql/tests/5_union.js b/legacy/json-sql/tests/5_union.js new file mode 100644 index 00000000..a1e79fcf --- /dev/null +++ b/legacy/json-sql/tests/5_union.js @@ -0,0 +1,256 @@ +'use strict'; + +var jsonSql = require('../lib')(); +var expect = require('chai').expect; + +describe('Union, except, intersect', function() { + describe('queries', function() { + it('should throw error without `queries` property', function() { + expect(function() { + jsonSql.build({ + type: 'union' + }); + }).to.throw('`queries` property is not set in `union` clause'); + }); + + it('should throw error with non-array value', function() { + expect(function() { + jsonSql.build({ + type: 'union', + queries: 'wrong' + }); + }).to.throw('`queries` property should be an array in `union` clause'); + }); + + it('should throw error with value length < 2', function() { + expect(function() { + jsonSql.build({ + type: 'union', + queries: [{ + table: 'users' + }] + }); + }).to.throw('`queries` property should not have length less than 2 in `union` clause'); + }); + + it('should be ok with value length = 2', function() { + var result = jsonSql.build({ + type: 'union', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }] + }); + + expect(result.query).to.be.equal('(select * from "users") union (select * from "vipUsers");'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('type & all combinations', function() { + it('should be ok with `type` = "union", `all` = true', function() { + var result = jsonSql.build({ + type: 'union', + all: true, + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }] + }); + + expect(result.query).to.be.equal('(select * from "users") union all (select * from ' + + '"vipUsers");'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `type` = "except"', function() { + var result = jsonSql.build({ + type: 'except', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }] + }); + + expect(result.query).to.be.equal('(select * from "users") except (select * from "vipUsers");'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `type` = "except", `all` = true', function() { + var result = jsonSql.build({ + type: 'except', + all: true, + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }] + }); + + expect(result.query).to.be.equal('(select * from "users") except all (select * from ' + + '"vipUsers");'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `type` = "intersect"', function() { + var result = jsonSql.build({ + type: 'intersect', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }] + }); + + expect(result.query).to.be.equal('(select * from "users") intersect (select * from ' + + '"vipUsers");'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `type` = "intersect", `all` = true', function() { + var result = jsonSql.build({ + type: 'intersect', + all: true, + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }] + }); + + expect(result.query).to.be.equal('(select * from "users") intersect all (select * from ' + + '"vipUsers");'); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `type` = "union" subquery', function() { + var result = jsonSql.build({ + query: { + type: 'union', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }] + } + }); + + expect(result.query).to.be.equal('select * from ((select * from "users") union (select * ' + + 'from "vipUsers"));'); + expect(result.values).to.be.eql({}); + }); + }); + + describe('sort', function() { + it('should be ok with string value', function() { + var result = jsonSql.build({ + type: 'union', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }], + sort: 'age' + }); + + expect(result.query).to.be.equal( + '(select * from "users") union (select * from "vipUsers") order by "age";' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with array value', function() { + var result = jsonSql.build({ + type: 'union', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }], + sort: ['age', 'gender'] + }); + + expect(result.query).to.be.equal( + '(select * from "users") union (select * from "vipUsers") order by "age", "gender";' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with object value', function() { + var result = jsonSql.build({ + type: 'union', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }], + sort: { + age: 1, + gender: -1 + } + }); + + expect(result.query).to.be.equal( + '(select * from "users") union (select * from "vipUsers") order by "age" asc, "gender" desc;' + ); + expect(result.values).to.be.eql({}); + }); + }); + + describe('limit, offset', function() { + it('should be ok with `limit` property', function() { + var result = jsonSql.build({ + type: 'union', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }], + limit: 5 + }); + + expect(result.query).to.be.equal( + '(select * from "users") union (select * from "vipUsers") limit 5;' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `offset` property', function() { + var result = jsonSql.build({ + type: 'union', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }], + offset: 5 + }); + + expect(result.query).to.be.equal( + '(select * from "users") union (select * from "vipUsers") offset 5;' + ); + expect(result.values).to.be.eql({}); + }); + + it('should be ok with `limit` and `offset` properties', function() { + var result = jsonSql.build({ + type: 'union', + queries: [{ + table: 'users' + }, { + table: 'vipUsers' + }], + limit: 10, + offset: 20 + }); + + expect(result.query).to.be.equal( + '(select * from "users") union (select * from "vipUsers") limit 10 offset 20;' + ); + expect(result.values).to.be.eql({}); + }); + }); +}); diff --git a/legacy/json-sql/tests/6_postgresDialect.js b/legacy/json-sql/tests/6_postgresDialect.js new file mode 100644 index 00000000..790c0ec2 --- /dev/null +++ b/legacy/json-sql/tests/6_postgresDialect.js @@ -0,0 +1,140 @@ +'use strict'; + +var jsonSql = require('../lib')({ + dialect: 'postgresql', + namedValues: false +}); +var expect = require('chai').expect; + +describe('PostgreSQL dialect', function() { + describe('json', function() { + it('should correctly wrap each part of json path', function() { + var result = jsonSql.build({ + table: 'test', + fields: ['params->a->>b'], + condition: { + 'params->>c': {$like: '7%'} + } + }); + + expect(result.query).to.be.equal( + 'select "params"->\'a\'->>\'b\' from "test" ' + + 'where "params"->>\'c\' like ${1};' + ); + }); + + it('should be ok with `$jsonContains` conditional operator', function() { + var result = jsonSql.build({ + table: 'test', + condition: { + 'params->a': { + $jsonContains: {b: 1} + } + } + }); + + expect(result.query).to.be.equal( + 'select * from "test" where "params"->\'a\' @> ${1};' + ); + expect(result.values).to.be.eql(['{"b":1}']); + }); + + it('should be ok with `$jsonIn` conditional operator', function() { + var result = jsonSql.build({ + table: 'test', + condition: { + 'params->a': { + $jsonIn: {$field: 'data->b'} + } + } + }); + + expect(result.query).to.be.equal( + 'select * from "test" where "params"->\'a\' <@ "data"->\'b\';' + ); + expect(result.values).to.be.eql([]); + }); + + it('should be ok with `$jsonHas` conditional operator', function() { + var result = jsonSql.build({ + table: 'test', + condition: { + params: {$jsonHas: 'account'} + } + }); + + expect(result.query).to.be.equal('select * from "test" where "params" ? ${1};'); + expect(result.values).to.be.eql(['account']); + }); + + it('should be ok with `$jsonHasAny` conditional operator', function() { + var result = jsonSql.build({ + table: 'test', + condition: { + params: {$jsonHasAny: ['a', 'b']} + } + }); + + expect(result.query).to.be.equal( + 'select * from "test" where "params" ?| array[${1}, ${2}];' + ); + expect(result.values).to.be.eql(['a', 'b']); + }); + + it('should be ok with `$jsonHasAll` conditional operator', function() { + var result = jsonSql.build({ + table: 'test', + condition: { + params: {$jsonHasAll: ['a', 'b']} + } + }); + + expect(result.query).to.be.equal( + 'select * from "test" where "params" ?& array[${1}, ${2}];' + ); + expect(result.values).to.be.eql(['a', 'b']); + }); + + it('should be ok with `$upper` conditional operator', function() { + var result = jsonSql.build({ + table: 'test', + condition: { + params: {$upper: ['params', '3498862814541110459l']} + } + }); + + expect(result.query).to.be.equal( + 'select * from "test" where upper("params") = upper(${1});' + ); + expect(result.values).to.be.eql(['3498862814541110459l']); + }); + + it('should be ok with `$lower` conditional operator', function() { + var result = jsonSql.build({ + table: 'test', + condition: { + params: {$lower: ['params', '3498862814541110459L']} + } + }); + + expect(result.query).to.be.equal( + 'select * from "test" where lower("params") = lower(${1});' + ); + expect(result.values).to.be.eql(['3498862814541110459L']); + }); + + it('should be ok with `$decode` conditional operator', function() { + var result = jsonSql.build({ + table: 'test', + condition: { + params: {$decode: ['params', '3498862814541110459L', 'hex']} + } + }); + + expect(result.query).to.be.equal( + 'select * from "test" where "params" = decode(${1}, ${2});' + ); + expect(result.values).to.be.eql(['3498862814541110459L', 'hex']); + }); + }); +}); diff --git a/legacy/json-sql/tests/7_create.js b/legacy/json-sql/tests/7_create.js new file mode 100644 index 00000000..a93ca702 --- /dev/null +++ b/legacy/json-sql/tests/7_create.js @@ -0,0 +1,279 @@ +'use strict'; + +var jsonSql = require('../lib')(); +var expect = require('chai').expect; + +describe('Create', function () { + it('should throw error without `tableFields` property', function () { + expect(function () { + jsonSql.build({ + type: 'create', + table: 'users' + }); + }).to.throw('`tableFields` property is not set in `create` clause'); + }); + + it('should throw error with incorrect field type', function () { + expect(function () { + jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "age", + type: "NotNumber" + } + ] + }); + }).to.throw('Invalid type of field: NotNumber'); + }); + + it('should be ok with `tableFields` property', function () { + var result = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "age", + type: "Number" + } + ] + }); + + + expect(result.query).to.be.equal('create table if not exists "users"("age" int);'); + }); + + it('should throw error when length property for string field not provided', function () { + expect(function () { + jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String" + } + ] + }); + }).to.throw('Field length can\'t be less or equal 0'); + }); + + it('should throw error with empty name', function () { + expect(function () { + jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: " ", + type: "String" + } + ] + }); + }).to.throw('Name most contains characters'); + }); + + it('should be ok with string field and length', function () { + var result = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16 + } + ] + }); + + expect(result.query).to.be.equal('create table if not exists "users"("name" varchar(16));'); + }); + + it('should be ok with string field not null', function () { + var result = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true + } + ] + }); + + expect(result.query).to.be.equal('create table if not exists "users"("name" varchar(16) NOT NULL);'); + }); + + it('should be ok with string field not null primary key', function () { + var result = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true, + primary_key: true + } + ] + }); + + expect(result.query).to.be.equal('create table if not exists "users"("name" varchar(16) NOT NULL PRIMARY KEY);'); + }); + + + it('should be ok with string field not null unique', function () { + var result = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true, + unique: true + } + ] + }); + + expect(result.query).to.be.equal('create table if not exists "users"("name" varchar(16) NOT NULL UNIQUE);'); + }); + + + it('should be allow only one primary key field', function () { + expect(function () { + jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true, + primary_key: true + }, + { + name: "secondname", + type: "String", + length: 16, + not_null: true, + primary_key: true + } + ] + }) + }).to.throw("Too much primary key 'secondname' in table"); + }); + + it('should be allow only unique field name', function () { + expect(function () { + jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true, + primary_key: true + }, + { + name: "name", + type: "String", + length: 16, + not_null: true + } + ] + }) + }).to.throw("Two parameters with same name: name"); + }); + + it("should allow few fields", function () { + var result = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true, + primary_key: true + }, + { + name: "age", + type: "Number", + not_null: true + } + ] + }); + + expect(result.query).to.be.equal('create table if not exists "users"("name" varchar(16) NOT NULL PRIMARY KEY,"age" int NOT NULL);'); + }); + + it("should allow few fields", function () { + var result = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true, + primary_key: true + }, + { + name: "age", + type: "Number", + not_null: true + } + ], + foreignKeys: [ + { + field: "name", + table: "person", + table_field: "id" + } + ] + }); + + expect(result.query).to.be.equal('create table if not exists "users"("name" varchar(16) NOT NULL PRIMARY KEY,"age" int NOT NULL, FOREIGN KEY ("name") REFERENCES person("id"));'); + }); + + it("should allow few fields", function () { + var result = jsonSql.build({ + type: 'create', + table: 'users', + tableFields: [ + { + name: "name", + type: "String", + length: 16, + not_null: true, + primary_key: true + }, + { + name: "age", + type: "Number", + not_null: true + } + ], + foreignKeys: [ + { + field: "name", + table: "person", + table_field: "id" + } + ] + }); + + expect(result.query).to.be.equal('create table if not exists "users"("name" varchar(16) NOT NULL PRIMARY KEY,"age" int NOT NULL, FOREIGN KEY ("name") REFERENCES person("id"));'); + }); +}); diff --git a/legacy/json-sql/tests/8_index.js b/legacy/json-sql/tests/8_index.js new file mode 100644 index 00000000..464a85b3 --- /dev/null +++ b/legacy/json-sql/tests/8_index.js @@ -0,0 +1,45 @@ +'use strict'; + +var jsonSql = require('../lib')(); +var expect = require('chai').expect; + +/* + jsonSql.build({ + type: 'index', + table: 'users', + index: { + name: "user_id", + field: "id" + } + }); + */ + +describe('Index', function() { + it('should throw error without name property', function() { + expect(function() { + jsonSql.build({ + type: 'index', + table: 'users' + }); + }).to.throw('`name` property is not set in `index` clause'); + }); + + it('should throw error without indexOn property', function() { + expect(function() { + jsonSql.build({ + type: 'index', + table: 'users', + name: 'index_id' + }); + }).to.throw('`indexOn` property is not set in `index` clause'); + }); + + it('should be ok with name and field property', function () { + var result = jsonSql.build({ + type: "index", + table: "users", + name: "index_id", + indexOn: "id" + }); + }); +}); From d3ef4882abc24cca2321c0d890e5976a9dffa8da Mon Sep 17 00:00:00 2001 From: adamant-al Date: Mon, 5 Sep 2022 10:18:00 +0300 Subject: [PATCH 35/36] chore: update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b282bcb..9592f59d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "adamant", - "version": "0.6.5", + "version": "0.7.7", "private": true, "scripts": { "start": "node app.js", From 088965163f342b5d9d11d872ad79f120a62c7fee Mon Sep 17 00:00:00 2001 From: adamant-al Date: Mon, 5 Sep 2022 10:18:23 +0300 Subject: [PATCH 36/36] fix: update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9592f59d..e46a2b32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "adamant", - "version": "0.7.7", + "version": "0.7.0", "private": true, "scripts": { "start": "node app.js",