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 8ad9533..127bff6 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 || {}; @@ -8,9 +10,12 @@ function queryBuilder(initial_state) { let sql = ""; let bound_vars = []; if (this.state.count) { - sql = sql + " SELECT 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 +58,22 @@ function queryBuilder(initial_state) { } if (this.state.orderBy) { - sql = sql + ` ORDER BY ${this.state.orderBy[0]} ${this.state.orderBy[1]} ` + let ordersBy = []; + for (order of this.state.orderBy) { + let column = order[0], + direction = order[1], + orderParams = order[2]; + + if(orderParams){ + if(orderParams.function == 'similarity'){ + column = similarity(column, orderParams.values.pop()); + } + } + + ordersBy.push(`${column} ${direction}`) + } + + sql = sql + " ORDER BY " + ordersBy.join(", "); } if (this.state.paginate) { @@ -76,16 +96,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); 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" diff --git a/test/queryBuilder_test.js b/test/queryBuilder_test.js index c5fa670..7956616 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 with order by function', 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()); }); });