From 7fa6cb6656778c1e8407664dde7fe5d471650416 Mon Sep 17 00:00:00 2001 From: Alexander Sokol Date: Thu, 15 Mar 2018 12:48:30 +0200 Subject: [PATCH 1/4] [LBO-438] Vendor Search to sort matches displayed in admin portal --- lib/queryBuilder.js | 65 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/lib/queryBuilder.js b/lib/queryBuilder.js index 8ad9533..35e728e 100644 --- a/lib/queryBuilder.js +++ b/lib/queryBuilder.js @@ -10,7 +10,10 @@ function queryBuilder(initial_state) { if (this.state.count) { sql = sql + " SELECT COUNT(*) " } else { - sql = sql + " SELECT * " + if(!this.state.select){ + this.state.select = ['*']; + } + sql = sql + " SELECT " + this.state.select.join(", ") } if (this.state.from) { sql = sql + ` FROM ${this.state.from} ` @@ -53,7 +56,29 @@ function queryBuilder(initial_state) { } if (this.state.orderBy) { - sql = sql + ` ORDER BY ${this.state.orderBy[0]} ${this.state.orderBy[1]} ` + let ordersBy = [], + placeholder; + for (order of this.state.orderBy) { + let column = order[0], + direction = order[1], + orderParams = order[2] + statements = []; + + if(orderParams){ + if(orderParams.function == 'similarity'){ + column = 'similarity(' + column + ', '; + for (value of orderParams.values) { + placeholder = "$" + bound_vars.push(value); + statements.push(` ${placeholder} `) + } + column = column + statements.join(", ") + ')'; + } + } + + ordersBy.push(` ${column} ${direction} `) + } + + sql = sql + " ORDER BY " + ordersBy.join(", "); } if (this.state.paginate) { @@ -76,16 +101,50 @@ function queryBuilder(initial_state) { }; out.from = from.bind(out); + let select = function (fields) { + this.state.select = fields; + return this; + }; + out.select = select.bind(out); + + let addSelect = function (field) { + this.state.select = this.state.select || []; + this.state.select.push(field); + return this; + }; + out.addSelect = addSelect.bind(out); + let orderBy = function (field, direction) { delete this.state.count; - this.state.orderBy = [field, direction]; + this.state.orderBy = []; + this.state.orderBy.push([field, direction, null]); return this; }; out.orderBy = orderBy.bind(out); + let addOrderBy = function (field, direction) { + delete this.state.count; + this.state.orderBy = this.state.orderBy || []; + this.state.orderBy.push([field, direction, null]); + return this; + }; + out.addOrderBy = addOrderBy.bind(out); + + let addOrderByFunction = function (functionName, field, values, direction) { + delete this.state.count; + this.state.orderBy = this.state.orderBy || []; + this.state.orderBy.push([field, direction, { + function: functionName, + values: values || [] + }]); + return this; + }; + out.addOrderByFunction = addOrderByFunction.bind(out); + let where = function (field, operator, value) { this.state.wheres = this.state.wheres || []; this.state.wheres.push([field, operator, value]); + return this; }; out.where = where.bind(out); From a4e20bc97b679d2a7fc0ebd2eee68d6c17fbfa6d Mon Sep 17 00:00:00 2001 From: Alexander Sokol Date: Thu, 15 Mar 2018 13:26:06 +0200 Subject: [PATCH 2/4] [LBO-438] Vendor Search to sort matches displayed in admin portal --- lib/functions/index.js | 5 +++++ lib/functions/similarity.js | 6 ++++++ lib/queryBuilder.js | 15 +++++---------- package-lock.json | 13 +++++++++++++ package.json | 1 + 5 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 lib/functions/index.js create mode 100644 lib/functions/similarity.js create mode 100644 package-lock.json diff --git a/lib/functions/index.js b/lib/functions/index.js new file mode 100644 index 0000000..5d04500 --- /dev/null +++ b/lib/functions/index.js @@ -0,0 +1,5 @@ + +module.exports = { + similarity: require('./similarity') +} + \ No newline at end of file diff --git a/lib/functions/similarity.js b/lib/functions/similarity.js new file mode 100644 index 0000000..47c2be0 --- /dev/null +++ b/lib/functions/similarity.js @@ -0,0 +1,6 @@ +const escape = require("pg-escape"); + +module.exports = function (field, word) { + return escape('similarity(%I, %L)', field, word) +} + \ No newline at end of file diff --git a/lib/queryBuilder.js b/lib/queryBuilder.js index 35e728e..a4bb073 100644 --- a/lib/queryBuilder.js +++ b/lib/queryBuilder.js @@ -1,5 +1,7 @@ // See test/queryBuilder_test.js for usage examples +const {similarity} = require('./functions/index'); + function queryBuilder(initial_state) { let out = {}; out.state = initial_state || {}; @@ -56,22 +58,15 @@ function queryBuilder(initial_state) { } if (this.state.orderBy) { - let ordersBy = [], - placeholder; + let ordersBy = []; for (order of this.state.orderBy) { let column = order[0], direction = order[1], - orderParams = order[2] - statements = []; + orderParams = order[2]; if(orderParams){ if(orderParams.function == 'similarity'){ - column = 'similarity(' + column + ', '; - for (value of orderParams.values) { - placeholder = "$" + bound_vars.push(value); - statements.push(` ${placeholder} `) - } - column = column + statements.join(", ") + ')'; + column = similarity(column, orderParams.values.pop()); } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f260b5b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "lib-lequel", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "pg-escape": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pg-escape/-/pg-escape-0.2.0.tgz", + "integrity": "sha1-ZVlMFpFlm0q24Mu/nVB0S+R0mY4=" + } + } +} diff --git a/package.json b/package.json index d851210..a5cac6b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "eslint": "^4.0.0", "pg": "^6.2.4", + "pg-escape": "^0.2.0", "pg-pool": "^1.7.1", "postgrator": "^2.10.0", "url": "^0.11.0" From e74f713c325bb1638028bcfc2193cb0ba054323c Mon Sep 17 00:00:00 2001 From: Alexander Sokol Date: Fri, 16 Mar 2018 07:13:03 +0200 Subject: [PATCH 3/4] [LBO-438] Vendor Search to sort matches displayed in admin portal --- lib/queryBuilder.js | 4 +- test/queryBuilder_test.js | 102 +++++++++++++++++++++++++++++--------- 2 files changed, 81 insertions(+), 25 deletions(-) diff --git a/lib/queryBuilder.js b/lib/queryBuilder.js index a4bb073..127bff6 100644 --- a/lib/queryBuilder.js +++ b/lib/queryBuilder.js @@ -10,7 +10,7 @@ function queryBuilder(initial_state) { let sql = ""; let bound_vars = []; if (this.state.count) { - sql = sql + " SELECT COUNT(*) " + sql = sql + " SELECT COUNT(*)" } else { if(!this.state.select){ this.state.select = ['*']; @@ -70,7 +70,7 @@ function queryBuilder(initial_state) { } } - ordersBy.push(` ${column} ${direction} `) + ordersBy.push(`${column} ${direction}`) } sql = sql + " ORDER BY " + ordersBy.join(", "); diff --git a/test/queryBuilder_test.js b/test/queryBuilder_test.js index c5fa670..f928617 100644 --- a/test/queryBuilder_test.js +++ b/test/queryBuilder_test.js @@ -8,28 +8,28 @@ describe('queryBuilder', function() { it('should accept from', function() { query = queryBuilder(); query.from("a_thing"); - expect([" SELECT * FROM a_thing ", []]).to.eql(query.finalize()); + expect([" SELECT * FROM a_thing ", []]).to.eql(query.finalize()); }); it('with multiple froms, the last should win', function() { query = queryBuilder(); query.from("a_thing"); query.from("banana"); - expect([" SELECT * FROM banana ", []]).to.eql(query.finalize()); + expect([" SELECT * FROM banana ", []]).to.eql(query.finalize()); }); it('should accept count', function() { query = queryBuilder(); query.from("banana"); query.count(); - expect([" SELECT COUNT(*) FROM banana ", []]).to.eql(query.finalize()); + expect([" SELECT COUNT(*) FROM banana ", []]).to.eql(query.finalize()); }); it('should accept order by', function() { query = queryBuilder(); query.from("banana"); query.orderBy("species", "ASC"); - expect([" SELECT * FROM banana ORDER BY species ASC ", []]).to.eql(query.finalize()); + expect([" SELECT * FROM banana ORDER BY species ASC", []]).to.eql(query.finalize()); }); it('should clear order by when calling count', function() { @@ -37,7 +37,7 @@ describe('queryBuilder', function() { query.from("banana"); query.orderBy("species", "ASC"); query.count(); - expect([" SELECT COUNT(*) FROM banana ", []]).to.eql(query.finalize()); + expect([" SELECT COUNT(*) FROM banana ", []]).to.eql(query.finalize()); }); it('should clear count by when calling order by', function() { @@ -46,7 +46,7 @@ describe('queryBuilder', function() { query.count(); query.orderBy("species", "ASC"); - expect([" SELECT * FROM banana ORDER BY species ASC ", []]).to.eql(query.finalize()); + expect([" SELECT * FROM banana ORDER BY species ASC", []]).to.eql(query.finalize()); }); it('with multiple order bys, the last should win', function() { @@ -54,28 +54,28 @@ describe('queryBuilder', function() { query.from("banana"); query.orderBy("peel", "DESC"); query.orderBy("species", "ASC"); - expect([" SELECT * FROM banana ORDER BY species ASC ", []]).to.eql(query.finalize()); + expect([" SELECT * FROM banana ORDER BY species ASC", []]).to.eql(query.finalize()); }); it('should accept pagination', function() { query = queryBuilder(); query.from("banana"); query.paginate(5, 5); - expect([" SELECT * FROM banana LIMIT 5 OFFSET 20 ", []]).to.eql(query.finalize()); + expect([" SELECT * FROM banana LIMIT 5 OFFSET 20 ", []]).to.eql(query.finalize()); }); it('should accept string pagination', function() { query = queryBuilder(); query.from("banana"); query.paginate("5", "5"); - expect([" SELECT * FROM banana LIMIT 5 OFFSET 20 ", []]).to.eql(query.finalize()); + expect([" SELECT * FROM banana LIMIT 5 OFFSET 20 ", []]).to.eql(query.finalize()); }); it('should sanitize garbage in pagination', function() { query = queryBuilder(); query.from("banana"); query.paginate("5bbbb", "5aaaa"); - expect([" SELECT * FROM banana LIMIT 5 OFFSET 20 ", []]).to.eql(query.finalize()); + expect([" SELECT * FROM banana LIMIT 5 OFFSET 20 ", []]).to.eql(query.finalize()); }); it('with multiple paginates, the last should win', function() { @@ -83,14 +83,14 @@ describe('queryBuilder', function() { query.from("banana"); query.paginate(2, 2); query.paginate(5, 5); - expect([" SELECT * FROM banana LIMIT 5 OFFSET 20 ", []]).to.eql(query.finalize()); + expect([" SELECT * FROM banana LIMIT 5 OFFSET 20 ", []]).to.eql(query.finalize()); }); it('should accept where', function() { query = queryBuilder(); query.from("banana"); query.where("species","=", "cavendish"); - expect([" SELECT * FROM banana WHERE species = $1 ", ["cavendish"]]).to.eql(query.finalize()); + expect([" SELECT * FROM banana WHERE species = $1 ", ["cavendish"]]).to.eql(query.finalize()); }); it('should compose multiple wheres', function() { @@ -99,7 +99,7 @@ describe('queryBuilder', function() { query.where("species","=", "cavendish"); query.where("state","=", "ripe"); expect([ - " SELECT * FROM banana WHERE species = $1 AND state = $2 " + " SELECT * FROM banana WHERE species = $1 AND state = $2 " , ["cavendish", "ripe"]]).to.eql(query.finalize()); }); @@ -109,7 +109,7 @@ describe('queryBuilder', function() { query.where("species","in", ['cavendish','alchemist']); query.where("feeder","in", ['ape','cat']); expect([ - " SELECT * FROM banana WHERE species in ($1,$2) AND feeder in ($3,$4) " + " SELECT * FROM banana WHERE species in ($1,$2) AND feeder in ($3,$4) " ,["cavendish","alchemist","ape","cat"]]).to.eql(query.finalize()); }); @@ -119,7 +119,7 @@ describe('queryBuilder', function() { query.where("species","in", ['cavendish','alchemist']); query.where("feeder","=", 'ape'); expect([ - " SELECT * FROM banana WHERE species in ($1,$2) AND feeder = $3 " + " SELECT * FROM banana WHERE species in ($1,$2) AND feeder = $3 " ,["cavendish","alchemist","ape"]]).to.eql(query.finalize()); }); @@ -128,7 +128,7 @@ describe('queryBuilder', function() { query.from("sports"); query.where("name","like", "football"); expect([ - " SELECT * FROM sports WHERE name like $1 " + " SELECT * FROM sports WHERE name like $1 " ,["%football%"]]).to.eql(query.finalize()); }); @@ -137,7 +137,7 @@ describe('queryBuilder', function() { query.from("sports"); query.where("name","ilike", "Football"); expect([ - " SELECT * FROM sports WHERE name ilike $1 " + " SELECT * FROM sports WHERE name ilike $1 " ,["%Football%"]]).to.eql(query.finalize()); }); @@ -148,7 +148,7 @@ describe('queryBuilder', function() { query.where("feeder","=", 'ape'); query.where("name","like", "Football"); expect([ - " SELECT * FROM banana WHERE species in ($1,$2) AND feeder = $3 AND name like $4 " + " SELECT * FROM banana WHERE species in ($1,$2) AND feeder = $3 AND name like $4 " ,["cavendish","alchemist","ape","%Football%"]]).to.eql(query.finalize()); }); @@ -159,7 +159,7 @@ describe('queryBuilder', function() { query.where("feeder","=", 'ape'); query.where("name","ilike", "Football"); expect([ - " SELECT * FROM banana WHERE species in ($1,$2) AND feeder = $3 AND name ilike $4 " + " SELECT * FROM banana WHERE species in ($1,$2) AND feeder = $3 AND name ilike $4 " ,["cavendish","alchemist","ape","%Football%"]]).to.eql(query.finalize()); }); @@ -168,8 +168,8 @@ describe('queryBuilder', function() { query.from("banana"); second_query = query.clone(); second_query.count(); - expect([" SELECT * FROM banana " , []]).to.eql(query.finalize()); - expect([" SELECT COUNT(*) FROM banana " , []]).to.eql(second_query.finalize()); + expect([" SELECT * FROM banana " , []]).to.eql(query.finalize()); + expect([" SELECT COUNT(*) FROM banana " , []]).to.eql(second_query.finalize()); }); it('should compose all query types and clone cleanly', function() { @@ -181,8 +181,64 @@ describe('queryBuilder', function() { second_query = query.clone(); second_query.count(); query.paginate(5, 5); - expect([" SELECT * FROM banana WHERE species = $1 AND state = $2 ORDER BY species ASC LIMIT 5 OFFSET 20 " , ["cavendish", "ripe"]]).to.eql(query.finalize()); - expect([" SELECT COUNT(*) FROM banana WHERE species = $1 AND state = $2 ",["cavendish", "ripe"]]).to.eql(second_query.finalize()); + expect([" SELECT * FROM banana WHERE species = $1 AND state = $2 ORDER BY species ASC LIMIT 5 OFFSET 20 " , ["cavendish", "ripe"]]).to.eql(query.finalize()); + expect([" SELECT COUNT(*) FROM banana WHERE species = $1 AND state = $2 ",["cavendish", "ripe"]]).to.eql(second_query.finalize()); + }); + + it('should compose custom select fields', function() { + query = queryBuilder(); + query.from("banana"); + query.select(["first_name"]); + query.addSelect("last_name"); + query.where("species","in", ['cavendish','alchemist']); + query.where("feeder","=", 'ape'); + expect([ + " SELECT first_name, last_name FROM banana WHERE species in ($1,$2) AND feeder = $3 " + ,["cavendish","alchemist","ape"]]).to.eql(query.finalize()); + }); + + it('should compose custom select fields and several orders', function() { + query = queryBuilder(); + query.from("banana"); + query.select(["first_name"]); + query.addSelect("last_name"); + query.where("species","in", ['cavendish','alchemist']); + query.where("feeder","=", 'ape'); + query.orderBy("species", "ASC"); + query.addOrderBy("first_name", "DESC"); + expect([ + " SELECT first_name, last_name FROM banana WHERE species in ($1,$2) AND feeder = $3 ORDER BY species ASC, first_name DESC" + ,["cavendish","alchemist","ape"]]).to.eql(query.finalize()); + }); + + it('should compose custom select fields and several orders', function() { + query = queryBuilder(); + query.from("banana"); + query.select(["first_name"]); + query.addSelect("last_name"); + query.where("species","in", ['cavendish','alchemist']); + query.where("feeder","=", 'ape'); + query.orderBy("species", "ASC"); + query.addOrderBy("first_name", "DESC"); + query.addOrderByFunction('similarity', 'species', ['cavendish'], 'DESC') + expect([ + " SELECT first_name, last_name FROM banana WHERE species in ($1,$2) AND feeder = $3 ORDER BY species ASC, first_name DESC, similarity(species, 'cavendish') DESC" + ,["cavendish","alchemist","ape"]]).to.eql(query.finalize()); + }); + + it('test injection', function() { + query = queryBuilder(); + query.from("banana"); + query.select(["first_name"]); + query.addSelect("last_name"); + query.where("species","in", ['cavendish','alchemist']); + query.where("feeder","=", 'ape'); + query.orderBy("species", "ASC"); + query.addOrderBy("first_name", "DESC"); + query.addOrderByFunction('similarity', 'species', ['\'; (select * from banana);'], 'DESC') + expect([ + " SELECT first_name, last_name FROM banana WHERE species in ($1,$2) AND feeder = $3 ORDER BY species ASC, first_name DESC, similarity(species, '''; (select * from banana);') DESC" + ,["cavendish","alchemist","ape"]]).to.eql(query.finalize()); }); }); From a3e5c15388e25732ee8dc34267f5f21014a0c6f6 Mon Sep 17 00:00:00 2001 From: Alexander Sokol Date: Fri, 16 Mar 2018 07:13:50 +0200 Subject: [PATCH 4/4] [LBO-438] Vendor Search to sort matches displayed in admin portal --- test/queryBuilder_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/queryBuilder_test.js b/test/queryBuilder_test.js index f928617..7956616 100644 --- a/test/queryBuilder_test.js +++ b/test/queryBuilder_test.js @@ -211,7 +211,7 @@ describe('queryBuilder', function() { ,["cavendish","alchemist","ape"]]).to.eql(query.finalize()); }); - it('should compose custom select fields and several orders', function() { + it('should compose custom select fields and several orders with order by function', function() { query = queryBuilder(); query.from("banana"); query.select(["first_name"]);