├── circle.yml ├── bin ├── server.js ├── utils.js ├── rql.js └── rql-query.js ├── src ├── Nodes │ ├── FunctionNodes │ │ ├── CsvDriver │ │ │ ├── test │ │ │ │ ├── sample.csv │ │ │ │ └── csv.js │ │ │ └── CsvNode.js │ │ ├── RedisDriver │ │ │ ├── utils.js │ │ │ ├── types │ │ │ │ ├── generic.js │ │ │ │ ├── string.js │ │ │ │ └── hash.js │ │ │ └── RedisNode.js │ │ ├── PostgreSQLDriver │ │ │ ├── functions │ │ │ │ ├── insert.js │ │ │ │ ├── softInsert.js │ │ │ │ ├── find.js │ │ │ │ ├── update.js │ │ │ │ ├── count.js │ │ │ │ └── aggregate.js │ │ │ ├── updateGenerator.js │ │ │ ├── test │ │ │ │ ├── updateGenerator.js │ │ │ │ ├── insertGenerator.js │ │ │ │ ├── utils.js │ │ │ │ └── whereGenerator.js │ │ │ ├── insertGenerator.js │ │ │ ├── utils.js │ │ │ ├── PostgreSQLNode.js │ │ │ └── whereGenerator.js │ │ ├── MongoDBDriver │ │ │ ├── functions │ │ │ │ ├── insert.js │ │ │ │ ├── find.js │ │ │ │ └── aggregate.js │ │ │ ├── test │ │ │ │ └── utils.js │ │ │ ├── utils.js │ │ │ └── MongoDBNode.js │ │ ├── MySQLDriver │ │ │ ├── functions │ │ │ │ ├── find.js │ │ │ │ ├── count.js │ │ │ │ ├── avg.js │ │ │ │ └── sum.js │ │ │ ├── utils.js │ │ │ ├── MySQLNode.js │ │ │ └── whereGenerator.js │ │ ├── HttpDriver │ │ │ ├── test │ │ │ │ └── http.js │ │ │ └── HttpNode.js │ │ ├── MapReduce │ │ │ ├── MapReduceNode.js │ │ │ ├── mapReduce.js │ │ │ └── test │ │ │ │ ├── MapReduceNode.js │ │ │ │ └── mapReduce.js │ │ └── RapidAPIDriver │ │ │ └── RapidAPINode.js │ ├── RenameNode.js │ ├── FlatObjectNode.js │ ├── OptionalNode.js │ ├── LeafNode.js │ ├── README.md │ ├── CachedFunctionNode.js │ ├── CompositeNode.js │ ├── ObjectNode.js │ ├── CastedLeafNode.js │ ├── ArrayNode.js │ ├── utils.js │ ├── LogicNode.js │ └── FunctionNode.js ├── RQLQuery.js ├── Parser │ ├── Parser.js │ ├── grammer-raw.pegjs │ └── grammer.pegjs └── RQL.js ├── index.js ├── .idea ├── watcherTasks.xml ├── misc.xml ├── vcs.xml ├── RapidQL.iml ├── jsLinters │ └── jslint.xml ├── modules.xml └── WQL.iml ├── .vscode └── launch.json ├── test ├── rename-node.js ├── driverTests.js ├── object-node.js ├── E2E.js ├── node-interface.js ├── optional-node.js ├── generative │ ├── gen-typecasted-leaf-nodes.js │ └── gen-parser.js ├── leaf-node.js ├── parserE2E.js ├── flat-object-node.js ├── composite-node.js ├── array-node.js ├── utils.js ├── functionNode.js ├── type-casted-leaf-node.js └── logic-node.js ├── package.json ├── .gitignore └── README.md /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 9.0.0 -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Created by iddo on 7/20/17. 4 | */ 5 | console.log("server"); -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/CsvDriver/test/sample.csv: -------------------------------------------------------------------------------- 1 | name,age,sex 2 | iddo,20,m 3 | matan,13,m 4 | daria,18,f 5 | sharon,45,f 6 | michael,51,m -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Iddo on 12/17/2016. 3 | */ 4 | 5 | "use strict"; 6 | 7 | const RQL = require('./src/RQL'); 8 | 9 | module.exports = RQL; -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/RapidQL.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/jslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/RedisDriver/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/19/17. 3 | */ 4 | module.exports.getArg = async (obj, key) => { 5 | if (!obj['key']) 6 | throw `Redis error - "${key}" is required`; 7 | else 8 | return obj[key]; 9 | }; 10 | 11 | module.exports.flattenObject = obj => Object.keys(obj).reduce((r, k) => {return r.concat(k, obj[k]);}, []); -------------------------------------------------------------------------------- /src/Nodes/RenameNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/13/17. 3 | */ 4 | "use strict"; 5 | 6 | class RenameNode { 7 | constructor(name, innerNode) { 8 | this.name = name; 9 | this.innerNode = innerNode; 10 | } 11 | 12 | getName() { 13 | return this.name; 14 | } 15 | 16 | //noinspection JSAnnotator 17 | eval(context, ops) { 18 | return this.innerNode.eval(context, ops); 19 | } 20 | } 21 | 22 | module.exports = RenameNode; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/index.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.idea/WQL.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Nodes/FlatObjectNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 4/19/18. 3 | */ 4 | "use strict"; 5 | const utils = require('./utils'); 6 | 7 | class FlatObjectNode { 8 | constructor(innerNode) { 9 | this.innerNode = innerNode; 10 | } 11 | 12 | getName() { 13 | return this.innerNode.getName(); 14 | } 15 | 16 | //noinspection JSAnnotator 17 | async eval(context, ops) { 18 | return await this.innerNode.eval(context, ops); 19 | } 20 | } 21 | 22 | module.exports = FlatObjectNode; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/functions/insert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 6/23/17. 3 | */ 4 | 5 | const insertClauseGenerator = require('./../insertGenerator').insertClauseGenerator; 6 | 7 | module.exports = (DBSchema, DBTable, client, args) => { 8 | let queryString = insertClauseGenerator(DBSchema, DBTable, args); 9 | 10 | queryString += ' RETURNING *;'; 11 | 12 | return new Promise((resolve, reject) => { 13 | client.query(queryString, (err, result) => { 14 | if (err) { 15 | reject(err); 16 | } else { 17 | resolve(result.rows || []); 18 | } 19 | }); 20 | }); 21 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/updateGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/7/17. 3 | */ 4 | const quoteAsNeeded = require('./utils').quoteAsNeeded; 5 | 6 | function setClauseGenerator(args) { 7 | let queryString = ''; 8 | if(Object.keys(args).length > 0) { 9 | queryString += ' SET '; 10 | queryString += Object.entries(args) //Get entries [[k1,v1],[k2,v2]] 11 | .map(p=>[p[0], quoteAsNeeded(p[1])]) //Quote values 12 | .map(p => p.join(" = ")) //Join into "k1 = v1" 13 | .join(", "); //Join with comma separated 14 | } 15 | return queryString; 16 | } 17 | 18 | module.exports.setClauseGenerator = setClauseGenerator; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/functions/softInsert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 6/30/17. 3 | */ 4 | const insertClauseGenerator = require('./../insertGenerator').insertClauseGenerator; 5 | 6 | module.exports = (DBSchema, DBTable, client, args) => { 7 | let queryString = insertClauseGenerator(DBSchema, DBTable, args); 8 | 9 | 10 | queryString += ' ON CONFLICT DO NOTHING RETURNING *;'; 11 | 12 | return new Promise((resolve, reject) => { 13 | client.query(queryString, (err, result) => { 14 | if (err) { 15 | reject(err); 16 | } else { 17 | resolve(result.rows || []); 18 | } 19 | }); 20 | }); 21 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MongoDBDriver/functions/insert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/28/17. 3 | */ 4 | const flattenObject = require('./../utils').flattenObject; 5 | const convertObjectIds = require('./../utils').convertObjectIds; 6 | const unconvertObjectIds = require('./../utils').unconvertObjectIds; 7 | 8 | module.exports = (DBTable, db, args) => { 9 | return new Promise((resolve, reject) => { 10 | const newObj = args; 11 | db.collection(DBTable).insertOne(newObj, (err, doc) => { 12 | if (err) 13 | reject(`MongoDB error performing insert: ${err}`); 14 | else { 15 | resolve(doc); 16 | } 17 | }); 18 | }); 19 | }; -------------------------------------------------------------------------------- /src/Nodes/OptionalNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/13/17. 3 | */ 4 | "use strict"; 5 | 6 | class OptionalNode { 7 | constructor(innerNode) { 8 | this.innerNode = innerNode; 9 | } 10 | 11 | getName() { 12 | return this.innerNode.getName(); 13 | } 14 | 15 | //noinspection JSAnnotator 16 | eval(context, ops) { 17 | return new Promise((resolve, reject) => { 18 | this.innerNode.eval(context, ops) 19 | .then((val) => { 20 | resolve(val); 21 | }) 22 | .catch((err) => { 23 | resolve(null); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | module.exports = OptionalNode; -------------------------------------------------------------------------------- /src/Nodes/LeafNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Iddo on 12/17/2016. 3 | */ 4 | "use strict"; 5 | const utils = require('./utils'); 6 | 7 | class LeafNode { 8 | constructor(name) { 9 | this.name = name; 10 | } 11 | 12 | getName() { 13 | return this.name; 14 | } 15 | 16 | //noinspection JSAnnotator 17 | async eval(context) { 18 | return utils.resolve(this.getName(), context); 19 | } 20 | } 21 | 22 | module.exports = LeafNode; 23 | 24 | //TEST PLAYGROUND 25 | /*let context = {a:1}; 26 | let ln = new LeafNode("b"); 27 | 28 | ln.eval(context) 29 | .then((val) => { 30 | console.log(val); 31 | }) 32 | .catch((error) => { 33 | console.warn(error); 34 | }); 35 | 36 | */ -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/CsvDriver/test/csv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 1/5/18. 3 | */ 4 | const { expect, assert } = require('chai'); 5 | const CsvNode = require('./../CsvNode'); 6 | 7 | module.exports = () => { 8 | describe('reading csv file', () => { 9 | it('should read a file with a header column', async () => { 10 | let node = new CsvNode('Csv.read', [], { 11 | file: "./src/Nodes/FunctionNodes/CsvDriver/test/sample.csv", 12 | columns: "true" 13 | }); 14 | 15 | const results = await node.eval({}, {}); 16 | 17 | assert.equal(results.length, 5); // 5 results 18 | results.forEach(r => assert.typeOf(r, 'object')); // all objects 19 | results.forEach(r => assert.hasAllKeys(r, ['name','age','sex'])); // has right keys 20 | }); 21 | }); 22 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MongoDBDriver/functions/find.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/19/17. 3 | */ 4 | 5 | const flattenObject = require('./../utils').flattenObject; 6 | const convertObjectIds = require('./../utils').convertObjectIds; 7 | const unconvertObjectIds = require('./../utils').unconvertObjectIds; 8 | 9 | module.exports = (DBTable, db, args) => { 10 | return new Promise((resolve, reject) => { 11 | let query = flattenObject(convertObjectIds(args)); 12 | db.collection(DBTable).find(query).toArray((err, doc) => { 13 | if (err) 14 | reject(`MongoDB error performing find: ${err}`); 15 | else { 16 | let converted = unconvertObjectIds(doc); 17 | resolve(converted); 18 | } 19 | }); 20 | }); 21 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MySQLDriver/functions/find.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 3/10/17. 3 | */ 4 | const whereGenerator = require('./../whereGenerator'); 5 | 6 | 7 | function find(DBTable, client, args) { 8 | //We'll build the SQL query with that string 9 | let queryString = ""; 10 | 11 | //Base query 12 | queryString += `SELECT * FROM \`${DBTable}\``; 13 | 14 | //Add where conditions 15 | queryString += whereGenerator.whereGenerator(args); 16 | 17 | return new Promise((resolve, reject) => { 18 | client.query(queryString, (err, result) => { 19 | if (err) { 20 | reject(err); 21 | } else { 22 | resolve(result || []); 23 | } 24 | }); 25 | }); 26 | 27 | } 28 | 29 | module.exports = find; 30 | -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/functions/find.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 2/14/17. 3 | */ 4 | 5 | const whereGenerator = require('./../whereGenerator').whereGenerator; 6 | 7 | 8 | function find(DBSchema, DBTable, client, args) { 9 | //We'll build the SQL query with that string 10 | let queryString = ""; 11 | 12 | //Base query 13 | queryString += `SELECT * FROM ${DBSchema}.${DBTable}`; 14 | 15 | //Add where conditions 16 | queryString += whereGenerator(args); 17 | 18 | return new Promise((resolve, reject) => { 19 | client.query(queryString, (err, result) => { 20 | if (err) { 21 | reject(err); 22 | } else { 23 | resolve(result.rows || []); 24 | } 25 | }); 26 | }); 27 | 28 | } 29 | 30 | module.exports = find; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/test/updateGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/7/17. 3 | */ 4 | "use strict"; 5 | 6 | const assert = require('assert'), 7 | setClauseGenerator = require('./../updateGenerator').setClauseGenerator; 8 | 9 | module.exports = () => { 10 | describe("setClauseGenerator", () => { 11 | it('should support empty updates', () => { 12 | assert.equal(setClauseGenerator({}), ""); 13 | }); 14 | 15 | it('should support single update', () => { 16 | assert.equal(setClauseGenerator({'a':'b'}), " SET a = 'b'"); 17 | }); 18 | 19 | it('should support multiple updates', () => { 20 | assert.equal(setClauseGenerator({'a':'b', 'c': 1}), " SET a = 'b', c = 1"); 21 | assert.equal(setClauseGenerator({'a':'b', 'c': 1, 'd': 'a'}), " SET a = 'b', c = 1, d = 'a'"); 22 | }); 23 | }); 24 | }; -------------------------------------------------------------------------------- /src/RQLQuery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Iddo on 12/17/2016. 3 | */ 4 | "use strict"; 5 | 6 | const ObjectNode = require('./Nodes/ObjectNode'); 7 | 8 | class WQLQuery { 9 | 10 | /** 11 | * Construct a new WQLQuery object from root nodes and context 12 | * @param roots array of root nodes 13 | * @param options any options required by query nodes 14 | */ 15 | constructor(roots, options) { 16 | this.roots = roots; 17 | this.options = Object.assign({}, options); 18 | } 19 | 20 | //noinspection JSAnnotator 21 | /** 22 | * Performs the RQL query withing a specific context 23 | * @param context the context to perform the query in 24 | * @return Promise 25 | */ 26 | async eval(context) { 27 | const queryNode = new ObjectNode('root', this.roots); 28 | return await queryNode.eval(context, this.options); 29 | } 30 | } 31 | 32 | module.exports = WQLQuery; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MongoDBDriver/test/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/20/17. 3 | */ 4 | const assert = require('assert'), 5 | flattenObject = require('./../utils').flattenObject, 6 | reducers = require('./../utils').reducers; 7 | 8 | 9 | module.exports = () => { 10 | describe('flattenObject', () => { 11 | it('should not change one level object', () => { 12 | assert.deepEqual(flattenObject({a: 1, b: true, c: "asd"}), {a: 1, b: true, c: "asd"}); 13 | }); 14 | 15 | it('should flatten two level object', () => { 16 | assert.deepEqual(flattenObject({a: 1, b: true, c: "asd", o:{b:'c'}}), {a: 1, b: true, c: "asd", "o.b":'c'}); 17 | }); 18 | 19 | it('should flatten n level object', () => { 20 | assert.deepEqual(flattenObject({a: 1, b: true, c: "asd", o:{b:'c', d:{"c":"d"}}}), {a: 1, b: true, c: "asd", "o.b":'c', "o.d.c":"d" }); 21 | }); 22 | }); 23 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/functions/update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/7/17. 3 | */ 4 | const whereGenerator = require('./../whereGenerator').whereGenerator; 5 | const setClauseGenerator = require('./../updateGenerator').setClauseGenerator; 6 | 7 | module.exports = (DBSchema, DBTable, client, args) => { 8 | let queryString = ""; 9 | 10 | queryString += `UPDATE ${DBSchema}.${DBTable}`; 11 | 12 | if(args.hasOwnProperty('SET')) { 13 | queryString += setClauseGenerator(args['SET']); 14 | } 15 | 16 | //Add where conditions 17 | queryString += whereGenerator(args); 18 | 19 | queryString += ' RETURNING *;'; 20 | 21 | return new Promise((resolve, reject) => { 22 | client.query(queryString, (err, result) => { 23 | if (err) { 24 | reject(err); 25 | } else { 26 | resolve(result.rows || []); 27 | } 28 | }); 29 | }); 30 | }; -------------------------------------------------------------------------------- /src/Nodes/README.md: -------------------------------------------------------------------------------- 1 | # Nodes 2 | 3 | After being parsed by the parser, a query essentially becomes a forest of nodes (forest = graph structure with multiple trees). The interpreter than starts at the top of each tree. 4 | 5 | ## Execution 6 | 7 | - During the execution, the interpreter will call the `eval()` function of the topmost node in each tree. 8 | - The node will evaluate. If it has children nodes, it'll call their `eval()` function as well. 9 | - When done, it'll return the data to be added to the result. The interperter will append that data to the result, under the key returned by the node's `getName()` function. 10 | 11 | Thus, every node must implement the following methods: 12 | 13 | - **`constructor()`**: used to build a node by the parser. 14 | - **`getName()`**: returns the node name. 15 | - **`async eval()`**: evaluates the node, returning a promise which will resolve to it's value. 16 | 17 | ## Node Hierarchy 18 | 19 | ![](http://i.imgur.com/gVaFokf.jpg) -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/RedisDriver/types/generic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/19/17. 3 | */ 4 | 5 | 6 | const string = require('./string'), 7 | hash = require('./hash'); 8 | 9 | function getType(client, key) { 10 | return new Promise((resolve, reject) => { 11 | client.type(key, (err, data) => { 12 | if (err) 13 | reject(`Redis error getting type for key ${key}: ${err}`); 14 | else 15 | resolve(data); 16 | }); 17 | }); 18 | } 19 | module.exports.type = (client, args) => { 20 | return getType(client, args['key']); 21 | }; 22 | 23 | module.exports.find = async (client, args) => { 24 | // Preflight type check 25 | const type = await getType(client, args['key']); 26 | 27 | if (type === 'string') 28 | return await string.get(client, args); 29 | else if (type === 'hash') 30 | return await hash.hgetall(client, args); 31 | else 32 | throw `Redis operation find not supported on type ${type}`; 33 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/functions/count.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 3/1/17. 3 | */ 4 | 5 | const whereGenerator = require('./../whereGenerator').whereGenerator; 6 | 7 | function count(DBSchema, DBTable, client, args) { 8 | //We'll build the SQL query with that string 9 | let queryString = ""; 10 | 11 | //GROUP BY 12 | //If we're grouping, we want to select the group name as well 13 | let coloumns = ""; 14 | if (typeof args['GROUPBY'] == 'string') { 15 | coloumns += `, ${args['GROUPBY']}`; 16 | } 17 | 18 | //Base query 19 | queryString += `SELECT COUNT(*) ${coloumns} FROM ${DBSchema}.${DBTable}`; 20 | 21 | //Add where conditions 22 | queryString += whereGenerator(args); 23 | 24 | return new Promise((resolve, reject) => { 25 | client.query(queryString, (err, result) => { 26 | if (err) { 27 | reject(err); 28 | } else { 29 | resolve(result.rows || []); 30 | } 31 | }); 32 | }); 33 | 34 | } 35 | 36 | module.exports = count; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/RedisDriver/types/string.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/19/17. 3 | */ 4 | 5 | const getArg = require('./../utils').getArg; 6 | 7 | function get(client, key) { 8 | return new Promise((resolve, reject) => { 9 | client.get(key, (err, data) => { 10 | if (err) 11 | reject(`Redis error getting key ${key}: ${err}`); 12 | else 13 | resolve({value:data}); 14 | }); 15 | }); 16 | } 17 | module.exports.get = async (client, args) => { 18 | return await get(client, await getArg(args, 'key')); 19 | }; 20 | 21 | function set(client, key, value) { 22 | return new Promise((resolve, reject) => { 23 | client.set(key, value, (err, data) => { 24 | if (err) 25 | reject(`Redis error setting key ${key} with value ${value}: ${err}`); 26 | else 27 | resolve(data); 28 | }); 29 | }); 30 | } 31 | module.exports.set = async (client, args) => { 32 | return await set(client, await getArg(args, 'key'), await getArg(args, 'value')); 33 | }; -------------------------------------------------------------------------------- /src/Parser/Parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Iddo on 12/18/2016. 3 | */ 4 | "use strict"; 5 | const nearley = require("nearley"), 6 | grammer = require("./grammer"), 7 | peg = require("pegjs"); 8 | 9 | 10 | const WHITE_SPACES = [ 11 | ' ', 12 | '\n', 13 | '\t' 14 | ]; 15 | function removeComments(str) { 16 | str = str.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '');; 17 | return str; 18 | } 19 | function removeWhiteSpaces(str) { 20 | str = str.replace("\n", "").replace("\t", ""); 21 | return str.replace(/\s+(?=((\\[\\"]|[^\\"])*"(\\[\\"]|[^\\"])*")*(\\[\\"]|[^\\"])*$)/g, ''); 22 | } 23 | module.exports.removeWhiteSpaces = removeWhiteSpaces; 24 | module.exports.removeComments = removeComments; 25 | 26 | module.exports.parse = (str) => { 27 | //Find and replace comments 28 | str = removeComments(str); 29 | //Find and replace spaces 30 | str = removeWhiteSpaces(str); 31 | 32 | return new Promise((resolve, reject) => { 33 | try { 34 | resolve(grammer.parse(str)); 35 | 36 | } catch(e) { 37 | reject(e); 38 | } 39 | }); 40 | }; -------------------------------------------------------------------------------- /test/rename-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/13/17. 3 | */ 4 | "use strict"; 5 | 6 | const assert = require("assert"); 7 | const LeafNode = require("../src/Nodes/LeafNode"); 8 | const RenameNode = require("../src/Nodes/RenameNode"); 9 | const CompositeNode = require("../src/Nodes/CompositeNode"); 10 | 11 | describe("RenameNode", () => { 12 | const node = new RenameNode("b", new LeafNode("a")); 13 | 14 | describe('eval function', () => { 15 | it('should have an eval() function', () => { 16 | assert.equal('function', typeof node.eval) 17 | }); 18 | 19 | it(`should return a promise when called`, () => { 20 | let p = node.eval({a:1}); 21 | assert.equal(true, (Promise.resolve(p) == p)); //http://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise/38339199#38339199 22 | }); 23 | }); 24 | 25 | it('should return correct name', () => { 26 | assert.equal(node.getName(), "b"); 27 | }); 28 | 29 | it('should return correct value', async () => { 30 | const res = await node.eval({a:1}); 31 | assert.deepEqual(res, 1); 32 | }); 33 | }); -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/insertGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 6/29/17. 3 | */ 4 | const removeSpecialArgs = require('./utils').removeSpecialArgs; 5 | const quoteAsNeeded = require('./utils').quoteAsNeeded; 6 | const objValues = require('./utils').objValues; 7 | 8 | function epochToSqlTimestamp(timestamp) { 9 | return (new Date(parseInt(timestamp))).toISOString().slice(0, 19).replace('T', ' '); 10 | } 11 | 12 | function typeConverter(arg) { 13 | return (typeof arg != 'object') ? arg : 14 | (arg.hasOwnProperty('timestamp') && !isNaN(parseInt(arg.timestamp))) ? epochToSqlTimestamp(arg.timestamp) : 15 | null; 16 | } 17 | module.exports.typeConverter = typeConverter; 18 | 19 | module.exports.insertClauseGenerator = (DBSchema, DBTable, args) => { 20 | 21 | //Base query 22 | let query = 'INSERT INTO'; 23 | 24 | //Add relation name 25 | query += ` ${DBSchema}.${DBTable}`; 26 | 27 | //Add Columns 28 | query += ` (${Object.keys(args).join(", ")})`; 29 | 30 | //Add Values 31 | query += ` VALUES (${objValues(args).map(typeConverter).map(quoteAsNeeded).join(", ")})`; 32 | 33 | return query; 34 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/RedisDriver/types/hash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/19/17. 3 | */ 4 | 5 | const getArg = require('./../utils').getArg; 6 | const flattenObject = require('./../utils').flattenObject; 7 | 8 | function hgetall (client, key) { 9 | return new Promise((resolve, reject) => { 10 | client.hgetall(key, (err, data) => { 11 | if (err) 12 | reject(`Redis error getting key ${args[key]}: ${err}`); 13 | else 14 | resolve(data); 15 | }); 16 | }); 17 | } 18 | 19 | module.exports.hgetall = async (client, args) => { 20 | return await hgetall(client, await getArg(args, 'key')); 21 | }; 22 | 23 | function hmset (client, key, value) { 24 | return new Promise((resolve, reject) => { 25 | client.hmset(key, ...flattenObject(value), (err, data) => { 26 | if (err) 27 | reject(`Redis error setting key ${key}: ${err}`); 28 | else 29 | resolve(data); 30 | }); 31 | }); 32 | } 33 | 34 | module.exports.hmset = async (client, args) => { 35 | return await hmset(client, await getArg(args, 'key'), await getArg(args, 'value')); 36 | }; -------------------------------------------------------------------------------- /test/driverTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 2/16/17. 3 | */ 4 | 5 | /** 6 | * This is an ugly work around to run tests from specific DB Drivers in their folders. 7 | * There's probably a better solution but since I'm on a plane with no Wi-Fi I'll stick to that for now. 8 | */ 9 | 10 | describe('PostgreSQL Driver', ()=> { 11 | require('./../src/Nodes/FunctionNodes/PostgreSQLDriver/test/whereGenerator')(); 12 | require('./../src/Nodes/FunctionNodes/PostgreSQLDriver/test/utils')(); 13 | require('./../src/Nodes/FunctionNodes/PostgreSQLDriver/test/insertGenerator')(); 14 | require('./../src/Nodes/FunctionNodes/PostgreSQLDriver/test/updateGenerator')(); 15 | require('./../src/Nodes/FunctionNodes/MongoDBDriver/test/utils')(); 16 | }); 17 | 18 | describe('HTTP Driver', () => { 19 | require('./../src/Nodes/FunctionNodes/HttpDriver/test/http')(); 20 | }); 21 | 22 | describe('Map Reduce', () => { 23 | require('./../src/Nodes/FunctionNodes/MapReduce/test/mapReduce')(); 24 | require('./../src/Nodes/FunctionNodes/MapReduce/test/MapReduceNode')(); 25 | }); 26 | 27 | describe('CSV', () => { 28 | require('./../src/Nodes/FunctionNodes/CsvDriver/test/csv')(); 29 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rapidql", 3 | "version": "0.0.6", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/mocha/bin/mocha --recursive", 8 | "run": "nyc mocha --recursive" 9 | }, 10 | "engines": { 11 | "node": "6.1.0" 12 | }, 13 | "bin": { 14 | "rql": "bin/rql.js", 15 | "rql-server": "bin/server.js" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "UNLICENSED", 20 | "dependencies": { 21 | "cacheman": "^2.2.1", 22 | "chai": "^4.1.0", 23 | "chai-as-promised": "^7.1.1", 24 | "commander": "^2.11.0", 25 | "csv": "^2.0.0", 26 | "dnscache": "^1.0.1", 27 | "mocha": "^7.0.0", 28 | "mocha-testcheck": "^1.0.0-rc.0", 29 | "mongodb": "^2.2.30", 30 | "mustache": "^2.2.1", 31 | "mysql": "^2.13.0", 32 | "nearley": "^2.7.10", 33 | "nyc": "^15.0.0", 34 | "object-hash": "^1.2.0", 35 | "pegjs": "^0.10.0", 36 | "pg": "^6.1.2", 37 | "query-string": "^4.3.4", 38 | "rapidapi-connect": "0.0.4", 39 | "redis": "^2.7.1", 40 | "request": "^2.81.0", 41 | "simple-rate-limiter": "^0.2.3", 42 | "testcheck": "^1.0.0-rc.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/object-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 2/13/18. 3 | */ 4 | 5 | const chai = require("chai"); 6 | chai.use(require("chai-as-promised")); 7 | const { expect, assert } = chai; 8 | const { describe, it } = require('mocha'); 9 | const ObjectNode = require('../src/Nodes/ObjectNode'); 10 | const LeafNode = require('../src/Nodes/LeafNode'); 11 | 12 | describe('ObjectNode', () => { 13 | 14 | it('should handle valid objects properly', async () => { 15 | let n = new ObjectNode('obj', [ 16 | new LeafNode('a'), 17 | new LeafNode('b') 18 | ]); 19 | 20 | let v = await n.eval({ 21 | obj: { 22 | a: 'AA', 23 | b: 'BB' 24 | } 25 | }); 26 | 27 | assert.equal(v.a, 'AA'); 28 | assert.equal(v.b, 'BB'); 29 | }); 30 | 31 | it('should support referencing properties from outside object within object', async () => { 32 | let n = new ObjectNode('obj', [ 33 | new LeafNode('a'), 34 | new LeafNode('b') 35 | ]); 36 | 37 | let v = await n.eval({ 38 | obj: { 39 | a: 'AA' 40 | }, 41 | b: 'BB' 42 | }); 43 | 44 | assert.equal(v.a, 'AA'); 45 | assert.equal(v.b, 'BB'); 46 | }); 47 | }); 48 | 49 | -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MySQLDriver/functions/count.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 3/10/17. 3 | */ 4 | const whereGenerator = require('./../whereGenerator'); 5 | 6 | function count(DBTable, client, args) { 7 | //We'll build the SQL query with that string 8 | let queryString = ""; 9 | 10 | //GROUP BY 11 | //If we're grouping, we want to select the group name as well 12 | let coloumns = ""; 13 | if (typeof args['GROUPBY'] == 'string') { 14 | coloumns += `, ${args['GROUPBY']}`; 15 | } 16 | 17 | //Base query 18 | queryString += `SELECT COUNT(*) ${coloumns} FROM \`${DBTable}\``; 19 | 20 | //Add where conditions 21 | queryString += whereGenerator.whereGenerator(args); 22 | 23 | return new Promise((resolve, reject) => { 24 | client.query(queryString, (err, result) => { 25 | if (err) { 26 | reject(err); 27 | } else { 28 | result.forEach((r) => { 29 | r['count'] = r['COUNT(*)']; //Nicer syntax 30 | delete r['COUNT(*)']; 31 | }); 32 | resolve(result || []); 33 | } 34 | }); 35 | }); 36 | 37 | } 38 | 39 | module.exports = count; 40 | -------------------------------------------------------------------------------- /bin/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 1/28/18. 3 | */ 4 | 5 | const fs = require('fs'); 6 | 7 | /** 8 | * Reads and parses JSON in a file. Returns empty object if file empty 9 | * @param filename 10 | * @returns {Promise} 11 | */ 12 | function readJsonFile(filename) { 13 | return new Promise((resolve, reject) => { 14 | fs.readFile(filename, 'utf8', function(err, data) { 15 | if (err) 16 | return reject(`Error reading JSON file ${filename} : ${err}`); 17 | else if (!data) 18 | return resolve({}); 19 | 20 | try { 21 | return resolve(JSON.parse(data)); 22 | } catch (e) { 23 | return reject(`Error parsing JSON in file ${filename} : ${e}`); 24 | } 25 | }); 26 | }); 27 | } 28 | module.exports.readJsonFile = readJsonFile; 29 | 30 | function readFile(filename) { 31 | return new Promise((resolve, reject) => { 32 | fs.readFile(filename, 'utf8', function(err, data) { 33 | if (err) 34 | return reject(`Error reading file ${filename} : ${err}`); 35 | else if (!data) 36 | return reject(`File ${filename} is empty`); 37 | else 38 | return resolve(data); 39 | }); 40 | }); 41 | } 42 | 43 | module.exports.readFile = readFile; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MySQLDriver/functions/avg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/21/17. 3 | */ 4 | const whereGenerator = require('./../whereGenerator'); 5 | 6 | function avg(DBTable, client, args) { 7 | //We'll build the SQL query with that string 8 | let queryString = ""; 9 | 10 | //GROUP BY 11 | //If we're grouping, we want to select the group name as well 12 | let coloumns = ""; 13 | if (typeof args['GROUPBY'] == 'string') { 14 | coloumns += `, ${args['GROUPBY']}`; 15 | } 16 | 17 | if (!args['FIELD']) 18 | return new Promise((resolve, reject) => {reject(`MySQL: to use the .sum() function, must supply FIELD to sum`)}); 19 | 20 | //Base query 21 | queryString += `SELECT AVG(${args['FIELD']}) as ${args['FIELD']} ${coloumns} FROM \`${DBTable}\``; 22 | 23 | //Add where conditions 24 | queryString += whereGenerator.whereGenerator(args); 25 | 26 | return new Promise((resolve, reject) => { 27 | client.query(queryString, (err, result) => { 28 | if (err) { 29 | reject(err); 30 | } else { 31 | resolve(result || []); 32 | } 33 | }); 34 | }); 35 | 36 | } 37 | 38 | module.exports = avg; 39 | -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MySQLDriver/functions/sum.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/21/17. 3 | */ 4 | 5 | const whereGenerator = require('./../whereGenerator'); 6 | 7 | function sum(DBTable, client, args) { 8 | //We'll build the SQL query with that string 9 | let queryString = ""; 10 | 11 | //GROUP BY 12 | //If we're grouping, we want to select the group name as well 13 | let coloumns = ""; 14 | if (typeof args['GROUPBY'] == 'string') { 15 | coloumns += `, ${args['GROUPBY']}`; 16 | } 17 | 18 | if (!args['FIELD']) 19 | return new Promise((resolve, reject) => {reject(`MySQL: to use the .sum() function, must supply FIELD to sum`)}); 20 | 21 | //Base query 22 | queryString += `SELECT SUM(${args['FIELD']}) as ${args['FIELD']} ${coloumns} FROM \`${DBTable}\``; 23 | 24 | //Add where conditions 25 | queryString += whereGenerator.whereGenerator(args); 26 | 27 | return new Promise((resolve, reject) => { 28 | client.query(queryString, (err, result) => { 29 | if (err) { 30 | reject(err); 31 | } else { 32 | resolve(result || []); 33 | } 34 | }); 35 | }); 36 | 37 | } 38 | 39 | module.exports = sum; 40 | -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MongoDBDriver/functions/aggregate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/21/17. 3 | */ 4 | const flattenObject = require('./../utils').flattenObject; 5 | const convertObjectIds = require('./../utils').convertObjectIds; 6 | const unconvertObjectIds = require('./../utils').unconvertObjectIds; 7 | const removeSpecialArgs = require('./../utils').removeSpecialArgs; 8 | 9 | 10 | const aggregate = (DBTable, db, args, aggregateFunction) => { 11 | return new Promise((resolve, reject) => { 12 | const field = args['FIELD']; 13 | let query = flattenObject(convertObjectIds(removeSpecialArgs(args))); 14 | db.collection(DBTable).aggregate([ 15 | {$match:query}, 16 | {$group: { 17 | "_id": null, 18 | [field]: {[aggregateFunction]: `$${field}`} 19 | }} 20 | ], (err, result) => { 21 | if (err) { 22 | reject(`MongoDB error performing aggregation: ${err}`); 23 | } else { 24 | resolve(result[0]); 25 | } 26 | }); 27 | }); 28 | }; 29 | 30 | module.exports = (aggregateFunction) => { 31 | return function (DBTable, db, args) { 32 | return aggregate(DBTable, db, args, `$${aggregateFunction}`); 33 | } 34 | }; -------------------------------------------------------------------------------- /src/RQL.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 12/20/16. 3 | */ 4 | "use strict"; 5 | 6 | const parser = require('./Parser/Parser'), 7 | RQLQuery = require('./RQLQuery'); 8 | 9 | 10 | const BASE_CONTEXT = { 11 | "false": false, 12 | "true": true 13 | }; 14 | 15 | class RQL { 16 | constructor(ops) { 17 | this.ops = ops; 18 | this.ops.logger = { 19 | log(msg) { 20 | if (ops.logLevel === 'info') 21 | console.log(msg); 22 | } 23 | }; 24 | } 25 | 26 | query(queryString, context) { 27 | if(!context) context = {}; 28 | context = Object.assign(context, BASE_CONTEXT); 29 | return new Promise((resolve, reject) => { 30 | parser.parse(queryString) 31 | .catch(reject) 32 | .then((queryRoots) => { 33 | let queryObject = new RQLQuery(queryRoots, this.ops); 34 | queryObject.eval(context).then(resolve).catch(reject); 35 | }) 36 | }); 37 | } 38 | 39 | log(queryString, context) { 40 | this.query(queryString, context).then((val) => { 41 | console.log(JSON.stringify(val, null, 2)); 42 | }).catch((err) => { 43 | console.warn(err); 44 | }) 45 | } 46 | } 47 | 48 | module.exports = RQL; -------------------------------------------------------------------------------- /src/Nodes/CachedFunctionNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 1/21/18. 3 | */ 4 | "use strict"; 5 | 6 | const hash = require('object-hash'); 7 | 8 | if (!global._function_node_cache) global._function_node_cache = {}; 9 | 10 | class CachedFunctionNode { 11 | constructor(innerNode) { 12 | this.innerNode = innerNode; 13 | } 14 | 15 | getName() { 16 | return this.innerNode.getName(); 17 | } 18 | 19 | //noinspection JSAnnotator 20 | async eval(context, ops) { 21 | 22 | const processedArgs = this.innerNode.getProcessedArgs(context); 23 | 24 | const innerNodeHash = hash({ 25 | name: this.innerNode.getName(), 26 | args: processedArgs 27 | }); 28 | 29 | if (innerNodeHash in global._function_node_cache) { 30 | ops.logger.log(`Cache hit: ${this.innerNode.getName()}`); 31 | return await this.innerNode.continueTree(context, ops, await global._function_node_cache[innerNodeHash]); 32 | } else { 33 | ops.logger.log(`Cache miss: ${this.innerNode.getName()}`); 34 | global._function_node_cache[innerNodeHash] = this.innerNode.performFunction(processedArgs, context, ops); 35 | const innerNodeValue = await global._function_node_cache[innerNodeHash]; 36 | return await this.innerNode.continueTree(context, ops, innerNodeValue); 37 | } 38 | } 39 | } 40 | 41 | module.exports = CachedFunctionNode; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/test/insertGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 6/29/17. 3 | */ 4 | "use strict"; 5 | 6 | const assert = require('assert'), 7 | insertClauseGenerator = require('./../insertGenerator').insertClauseGenerator, 8 | typeConverter = require('./../insertGenerator').typeConverter; 9 | 10 | module.exports = () => { 11 | describe('typeConverter', () => { 12 | it('should return strings as is', () => { 13 | assert.equal(typeConverter("a"), "a"); 14 | }); 15 | 16 | it('should return numbers as is', () => { 17 | assert.equal(typeConverter(1), 1); 18 | assert.equal(typeConverter(1.5), 1.5); 19 | }); 20 | 21 | it('should wrap timestamps', () => { 22 | assert.deepEqual(typeConverter({"timestamp": 1498787839257}), '2017-06-30 01:57:19') 23 | }); 24 | }); 25 | 26 | 27 | describe('insertClauseGenerator', ()=> { 28 | it('should support empty insert', () => { 29 | assert.equal(insertClauseGenerator('a','b',{}), "INSERT INTO a.b () VALUES ()") 30 | }); 31 | 32 | it('should support single key', () => { 33 | assert.equal(insertClauseGenerator('a','b',{a:"aaa"}), "INSERT INTO a.b (a) VALUES ('aaa')") 34 | }); 35 | 36 | it('should support multiple keys', () => { 37 | assert.equal(insertClauseGenerator('a','b',{a:"aaa", b:3}), "INSERT INTO a.b (a, b) VALUES ('aaa', 3)") 38 | }); 39 | }); 40 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/CsvDriver/CsvNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 1/5/18. 3 | */ 4 | const fs = require('fs'); 5 | const parse = require('csv').parse; 6 | 7 | class CsvNode { 8 | constructor(name, children, args) { 9 | this.name = name; 10 | this.args = args; 11 | this.children = children; 12 | } 13 | 14 | getName() { 15 | return `${this.name}`; 16 | } 17 | 18 | eval(context, ops) { 19 | return new Promise((resolve, reject) => { 20 | const self = this; 21 | 22 | const file = self.args['file'] || "", 23 | auto_parse = self.args['auto_parse'] !== undefined ? self.args['auto_parse'] === 'true' : true, 24 | auto_parse_date = self.args['auto_parse_date'] !== undefined ? self.args['auto_parse_date'] === 'true' : true, 25 | columns = self.args['columns'] !== undefined ? self.args['columns'] === 'true' : true, 26 | delimiter = self.args['delimiter'] || ","; 27 | 28 | const parser = parse({columns, delimiter, auto_parse, auto_parse_date}, function(err, data){ 29 | if (err) 30 | reject(`Error reading CSV file: ${err}`); 31 | resolve(data); 32 | }); 33 | 34 | fs.createReadStream(file).pipe(parser); 35 | 36 | }); 37 | } 38 | } 39 | module.exports = CsvNode; 40 | 41 | 42 | /** TEST **/ 43 | // let node = new CsvNode('Csv.read', [], { 44 | // file: "./sample.csv", 45 | // columns: "true" 46 | // }); 47 | // 48 | // node.eval({}, {}).then(console.log).catch(console.warn); -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/functions/aggregate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/21/17. 3 | */ 4 | 5 | const whereGenerator = require('./../whereGenerator').whereGenerator; 6 | 7 | function aggregate(DBSchema, DBTable, client, args, aggregateFunction) { 8 | //We'll build the SQL query with that string 9 | let queryString = ""; 10 | 11 | //GROUP BY 12 | //If we're grouping, we want to select the group name as well 13 | let coloumns = ""; 14 | if (typeof args['GROUPBY'] == 'string') { 15 | coloumns += `, ${args['GROUPBY']}`; 16 | } 17 | 18 | if (!args['FIELD']) 19 | return new Promise((resolve, reject) => {reject(`PostgreSQL: to use the ${aggregateFunction}() aggregate function, must supply FIELD to aggregate`)}); 20 | 21 | //Base query 22 | queryString += `SELECT ${aggregateFunction.toUpperCase()}(${args['FIELD']}) as ${args['FIELD']} ${coloumns} FROM ${DBSchema}.${DBTable}`; 23 | 24 | //Add where conditions 25 | queryString += whereGenerator(args); 26 | 27 | return new Promise((resolve, reject) => { 28 | client.query(queryString, (err, result) => { 29 | if (err) { 30 | reject(err); 31 | } else { 32 | resolve(result.rows || []); 33 | } 34 | }); 35 | }); 36 | 37 | } 38 | 39 | module.exports = (aggregateFunction) => { 40 | return function (DBSchema, DBTable, client, args) { 41 | return aggregate(DBSchema, DBTable, client, args, aggregateFunction); 42 | } 43 | }; -------------------------------------------------------------------------------- /test/E2E.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/14/17. 3 | */ 4 | const RapidQL = require('./../index'); 5 | 6 | const rql = new RapidQL({ 7 | RapidAPI : { 8 | projectName : 'Demo', 9 | apiKey: 'e0e4f9cc-c076-4cae-ad5b-f5d49beacd8a' 10 | }, 11 | PostgreSQL: { 12 | local:{ 13 | "user":"iddo", 14 | "database":"postgres", 15 | "password":null, 16 | "host":"localhost", 17 | "port":5432 18 | } 19 | }, 20 | Http: { 21 | rateLimit: {count:250, period:1000} // Limits to 250 requests per 1000 seconds 22 | } 23 | }); 24 | 25 | const assert = require("assert"); 26 | 27 | // describe('E2E API Queries', function () { 28 | // this.timeout(10000); 29 | // it('should run', async () => { 30 | // let r = await rql.query(` 31 | // { 32 | // res:RapidAPI.NasaAPI.getPictureOfTheDay() { 33 | // explanation, 34 | // title, 35 | // pic_url:url, 36 | // ? randomFieldThatShouldBeNull 37 | // } 38 | // } 39 | // `, {}); 40 | // assert.equal(r.hasOwnProperty('RapidAPI.NasaAPI.getPictureOfTheDay'), false); 41 | // assert.equal(r.hasOwnProperty('res'), true); 42 | // assert.equal(r.res.hasOwnProperty('explanation'), true); 43 | // assert.equal(r.res.hasOwnProperty('title'), true); 44 | // assert.equal(r.res.hasOwnProperty('pic_url'), true); 45 | // assert.equal(r.res.randomFieldThatShouldBeNull, null); 46 | // }); 47 | // }); -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 6/28/17. 3 | */ 4 | /** 5 | * Remove special selectors (LIMIT, ORDERBY etc...) from query arguments 6 | * @param queryArgs 7 | * @returns {*} 8 | */ 9 | function removeSpecialArgs(queryArgs, specialKeys) { 10 | specialKeys.forEach((key) => { 11 | delete queryArgs[key]; 12 | }); 13 | return queryArgs; 14 | } 15 | 16 | /** 17 | * Returns true if val is a number / string with only a number 18 | * @param val 19 | * @returns {boolean} 20 | */ 21 | function isNumberParseable(val) { 22 | return (!isNaN(parseFloat(val)) && `${parseFloat(val)}` === val) || (typeof val == 'number'); 23 | } 24 | 25 | /** 26 | * Return a string. If value is a string, it'll be quoted. If it's a number or can be parsed as one - it won't. 27 | * @param value 28 | * @returns {string} 29 | */ 30 | function quoteAsNeeded(value) { 31 | return (typeof value == 'number') ? `${value}` : 32 | (isNumberParseable(value)) ? `${parseFloat(value)}` : 33 | (typeof value == 'string') ? `'${value.replace(/'/g, "''")}'` 34 | : `'${value}'`; 35 | } 36 | 37 | /** 38 | * Return an objects values as an array (pollyfill for ES2017's Object.values) 39 | * @param obj 40 | * @returns {Array} 41 | */ 42 | function objValues(obj) { 43 | return Object.keys(obj).map(key => obj[key]); 44 | 45 | } 46 | 47 | module.exports.removeSpecialArgs = removeSpecialArgs; 48 | module.exports.isNumberParseable = isNumberParseable; 49 | module.exports.quoteAsNeeded = quoteAsNeeded; 50 | module.exports.objValues = objValues; -------------------------------------------------------------------------------- /src/Nodes/CompositeNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 12/19/16. 3 | */ 4 | "use strict"; 5 | const ObjectNode = require('./ObjectNode'); 6 | const ArrayNode = require('./ArrayNode'); 7 | 8 | class CompositeNode { 9 | constructor(name, children) { 10 | this.name = name; 11 | this.children = children; 12 | } 13 | 14 | getName() { 15 | return this.name; 16 | } 17 | 18 | //noinspection JSAnnotator 19 | eval(context, ops) { 20 | if (!context[this.getName()]) { 21 | // If object not in context, return error 22 | return new Promise((resolve, reject) => { 23 | reject(`Key "${this.getName()}" does not exist in context`); 24 | }); 25 | } else if (typeof context[this.getName()] !== 'object') { 26 | // If object not an object, return error 27 | return new Promise((resolve, reject) => { 28 | reject(`Cannot expand key "${this.getName()}" of type ${typeof context[this.getName()]}`); 29 | }); 30 | } else if (this.children.length === 0) { 31 | // If object has no children, append all 32 | return new Promise((resolve, reject) => { 33 | resolve(context[this.getName()]); 34 | }); 35 | } 36 | else if(Array.isArray(context[this.getName()])) { 37 | return (new ArrayNode(this.getName(), this.children)).eval(context, ops); 38 | } else { 39 | return (new ObjectNode(this.getName(), this.children)).eval(context, ops); 40 | } 41 | } 42 | } 43 | 44 | module.exports = CompositeNode; -------------------------------------------------------------------------------- /test/node-interface.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 12/30/16. 3 | */ 4 | "use strict"; 5 | 6 | /** 7 | * Tests all nodes to make sure that they are coherent with the interface: 8 | * - Construct with name 9 | * - Get name and make sure it is equal 10 | * - Eval is a function returning a promise 11 | */ 12 | 13 | const assert = require('assert'); 14 | 15 | const NodeTypes = [ 16 | `ArrayNode`, 17 | `CompositeNode`, 18 | `FunctionNode`, 19 | `LeafNode`, 20 | `ObjectNode` 21 | ]; 22 | 23 | describe(`Node interfaces`, () => { 24 | 25 | NodeTypes.forEach((NodeType) => { 26 | describe(`Node - ${NodeType}`, () => { 27 | const NodeClass = require(`../src/Nodes/${NodeType}`); 28 | const _name = 'RapidAPI.a', 29 | _children = [], 30 | _args = {}; 31 | const node = new NodeClass(_name, _children, _args); 32 | 33 | it('should return name properly', () => { 34 | assert.equal(_name, node.getName()); 35 | }); 36 | 37 | describe('eval function', () => { 38 | it('should have an eval() function', () => { 39 | assert.equal('function', typeof node.eval) 40 | }); 41 | 42 | it(`should return a promise when called`, () => { 43 | let p = node.eval({"RapidAPI.a":["ad"]}, {RapidAPI:{}}); 44 | assert.equal(true, (Promise.resolve(p) == p)); //http://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise/38339199#38339199 45 | }); 46 | }); 47 | }); 48 | }); 49 | 50 | }); -------------------------------------------------------------------------------- /test/optional-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/13/17. 3 | */ 4 | 5 | "use strict"; 6 | 7 | const assert = require("assert"); 8 | const LeafNode = require("../src/Nodes/LeafNode"); 9 | const OptionalNode = require("../src/Nodes/OptionalNode"); 10 | const CompositeNode = require("../src/Nodes/CompositeNode"); 11 | 12 | describe("OptionalNode", () => { 13 | const node = new OptionalNode(new LeafNode("a")); 14 | describe('eval function', () => { 15 | it('should have an eval() function', () => { 16 | assert.equal('function', typeof node.eval) 17 | }); 18 | 19 | it(`should return a promise when called`, () => { 20 | let p = node.eval({}, {}); 21 | assert.equal(true, (Promise.resolve(p) == p)); //http://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise/38339199#38339199 22 | }); 23 | }); 24 | 25 | it('should return correct name', () => { 26 | assert.equal(node.getName(), "a"); 27 | }); 28 | 29 | it('should return value on success', async () => { 30 | const res = await node.eval({a:"bbb"}); 31 | assert.equal(res, "bbb"); 32 | }); 33 | 34 | it('should return null on rejection', async () => { 35 | const res = await node.eval({}); 36 | assert.equal(res, null); 37 | }); 38 | 39 | it('should return null on rejection in children', async () => { 40 | const compNode = new OptionalNode(new CompositeNode('a', [ 41 | new LeafNode('d') 42 | ])); 43 | const res = await compNode.eval({ 44 | a: {b:1} 45 | }); 46 | assert.equal(res, null); 47 | }); 48 | }); -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MySQLDriver/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/21/17. 3 | */ 4 | /** 5 | * Created by iddo on 6/28/17. 6 | */ 7 | /** 8 | * Remove special selectors (LIMIT, ORDERBY etc...) from query arguments 9 | * @param queryArgs 10 | * @returns {*} 11 | */ 12 | function removeSpecialArgs(queryArgs, _specialKeys) { 13 | queryArgs = Object.assign({}, queryArgs); 14 | _specialKeys.forEach((key) => { 15 | delete queryArgs[key]; 16 | }); 17 | return queryArgs; 18 | } 19 | 20 | /** 21 | * Returns true if val is a number / string with only a number 22 | * @param val 23 | * @returns {boolean} 24 | */ 25 | function isNumberParseable(val) { 26 | return (!isNaN(parseFloat(val)) && `${parseFloat(val)}` === val) || (typeof val == 'number'); 27 | } 28 | 29 | /** 30 | * Return a string. If value is a string, it'll be quoted. If it's a number or can be parsed as one - it won't. 31 | * @param value 32 | * @returns {string} 33 | */ 34 | function quoteAsNeeded(value) { 35 | return (typeof value == 'number') ? `${value}` : 36 | (isNumberParseable(value)) ? `${parseFloat(value)}` : 37 | (typeof value == 'string') ? `'${value.replace(/'/g, "''")}'` 38 | : `'${value}'`; 39 | } 40 | 41 | /** 42 | * Return an objects values as an array (pollyfill for ES2017's Object.values) 43 | * @param obj 44 | * @returns {Array} 45 | */ 46 | function objValues(obj) { 47 | return Object.keys(obj).map(key => obj[key]); 48 | 49 | } 50 | 51 | module.exports.removeSpecialArgs = removeSpecialArgs; 52 | module.exports.isNumberParseable = isNumberParseable; 53 | module.exports.quoteAsNeeded = quoteAsNeeded; 54 | module.exports.objValues = objValues; 55 | -------------------------------------------------------------------------------- /test/generative/gen-typecasted-leaf-nodes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/27/17. 3 | */ 4 | "use strict"; 5 | 6 | const { describe, it } = require('mocha'); 7 | const { check, gen } = require('mocha-testcheck'); 8 | const { expect, assert } = require('chai'); 9 | require('mocha-testcheck').install(); 10 | 11 | const LeafNode = require('../../src/Nodes/LeafNode'), 12 | CompositeNode = require('../../src/Nodes/CompositeNode'), 13 | RenameNode = require('../../src/Nodes/RenameNode'), 14 | OptionalNode = require('../../src/Nodes/OptionalNode'), 15 | CastedLeafNode = require('../../src/Nodes/CastedLeafNode'), 16 | FunctionNode = require('../../src/Nodes/FunctionNode'); 17 | 18 | const parse = require('../../src/Parser/Parser').parse; 19 | 20 | describe('Generative - CastedLeafNode.typeConverters', () => { 21 | describe('int', () => { 22 | const converter = CastedLeafNode.typeConverters.int; 23 | 24 | check.it('should return ints as ints', gen.int, (i) => { 25 | assert.equal(converter(i),i); 26 | }); 27 | 28 | check.it('should return floats as ints', gen.numberWithin(-100000, 100000), (i) => { 29 | assert.equal(converter(i), parseInt(i)); 30 | }); 31 | 32 | check.it('should parse strings as ints', gen.int, (i) => { 33 | assert.equal(converter(`${i}`),i); 34 | }); 35 | }); 36 | 37 | describe('float', () => { 38 | const converter = CastedLeafNode.typeConverters.float; 39 | 40 | check.it('should return floats as floats', gen.numberWithin(-100000, 100000), (i) => { 41 | assert.equal(converter(i),i); 42 | }); 43 | 44 | check.it('should parse strings as floats', gen.numberWithin(-100000, 100000), (i) => { 45 | assert.equal(converter(`${i}`),i); 46 | }); 47 | }); 48 | }); -------------------------------------------------------------------------------- /bin/rql.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Created by iddo on 3/13/17. 4 | */ 5 | 6 | const CONFIG_FILE_NAME = ".rqlconfig", 7 | CONTEXT_FILE_NAME = ".rqlcontext"; 8 | 9 | const fs = require('fs'), 10 | RQL = require('./../index'); 11 | 12 | let configs = {}, baseContext = {}; 13 | 14 | const { readJsonFile } = require('./utils'); 15 | const program = require('commander'); 16 | 17 | 18 | program 19 | .version('0.0.1') 20 | .description("RapidQL Command Line Tool") 21 | .command("query [query]", "perform and rql query") 22 | .parse(process.argv); 23 | 24 | 25 | //Try and read configs 26 | // fs.readFile(CONFIG_FILE_NAME, 'utf8', function(err, data) { 27 | // if(!err && data) { 28 | // try { 29 | // configs = JSON.parse(data); 30 | // } catch (e) { 31 | // throw `Error reading config file: ${e}`; 32 | // } 33 | // } 34 | // //Try and base context 35 | // fs.readFile(CONTEXT_FILE_NAME, 'utf8', function(err, data) { 36 | // if(!err && data) { 37 | // try { 38 | // baseContext = JSON.parse(data); 39 | // } catch (e) { 40 | // throw `Error reading context file: ${e}`; 41 | // } 42 | // } 43 | // 44 | // //Read query 45 | // if (process.argv.length < 3) 46 | // throw `Please provide query`; 47 | // let queryString = process.argv[2]; 48 | // const rqlClient = new RQL(configs); 49 | // rqlClient.query(queryString, baseContext) 50 | // .catch((err) => { 51 | // console.warn(`Error performing query: \n${err}`); 52 | // }) 53 | // .then((res) => { 54 | // console.log(JSON.stringify(res, null, 4)); 55 | // process.exit(0) 56 | // }); 57 | // }); 58 | // }); -------------------------------------------------------------------------------- /test/leaf-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 12/30/16. 3 | */ 4 | "use strict"; 5 | 6 | const assert = require("assert"); 7 | const LeafNode = require("../src/Nodes/LeafNode"); 8 | 9 | describe("LeafNode", () => { 10 | let _name = "a"; 11 | let _val = 'b'; 12 | let _context = { 13 | [_name] : _val 14 | }; 15 | let ln = new LeafNode('a'); 16 | it('should return name properly', () => { 17 | assert.equal('a', ln.getName()); 18 | }); 19 | 20 | describe('eval() validity', () => { 21 | it('should return value if in context', (done) => { 22 | ln.eval(_context).then((val) => { 23 | assert.equal(val, _val); 24 | done(); 25 | }).catch(done); 26 | }); 27 | 28 | it('should return error if value is not in context', (done) => { 29 | ln.eval({}).then((val) => { //Running on empty context 30 | done("Should not resolve"); 31 | }).catch((err) => { 32 | assert(""+err, "Name a does not exist in context {}"); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should return deep value from object', (done) => { 38 | (new LeafNode('a.b')).eval({ 39 | a : { 40 | b : 12 41 | } 42 | }).then((val) => { 43 | assert.equal(val, 12); 44 | done(); 45 | }).catch((err) => { 46 | done(`Failed with error: ${err}`); 47 | }); 48 | }); 49 | 50 | it('should resolve path with points directly when possible', (done) => { 51 | (new LeafNode('a.b')).eval({ 52 | "a.b": 12 53 | }).then((val) => { 54 | assert.equal(val, 12); 55 | done(); 56 | }).catch((err) => { 57 | done(`Failed with error: ${err}`); 58 | }); 59 | }); 60 | }); 61 | }); -------------------------------------------------------------------------------- /src/Nodes/ObjectNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Iddo on 12/17/2016. 3 | */ 4 | "use strict"; 5 | 6 | const { createMixedContext } = require('./utils'); 7 | 8 | class ObjectNode { 9 | constructor(name, children) { 10 | this.name = name; 11 | this.children = children; 12 | } 13 | 14 | getName() { 15 | return this.name; 16 | } 17 | 18 | //noinspection JSAnnotator 19 | eval(context, ops) { 20 | const ctx = createMixedContext(context, context[this.getName()] || {}); 21 | let res = {}; 22 | let promises = []; 23 | this.children.forEach((child) => { 24 | promises.push(new Promise((resolve, reject) => { 25 | child.eval(ctx, ops) 26 | .then((val) => { 27 | // Treatment for FlatObjectNodes - need to add multiple properties 28 | if (child.constructor.name === 'FlatObjectNode' && typeof val === 'object') 29 | res = Object.assign(res, val); 30 | else 31 | res[child.getName()] = val; 32 | resolve(); 33 | }) 34 | .catch(reject); 35 | })); 36 | }); 37 | return new Promise((resolve, reject) => { 38 | Promise.all(promises) 39 | .then(()=> { 40 | resolve(res); 41 | }) 42 | .catch(reject); 43 | }); 44 | } 45 | } 46 | 47 | module.exports = ObjectNode; 48 | 49 | //Playground: 50 | /*const LeafNode = require("./LeafNode"); 51 | let context = { 52 | a: 1, 53 | b: { 54 | c:3, 55 | d:4 56 | } 57 | }; 58 | 59 | let on = new ObjectNode("b", [ 60 | new LeafNode("d"), 61 | new LeafNode("c") 62 | ]); 63 | 64 | on.eval(context) 65 | .then((val) => { 66 | console.log(val); 67 | }) 68 | .catch((error) => { 69 | console.warn(error); 70 | }); 71 | */ -------------------------------------------------------------------------------- /test/parserE2E.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/13/17. 3 | */ 4 | "use strict"; 5 | 6 | const assert = require('assert'), 7 | LeafNode = require('../src/Nodes/LeafNode'), 8 | CompositeNode = require('../src/Nodes/CompositeNode'), 9 | RenameNode = require('../src/Nodes/RenameNode'), 10 | OptionalNode = require('../src/Nodes/OptionalNode'), 11 | FunctionNode = require('../src/Nodes/FunctionNode'); 12 | 13 | 14 | const parse = require('../src/Parser/Parser').parse; 15 | 16 | 17 | describe('Parser E2E', () => { 18 | it('should match', async () => { 19 | let val = await parse(` 20 | { 21 | a 22 | } 23 | `); 24 | assert.deepEqual(val, [ 25 | new LeafNode('a') 26 | ]); 27 | }); 28 | 29 | it('should match', async () => { 30 | let val = await parse(` 31 | { 32 | a { 33 | b, 34 | c { 35 | d 36 | } 37 | } 38 | } 39 | `); 40 | assert.deepEqual(val, [ 41 | new CompositeNode('a', [ 42 | new LeafNode('b'), 43 | new CompositeNode('c', [ 44 | new LeafNode('d') 45 | ]) 46 | ]) 47 | ]); 48 | }); 49 | 50 | it('should match', async () => { 51 | let val = await parse(` 52 | { 53 | ?a { 54 | ll:b, 55 | c { 56 | ?er:d 57 | } 58 | } 59 | } 60 | `); 61 | assert.deepEqual(val, [ 62 | new OptionalNode(new CompositeNode('a', [ 63 | new RenameNode('ll', new LeafNode('b')), 64 | new CompositeNode('c', [ 65 | new OptionalNode(new RenameNode('er', new LeafNode('d'))) 66 | ]) 67 | ])) 68 | ]); 69 | }); 70 | }); -------------------------------------------------------------------------------- /bin/rql-query.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 1/28/18. 3 | */ 4 | const program = require('commander'); 5 | const { readFile, readJsonFile } = require('./utils'); 6 | const RQL = require('./../index'); 7 | 8 | program 9 | .option('--stdin-context', 'Stream context in stdin') 10 | .option('--parse-context [format]', "Format of context", "json") 11 | .option('--context-file [file]', "File to read context fromh. Defaults to .rqlcontext", ".rqlcontext") 12 | .option('--config-file [file]', "File to read configurations from. Defaults to .rqlconfig", ".rqlconfig") 13 | .option('-f, --query-file', "Argument is a file to read query from, rather than the query itself") 14 | .parse(process.argv); 15 | 16 | async function getContext() { 17 | let contextData; 18 | if (!program.stdinContext) { 19 | contextData = await readFile(program.contextFile); 20 | } 21 | 22 | if (program.parseContext.toLowerCase() === "json") { 23 | return JSON.parse(contextData); 24 | } 25 | 26 | if (program.parseContext.toLowerCase() === "csv") { 27 | const rqlClient = new RQL({}); 28 | // feeding like a mad dog 29 | return await rqlClient.query(`{ csv:Csv.read(file:csvFile){} }`, { 30 | csvFile : program.contextFile 31 | }); 32 | } 33 | } 34 | 35 | async function getConfig() { 36 | return await readJsonFile(program.configFile); 37 | } 38 | 39 | async function getQueryString() { 40 | let query = program.args[0]; 41 | 42 | if (program.queryFile) { 43 | return await readFile(query); 44 | } else { 45 | return query; 46 | } 47 | } 48 | 49 | async function execQuery() { 50 | const context = await getContext(); 51 | const config = await getConfig(); 52 | let query = await getQueryString(); 53 | 54 | const rqlClient = new RQL(config); 55 | 56 | const result = await rqlClient.query(query, context); 57 | 58 | return JSON.stringify(result, null, 4); 59 | } 60 | 61 | execQuery().then(console.log).catch(console.warn); 62 | 63 | // samples: 64 | // rql query --context-file=data.csv --parse-context=csv --query-file q4.rql -------------------------------------------------------------------------------- /test/flat-object-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 4/19/18. 3 | */ 4 | const chai = require("chai"); 5 | chai.use(require("chai-as-promised")); 6 | const { expect, assert } = chai; 7 | const { describe, it } = require('mocha'); 8 | const ObjectNode = require('../src/Nodes/ObjectNode'); 9 | const FlatObjectNode = require('../src/Nodes/FlatObjectNode'); 10 | const LeafNode = require('../src/Nodes/LeafNode'); 11 | 12 | describe('ObjectNode', () => { 13 | 14 | it('should handle valid objects properly', async () => { 15 | let n = new FlatObjectNode (new ObjectNode('obj', [ 16 | new LeafNode('a'), 17 | new LeafNode('b') 18 | ])); 19 | 20 | let v = await n.eval({ 21 | obj: { 22 | a: 'AA', 23 | b: 'BB' 24 | } 25 | }); 26 | 27 | assert.equal(v.a, 'AA'); 28 | assert.equal(v.b, 'BB'); 29 | }); 30 | 31 | it('should support referencing properties from outside object within object', async () => { 32 | let n = new FlatObjectNode (new ObjectNode('obj', [ 33 | new LeafNode('a'), 34 | new LeafNode('b') 35 | ])); 36 | 37 | let v = await n.eval({ 38 | obj: { 39 | a: 'AA' 40 | }, 41 | b: 'BB' 42 | }); 43 | 44 | assert.equal(v.a, 'AA'); 45 | assert.equal(v.b, 'BB'); 46 | }); 47 | 48 | it('should be flat (aka show as properties vs as object) when placed in Object Node', async () => { 49 | let n = new ObjectNode('root', [ 50 | new FlatObjectNode (new ObjectNode('obj', [ 51 | new LeafNode('a'), 52 | new LeafNode('b') 53 | ])) 54 | ]); 55 | 56 | let v = await n.eval({ 57 | root: { 58 | obj: { 59 | a: 'AA', 60 | b: 'BB' 61 | } 62 | } 63 | }); 64 | 65 | assert.deepEqual(v, { 66 | a: 'AA', 67 | b: 'BB' 68 | }); 69 | }); 70 | }); 71 | 72 | -------------------------------------------------------------------------------- /src/Nodes/CastedLeafNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/14/17. 3 | */ 4 | "use strict"; 5 | 6 | const typeConverters = { 7 | int(val) { 8 | if (typeof val === 'number' || typeof val === 'string') { 9 | if(!isNaN(parseInt(val))) 10 | return parseInt(val); 11 | else 12 | throw `String ${val} cannot be casted into an int`; 13 | } else { 14 | throw `TypeError: Type ${typeof val} cannot be casted to an int`; 15 | } 16 | }, 17 | float(val) { 18 | if (typeof val === 'number' || typeof val === 'string') { 19 | if(!isNaN(parseFloat(val))) 20 | return parseFloat(val); 21 | else 22 | throw `String ${val} cannot be casted into an float`; 23 | } else { 24 | throw `TypeError: Type ${typeof val} cannot be casted to an float`; 25 | } 26 | }, 27 | date(val) { 28 | // Special check for boolean 29 | if (typeof val === 'boolean') 30 | throw `TypeError: cannot convert ${val} to a date`; 31 | 32 | // Convert to date object 33 | const tempDate = new Date(val); 34 | 35 | // Check it is valid 36 | if (!isNaN(tempDate.getTime())) { 37 | // Convert to 0 timezone 38 | return tempDate; 39 | } else { 40 | throw `TypeError: cannot convert ${val} to a date`; 41 | } 42 | } 43 | }; 44 | 45 | class CastedLeafNode { 46 | constructor(type, innerNode) { 47 | this.type = type.toLowerCase(); 48 | if (!typeConverters.hasOwnProperty(this.type)) 49 | throw `TypeCastedLeaf node does not support casting to type ${type}`; 50 | this.innerNode = innerNode; 51 | } 52 | 53 | getName() { 54 | return this.innerNode.getName(); 55 | } 56 | 57 | //noinspection JSAnnotator 58 | async eval(context, ops) { 59 | const innerValue = await this.innerNode.eval(context, ops); 60 | return typeConverters[this.type](innerValue); 61 | } 62 | } 63 | 64 | module.exports = CastedLeafNode; 65 | module.exports.typeConverters = typeConverters; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /README.md 2 | # Created by .ignore support plugin (hsz.mobi) 3 | ### JetBrains template 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 6 | 7 | # User-specific stuff: 8 | .idea/workspace.xml 9 | .idea/tasks.xml 10 | 11 | # Sensitive or high-churn files: 12 | .idea/dataSources/ 13 | .idea/dataSources.ids 14 | .idea/dataSources.xml 15 | .idea/dataSources.local.xml 16 | .idea/sqlDataSources.xml 17 | .idea/dynamic.xml 18 | .idea/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/gradle.xml 22 | .idea/libraries 23 | 24 | # Mongo Explorer plugin: 25 | .idea/mongoSettings.xml 26 | 27 | ## File-based project format: 28 | *.iws 29 | 30 | ## Plugin-specific files: 31 | 32 | # IntelliJ 33 | /out/ 34 | 35 | # mpeltonen/sbt-idea plugin 36 | .idea_modules/ 37 | 38 | # JIRA plugin 39 | atlassian-ide-plugin.xml 40 | 41 | # Crashlytics plugin (for Android Studio and IntelliJ) 42 | com_crashlytics_export_strings.xml 43 | crashlytics.properties 44 | crashlytics-build.properties 45 | fabric.properties 46 | ### Node template 47 | # Logs 48 | logs 49 | *.log 50 | npm-debug.log* 51 | 52 | # Runtime data 53 | pids 54 | *.pid 55 | *.seed 56 | *.pid.lock 57 | 58 | # Directory for instrumented libs generated by jscoverage/JSCover 59 | lib-cov 60 | 61 | # Coverage directory used by tools like istanbul 62 | coverage 63 | 64 | # nyc test coverage 65 | .nyc_output 66 | 67 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 68 | .grunt 69 | 70 | # node-waf configuration 71 | .lock-wscript 72 | 73 | # Compiled binary addons (http://nodejs.org/api/addons.html) 74 | build/Release 75 | 76 | # Dependency directories 77 | node_modules 78 | jspm_packages 79 | 80 | # Optional npm cache directory 81 | .npm 82 | 83 | # Optional eslint cache 84 | .eslintcache 85 | 86 | # Optional REPL history 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | *.tgz 91 | 92 | # Yarn Integrity file 93 | .yarn-integrity 94 | 95 | playground.js 96 | samples.js 97 | playground 98 | 99 | # .DS_Store 100 | .DS_Store 101 | -------------------------------------------------------------------------------- /test/composite-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/28/17. 3 | */ 4 | 5 | const {assert} = require('chai'); 6 | const { describe, it } = require('mocha'); 7 | const ArrayNode = require('../src/Nodes/ArrayNode'); 8 | const CompositeNode = require('../src/Nodes/CompositeNode'); 9 | const LeafNode = require('../src/Nodes/LeafNode'); 10 | 11 | describe("CompositeNode", () => { 12 | it('should throw an error if object is not an array / object', (done) => { 13 | let n = new CompositeNode('arr', []); 14 | n.eval({arr: "asd"}, {}) 15 | .then(() => { 16 | done('Should have thrown an error'); 17 | }) 18 | .catch((err) => { 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should throw an error if object is not in context', (done) => { 24 | let n = new CompositeNode('arr', []); 25 | n.eval({}, {}) 26 | .then(() => { 27 | done('Should have thrown an error'); 28 | }) 29 | .catch((err) => { 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should materialize into array - implicit', async () => { 35 | let n = new CompositeNode('arr', []); 36 | let arr = ["aa", "bb", "cc"]; 37 | let res = await n.eval({arr:arr}, {}); 38 | assert.deepEqual(res, arr); 39 | }); 40 | 41 | it('should materialize into array - explicit', async () => { 42 | let n = new CompositeNode('arr', [ 43 | new LeafNode('a') 44 | ]); 45 | let arr = [{a:1}, {a:2}, {a:3}]; 46 | let res = await n.eval({arr:arr}, {}); 47 | assert.deepEqual(res, arr); 48 | }); 49 | 50 | it('should materialize into object - implicit', async () => { 51 | let n = new CompositeNode('obj', []); 52 | let obj = {a:'aa', b:'bb'}; 53 | let res = await n.eval({obj: obj}, {}); 54 | assert.deepEqual(res, obj); 55 | }); 56 | 57 | it('should materialize into object - implicit', async () => { 58 | let n = new CompositeNode('obj', [ 59 | new LeafNode('a'), 60 | new LeafNode('b') 61 | ]); 62 | let obj = {a:'aa', b:'bb'}; 63 | let res = await n.eval({obj: obj}, {}); 64 | assert.deepEqual(res, obj); 65 | }); 66 | }); -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/RedisDriver/RedisNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/19/17. 3 | */ 4 | const SEP_CHAR = '.'; 5 | 6 | const redis = require("redis"); 7 | 8 | global._redis_clients = {}; 9 | function getClient(DBName, DBConfig) { 10 | if (global._redis_clients.hasOwnProperty(DBName)) 11 | return global._redis_clients[DBName]; 12 | else { 13 | global._redis_clients[DBName] = redis.createClient(DBConfig); 14 | return getClient(DBName, DBConfig); 15 | } 16 | } 17 | 18 | 19 | // Types: 20 | const string = require('./types/string'); 21 | const hash = require('./types/hash'); 22 | const generic = require('./types/generic'); 23 | const functions = { 24 | get : string.get, 25 | set : string.set, 26 | hmset : hash.hmset, 27 | hgetall : hash.hgetall, 28 | type : generic.type, 29 | find : generic.find 30 | }; 31 | 32 | 33 | 34 | class RedisNode { 35 | constructor(name, children, args) { 36 | this.name = name; 37 | this.args = args; 38 | this.children = children; 39 | } 40 | 41 | getName() { 42 | return this.name; 43 | } 44 | 45 | async eval(context, ops) { 46 | const self = this; 47 | 48 | const tokenizedName = this.name.split(SEP_CHAR); 49 | const DBName = tokenizedName[1], 50 | operation = tokenizedName[2]; 51 | 52 | //Check operation exists 53 | if (!functions.hasOwnProperty(operation)) 54 | throw `Operation Error: operation ${operation} does not exist / is not supported`; 55 | 56 | 57 | //Create DB connection 58 | //Check configs exist 59 | if (!ops.hasOwnProperty('Redis')) { 60 | throw `Missing configs: Redis settings are missing`; 61 | } else if (!ops['Redis'].hasOwnProperty(DBName)) { 62 | throw `Missing configs: Redis settings for DB ${DBName} are missing`; 63 | } else { 64 | const DBConfigs = ops['Redis'][DBName]; 65 | 66 | const client = getClient(DBName, DBConfigs); 67 | 68 | //Route different functions 69 | const res = await functions[operation](client, self.args); 70 | return res; 71 | } 72 | } 73 | } 74 | 75 | module.exports = RedisNode; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/HttpDriver/test/http.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/27/17. 3 | */ 4 | const { expect, assert } = require('chai'); 5 | const HttpNode = require('./../HttpNode'); 6 | 7 | module.exports = () => { 8 | describe('get request', () => { 9 | it('should perform simple get request', async () => { 10 | let n = new HttpNode('Http.get', [], { 11 | url: "https://httpbin.org/get", 12 | params: { 13 | a: "b", 14 | c: 1 15 | } 16 | }); 17 | 18 | let res = await n.eval({}, {}); 19 | assert.equal(res.args.a, "b"); 20 | assert.equal(res.args.c, 1); 21 | }).timeout(10000); 22 | 23 | it("should properly send headers", async () => { 24 | let n = new HttpNode('Http.get', [], { 25 | url: "https://httpbin.org/get", 26 | headers: { 27 | "X-Test": "b" 28 | } 29 | }); 30 | 31 | let res = await n.eval({}, {}); 32 | assert.equal(res.headers["X-Test"], "b"); 33 | assert.equal(res.headers["Host"], "httpbin.org"); 34 | }).timeout(10000); 35 | }); 36 | 37 | describe('post request', () => { 38 | it('should send form data', async () => { 39 | let n = new HttpNode('Http.post', [], { 40 | url: "https://httpbin.org/post", 41 | form: { 42 | a: "b", 43 | c: 1 44 | } 45 | }); 46 | 47 | let res = await n.eval({}, {}); 48 | assert.equal(res.form.a, "b"); 49 | assert.equal(res.form.c, 1); 50 | assert.equal(res.json, null); 51 | assert.equal(res.url, "https://httpbin.org/post"); 52 | }).timeout(10000); 53 | 54 | it('should send json data', async () => { 55 | let n = new HttpNode('Http.post', [], { 56 | url: "https://httpbin.org/post", 57 | json: { 58 | a: "b", 59 | c: 1 60 | } 61 | }); 62 | 63 | let res = await n.eval({}, {}); 64 | assert.equal(res.json.a, "b"); 65 | assert.equal(res.json.c, 1); 66 | assert.equal(res.url, "https://httpbin.org/post"); 67 | }).timeout(10000); 68 | }); 69 | }; -------------------------------------------------------------------------------- /src/Nodes/ArrayNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 12/19/16. 3 | */ 4 | "use strict"; 5 | const ObjectNode = require('./ObjectNode'); 6 | const LeafNode = require('./LeafNode'); 7 | const { createMixedContext } = require('./utils'); 8 | 9 | class ArrayNode { 10 | constructor(name, children) { 11 | this.name = name; 12 | this.children = children; 13 | } 14 | 15 | getName() { 16 | return this.name; 17 | } 18 | 19 | //noinspection JSAnnotator 20 | eval(context, ops) { 21 | let arr = context[this.getName()]; 22 | let promises = []; 23 | if (!Array.isArray(arr)) { 24 | return Promise.reject(`TypeError: element ${this.getName()} in context is not an array`); 25 | } else { 26 | 27 | for (let i in arr) { 28 | /** 29 | * If iterating over array of objects, inner context is the object in the array. 30 | * If iterating over array of scalars ([s]), inner context is the object {[this.name]:s} 31 | */ 32 | let obj = arr[i]; 33 | 34 | let innerContext; 35 | 36 | let innerNode; 37 | if(typeof obj === "object") { 38 | innerContext = createMixedContext(context, { 39 | [this.getName()] : obj 40 | }); 41 | innerNode = new ObjectNode(this.getName(), this.children); 42 | } else if (this.children.length > 0) { 43 | innerContext = createMixedContext(context, { 44 | [this.getName()] : { 45 | [this.getName()] : obj 46 | } 47 | }); 48 | innerNode = new ObjectNode(this.getName(), this.children); 49 | } else { 50 | innerContext = createMixedContext(context, { 51 | [this.getName()] : obj 52 | }); 53 | innerNode = new LeafNode(this.getName()); 54 | } 55 | promises.push(innerNode.eval(innerContext, ops)); 56 | } 57 | 58 | return Promise.all(promises); 59 | } 60 | 61 | 62 | } 63 | } 64 | 65 | module.exports = ArrayNode; -------------------------------------------------------------------------------- /src/Nodes/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 8/31/17. 3 | */ 4 | 5 | /** 6 | * Find an object's property from a path 7 | * @param path dot notation path (a.b.c) 8 | * @param object object for property to be pulled from 9 | * @returns {*} 10 | */ 11 | module.exports.resolve = (path, object) => { 12 | if (object.hasOwnProperty(path)) 13 | return object[path]; 14 | return path.split('.').reduce(function (prev, curr) { 15 | if (prev) 16 | //if (prev.hasOwnProperty(curr)) 17 | if (prev[curr] !== undefined) 18 | return prev[curr]; 19 | throw `Name ${path} does not exist in context ${JSON.stringify(object, null, 4)}`; 20 | }, object); 21 | }; 22 | 23 | /** 24 | * Create an object that is a combination of an inner context and outer context. Will strive to return elements from inner context, defaulting to outer if not in inner 25 | * @param outerContext 26 | * @param innerContext 27 | * @returns {Proxy} 28 | */ 29 | module.exports.createMixedContext = (outerContext, innerContext) => { 30 | 31 | if (typeof innerContext != 'object' || typeof outerContext != 'object') 32 | throw `Contexts must be of object type`; 33 | 34 | const handler = { 35 | get: (target, name) => { 36 | // Lookup order: innerContext > outerContext > undefined 37 | // JS is ugly. Using "in" instead of !== undefined would have been MUCH nicer, but in is not supported with Proxy... (WHY????) 38 | return target.innerContext[name] !== undefined ? target.innerContext[name] 39 | : target.outerContext[name] !== undefined ? target.outerContext[name] 40 | : undefined; 41 | }, 42 | has: (target, name) => { 43 | return name in target.innerContext || name in target.outerContext; 44 | }, 45 | getOwnPropertyDescriptor: (target, name) => { 46 | if (name in target.innerContext || name in target.outerContext) { 47 | return { 48 | enumerable : true, 49 | configurable : true, 50 | writable : true 51 | }; 52 | } 53 | return undefined; 54 | } 55 | }; 56 | 57 | // Add support for native functions 58 | // Proxy.prototype.hasOwnProperty = function (name) { 59 | // return this[name] !== undefined ? this[name] : false; 60 | // }; 61 | 62 | return new Proxy({ 63 | innerContext, 64 | outerContext 65 | }, handler); 66 | }; -------------------------------------------------------------------------------- /test/array-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/28/17. 3 | */ 4 | 5 | const {assert} = require('chai'); 6 | const { describe, it } = require('mocha'); 7 | const ArrayNode = require('../src/Nodes/ArrayNode'); 8 | const LeafNode = require('../src/Nodes/LeafNode'); 9 | 10 | describe("ArrayNode", () => { 11 | it('should throw an error if object is not an array', (done) => { 12 | let n = new ArrayNode('arr', []); 13 | n.eval({arr: "asd"}, {}) 14 | .then(() => { 15 | done('Should have thrown an error'); 16 | }) 17 | .catch(() => { 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should throw an error if object is not in context', (done) => { 23 | let n = new ArrayNode('arr', []); 24 | n.eval({}, {}) 25 | .then(() => { 26 | done('Should have thrown an error'); 27 | }) 28 | .catch(() => { 29 | done(); 30 | }); 31 | }); 32 | 33 | it('should support object array', async () => { 34 | let n = new ArrayNode('arr', [ 35 | new LeafNode('a') 36 | ]); 37 | let arr = [{a:1}, {a:2}, {a:3}]; 38 | let res = await n.eval({arr:arr}, {}); 39 | assert.deepEqual(res, arr); 40 | }); 41 | 42 | it('should support referencing properties from outside context in object array', async () => { 43 | /* 44 | { 45 | arr { 46 | a, 47 | b 48 | } 49 | } 50 | */ 51 | let n = new ArrayNode('arr', [ 52 | new LeafNode('a'), 53 | new LeafNode('b') 54 | ]); 55 | let arrIn = [{a:1}, {a:2}, {a:3}]; 56 | let arrComp = [{a:1, b: "b"}, {a:2, b: "b"}, {a:3, b: "b"}]; 57 | let res = await n.eval({arr:arrIn, b: "b"}, {}); 58 | assert.deepEqual(res, arrComp); 59 | }); 60 | 61 | it('should support string array - explicit', async () => { 62 | let n = new ArrayNode('arr', [ 63 | new LeafNode('arr') 64 | ]); 65 | let arr = ["aa", "bb", "cc"]; 66 | let res = await n.eval({arr:arr}, {}); 67 | assert.deepEqual(res, [{arr:'aa'},{arr:'bb'},{arr:'cc'}]); 68 | }); 69 | 70 | it('should support string array - implicit', async () => { 71 | let n = new ArrayNode('arr', []); 72 | let arr = ["aa", "bb", "cc"]; 73 | let res = await n.eval({arr:arr}, {}); 74 | assert.deepEqual(res, arr); 75 | }); 76 | }); -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MongoDBDriver/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/20/17. 3 | */ 4 | const flattenObject = (obj) => { 5 | let toReturn = {}; 6 | 7 | for (let key in obj) { 8 | if(obj.hasOwnProperty(key)) { 9 | if(typeof obj[key] !== 'object') { 10 | toReturn[key] = obj[key]; 11 | } else if (obj[key]._bsontype !== 'ObjectID') { 12 | let internalObj = flattenObject(obj[key]); 13 | 14 | for (let internalKey in internalObj) { 15 | if (internalObj.hasOwnProperty(internalKey)) { 16 | toReturn[`${key}.${internalKey}`] = internalObj[internalKey]; 17 | } 18 | } 19 | } else { 20 | toReturn[key] = obj[key]; 21 | } 22 | } 23 | } 24 | 25 | return toReturn; 26 | }; 27 | module.exports.flattenObject = flattenObject; 28 | 29 | 30 | const ObjectId = require('mongodb').ObjectId; 31 | module.exports.ObjectId = ObjectId; 32 | 33 | /** 34 | * This function takes a MongoDB query and converts {"$oid":"5dfg5k6jh4k645l6h4"} to ObjectIds. 35 | * @param obj 36 | * @returns {*} 37 | */ 38 | function convertObjectIds(obj) { 39 | for (let key in obj) { 40 | if (typeof obj[key] === 'object') 41 | obj[key] = convertObjectIds(obj[key]); 42 | else { 43 | if (key === "$oid") 44 | return ObjectId(obj[key]); 45 | } 46 | } 47 | return obj; 48 | } 49 | module.exports.convertObjectIds = convertObjectIds; 50 | 51 | 52 | /** 53 | * This function takes a MongoDB document and converts ObjectIDs to Strings 54 | * @param obj 55 | * @returns {*} 56 | */ 57 | function unconvertObjectIds(obj) { 58 | for (let key in obj) { 59 | if(obj.hasOwnProperty(key) && obj[key] !== null && obj[key] !== undefined) 60 | if (typeof obj[key] === 'object') { 61 | if (obj[key]._bsontype == 'ObjectID') { 62 | obj[key] = obj[key]+""; 63 | } else { 64 | obj[key] = unconvertObjectIds(obj[key]); 65 | } 66 | } 67 | } 68 | return obj; 69 | } 70 | module.exports.unconvertObjectIds = unconvertObjectIds; 71 | 72 | const specialKeys = ["FIELD"]; 73 | function removeSpecialArgs(queryArgs) { 74 | queryArgs = Object.assign({}, queryArgs); 75 | specialKeys.forEach((key) => { 76 | delete queryArgs[key]; 77 | }); 78 | return queryArgs; 79 | } 80 | module.exports.removeSpecialArgs = removeSpecialArgs; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MongoDBDriver/MongoDBNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/19/17. 3 | */ 4 | const SEP_CHAR = '.'; 5 | 6 | const MongoClient = require('mongodb').MongoClient; 7 | 8 | const functions = { 9 | find: require('./functions/find'), 10 | sum: require('./functions/aggregate')('sum'), 11 | avg: require('./functions/aggregate')('avg'), 12 | min: require('./functions/aggregate')('min'), 13 | max: require('./functions/aggregate')('max'), 14 | insert: require('./functions/insert') 15 | }; 16 | 17 | global._mongodb_clients = {}; 18 | function getClient(DBName, DBConfigs) { 19 | return new Promise((resolve, reject) => { 20 | if (global._mongodb_clients.hasOwnProperty(DBName)) 21 | resolve(global._mongodb_clients[DBName]); 22 | else 23 | MongoClient.connect(DBConfigs, (err, db) => { 24 | if (err) 25 | reject(`MongoDB: Connection error: ${err}`); 26 | else { 27 | global._mongodb_clients[DBName] = db; 28 | resolve(global._mongodb_clients[DBName]); 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | class MongoDBNode { 35 | constructor(name, children, args) { 36 | this.name = name; 37 | this.args = args; 38 | this.children = children; 39 | } 40 | 41 | getName() { 42 | return this.name; 43 | } 44 | 45 | //noinspection JSAnnotator 46 | async eval(context, ops) { 47 | const self = this; 48 | 49 | const tokenizedName = this.name.split(SEP_CHAR); 50 | // MongoDB.local.users.find() 51 | const DBName = tokenizedName[1], 52 | DBTable = tokenizedName[2], 53 | operation = tokenizedName[3]; 54 | 55 | if(!functions.hasOwnProperty(operation)) 56 | throw `Operation Error: operation ${operation} does not exist / is not supported`; 57 | 58 | //Create DB connection 59 | //Check configs exist 60 | if (!ops.hasOwnProperty('MongoDB')) { 61 | throw `Missing configs: MongoDB settings are missing`; 62 | } else if (!ops['MongoDB'].hasOwnProperty(DBName)) { 63 | throw `Missing configs: MongoDB settings for DB ${DBName} are missing`; 64 | } else { 65 | const DBConfigs = ops['MongoDB'][DBName]; 66 | 67 | const db = await getClient(DBName, DBConfigs); 68 | 69 | //Route different functions 70 | return await functions[operation](DBTable, db, self.args); 71 | } 72 | } 73 | } 74 | 75 | module.exports = MongoDBNode; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MapReduce/MapReduceNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 8/27/17. 3 | */ 4 | const SEP_CHAR = '.'; 5 | const NODE_NAME = 'MapReduce'; 6 | const mapReduce = require('./mapReduce').mapReduce; 7 | 8 | class MapReduceNode { 9 | constructor (name, children, args) { 10 | // Check # of children 11 | if (0 >= children.length) { 12 | throw `MapReduce Error: node needs 1 child. 0 found.`; 13 | } else if (1 < children.length) { 14 | console.warn(`MapReduce Warning: MapReduce node got ${children.length} children. Only first one - ${children[0].getName()} will be used. Children: `); 15 | children.map((child) => {console.log (`- ${child.getName()}`)}); 16 | } 17 | 18 | // Get pipe name 19 | const tokenizedName = name.split(SEP_CHAR); 20 | // Check there is a pipe object identifier 21 | if (tokenizedName.length < 2) 22 | throw `MapReduce Error: No pipe name. Should be MapReduce.pipeName(){}`; 23 | const pipeName = tokenizedName[1]; 24 | 25 | this.name = name; 26 | this.args = args; 27 | this.children = children; 28 | this.pipeName = pipeName; 29 | } 30 | 31 | getName () { 32 | return `${this.name}`; 33 | } 34 | 35 | async eval (context, ops) { 36 | // Make sure pipe exists in ops 37 | if (!ops.hasOwnProperty("MapReduce") || 38 | !ops['MapReduce'].hasOwnProperty(this.pipeName) || 39 | typeof ops['MapReduce'][this.pipeName] !== 'object') 40 | throw `MapReduce Error: Options object doesn't have MapReduce configurations / configurations are invalid.`; 41 | 42 | const pipe = ops['MapReduce'][this.pipeName]; 43 | 44 | // Make sure pipe has right data 45 | if(!pipe.hasOwnProperty("map") || typeof pipe["map"] !== 'function') 46 | throw `MapReduce Error: pipe does not have "map" function / parameter is not a valid function`; 47 | if(!pipe.hasOwnProperty("reduce") || typeof pipe["reduce"] !== 'function') 48 | throw `MapReduce Error: pipe does not have "reduce" function / parameter is not a valid function`; 49 | if(!pipe.hasOwnProperty("reduceInitial")) 50 | throw `MapReduce Error: pipe does not have "reduceInitial" value`; 51 | 52 | let innerContext = Object.assign({}, context); 53 | let innerNode = this.children[0]; 54 | const childResult = await innerNode.eval(innerContext, ops); 55 | 56 | if (!Array.isArray(childResult)) 57 | throw `MapReduce Error: internal results is not an array, and thus cannot be MapReduced. Type is: ${typeof childResult}.`; 58 | 59 | const result = mapReduce(childResult, pipe["map"], pipe["reduce"], pipe["reduceInitial"]); 60 | this.children = []; // Removing children so it doesn't recurse down the tree again. 61 | return result; 62 | } 63 | } 64 | 65 | module.exports = MapReduceNode; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/RapidAPIDriver/RapidAPINode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 2/14/17. 3 | */ 4 | /** 5 | * Created by iddo on 12/19/16. 6 | */ 7 | "use strict"; 8 | const RapidAPI = require('rapidapi-connect'), 9 | LeafNode = require('./../../LeafNode'), 10 | ObjectNode = require('./../../ObjectNode'), 11 | ArrayNode = require('./../../ArrayNode'), 12 | CompositeNode = require('./../../CompositeNode'); 13 | 14 | class RapidAPINode { 15 | constructor(name, children, args) { 16 | this.name = name; 17 | this.args = args; 18 | for (let k in args) { 19 | if (this.args.hasOwnProperty(k)) { 20 | if (typeof this.args[k] == 'object') { 21 | this.args[k] = JSON.stringify(this.args[k]) 22 | } 23 | } 24 | } 25 | this.children = children; 26 | } 27 | 28 | getName() { 29 | return this.name; 30 | } 31 | 32 | //noinspection JSAnnotator 33 | eval(context, ops) { 34 | //Init RapidAPI 35 | if (!ops.hasOwnProperty('RapidAPI')) { 36 | return new Promise((resolve, reject) => {reject(`OptionError: ops don't have RapidAPI keys`)}); 37 | } else { 38 | 39 | const rapid = new RapidAPI(ops['RapidAPI']['projectName'], ops['RapidAPI']['apiKey']); 40 | return new Promise((resolve, reject) => { 41 | 42 | rapid.call(...this.getName().split('.').slice(1), this.args) 43 | .on('success', (payload) => { 44 | //If JSON and not parsed -> parse 45 | try { 46 | payload = JSON.parse(payload); 47 | } catch (err) { 48 | err = err; 49 | } //Otherwise - no biggie, "you don't always get what you want" - M. Jagger 50 | for (let k in payload) { 51 | if(payload.hasOwnProperty(k)) 52 | if(typeof payload[k] == 'number') 53 | payload[k] = payload[k]+""; 54 | } 55 | resolve(payload); 56 | }) 57 | .on('error', (err) => { 58 | reject(`APIError: got error from ${this.getName()} API: ${typeof err === 'object' ? JSON.stringify(err) : err}`); 59 | }); 60 | }); 61 | } 62 | 63 | } 64 | } 65 | 66 | module.exports = RapidAPINode; 67 | 68 | //PLAYGROUND: 69 | 70 | /*let fn = new RapidAPINode('GoogleTranslate.translate', [], { 71 | 'apiKey': 'AIzaSyCDogEcpeA84USVXMS471PDt3zsG-caYDM', 72 | 'string': 'hello world, what a great day', 73 | 'targetLanguage': 'de' 74 | }); 75 | 76 | let ops = { 77 | RapidAPI : { 78 | projectName : 'Iddo_demo_app_1', 79 | apiKey: '4c6e5cc0-8426-4c95-9e3b-60adba0e50f6' 80 | } 81 | }; 82 | 83 | fn.eval({}, ops) 84 | .then((res) => {console.log(JSON.stringify(res))}) 85 | .catch((err) => {console.warn(JSON.stringify(err))});*/ -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/test/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 6/28/17. 3 | */ 4 | "use strict"; 5 | 6 | const assert = require('assert'), 7 | utils = require('./../utils'); 8 | 9 | module.exports = () => { 10 | describe('removeSpecialArgs', ()=> { 11 | it('should remove a specified argument', () => { 12 | assert.deepEqual( 13 | utils.removeSpecialArgs({'a':'111','b':'222'}, ['a']), 14 | {'b':'222'} 15 | ); 16 | }); 17 | 18 | it('should handle empty key array', () => { 19 | assert.deepEqual( 20 | utils.removeSpecialArgs({'a':'111','b':'222'}, []), 21 | {'a':'111','b':'222'} 22 | ); 23 | }); 24 | 25 | it('should handle an empty object', () => { 26 | assert.deepEqual( 27 | utils.removeSpecialArgs({}, ['a']), 28 | {} 29 | ); 30 | }); 31 | 32 | it('should handle non-existing keys', () => { 33 | assert.deepEqual( 34 | utils.removeSpecialArgs({'a':'111','b':'222'}, ['c','d']), 35 | {'a':'111','b':'222'} 36 | ); 37 | }); 38 | }); 39 | 40 | describe('isNumberParseable', () => { 41 | it('should identify number parseable', () => { 42 | assert.equal(utils.isNumberParseable("1"), true); 43 | assert.equal(utils.isNumberParseable("1.5"), true); 44 | assert.equal(utils.isNumberParseable("-1.5"), true); 45 | assert.equal(utils.isNumberParseable(-1.5), true); 46 | }); 47 | 48 | it('should identify non number parseable', () => { 49 | assert.equal(utils.isNumberParseable("1 apple"), false); 50 | assert.equal(utils.isNumberParseable("iddo"), false); 51 | assert.equal(utils.isNumberParseable("2017-1"), false); 52 | }); 53 | }); 54 | 55 | describe('quoteAsNeeded', () => { 56 | it('should quote strings', () => { 57 | assert.equal(utils.quoteAsNeeded('a'), "'a'"); 58 | }); 59 | 60 | it('should escape a single quote in strings', () => { 61 | assert.equal(utils.quoteAsNeeded("a'"), "'a'''"); 62 | }); 63 | 64 | it('should escape multiple quotes in strings', () => { 65 | assert.equal(utils.quoteAsNeeded("a' b'"), "'a'' b'''"); 66 | }); 67 | 68 | it('shouldn\'t quote numbers', () => { 69 | assert.equal(utils.quoteAsNeeded(1), "1"); 70 | }); 71 | 72 | it('shouldn\'t quote strings parsed as ints', () => { 73 | assert.equal(utils.quoteAsNeeded("1"), "1"); 74 | }); 75 | 76 | it('shouldn\'t quote strings parsed as floats', () => { 77 | assert.equal(utils.quoteAsNeeded("1.5"), "1.5"); 78 | }); 79 | }); 80 | 81 | describe('objValues', () => { 82 | it('should get an objects values', () => { 83 | assert.deepEqual(utils.objValues({'s':'ss', 'a':1}), ['ss',1]) 84 | }); 85 | }); 86 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MapReduce/mapReduce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 8/24/17. 3 | */ 4 | 5 | /** 6 | * Maps the array using the provided map function 7 | * @param arr the array of objects to be mapped 8 | * @param map map function. Accepts a single array item, and can return either a single item, or an array of items. Each item should have the structure {key: "", value: ...}. 9 | * @returns {*} 10 | */ 11 | function mapStep(arr, map) { 12 | return arr.map(map).reduce((prev, current) => { 13 | return prev.concat(current); 14 | },[]); 15 | } 16 | 17 | /** 18 | * Scatter an array into an object. From [{key:"a", value: "1"}, {key:"a", value: "2"}, {key:"b", value: "1"}] to {a: ["1", "2"], b: ["1"]} 19 | * @param mapped Array of form [{key:"a", value: "1"}, ...] 20 | * @returns {*} object of form {a: ["1", "2"], b: ["1"]} 21 | */ 22 | function scatterStep(mapped) { 23 | return mapped.reduce((prev, current) => { 24 | if (prev.hasOwnProperty(current["key"])) { 25 | prev[current["key"]].push(current["value"]); 26 | return prev; 27 | } else { 28 | prev[current["key"]] = [current["value"]]; 29 | return prev; 30 | } 31 | }, {}); 32 | } 33 | 34 | /** 35 | * Applies they user provided reduce function 36 | * @param grouped object of form {a: ["1", "2"], b: ["1"]} 37 | * @param reduce reduce function f(previous, current) 38 | * @param reduceInitial initial value to be used as previous when applying reduce. 39 | * @returns {*} object of form {a: ReducedValue, b: ReducedValue} 40 | */ 41 | function reduceStep(grouped, reduce, reduceInitial = null) { 42 | return Object.keys(grouped).reduce((previous, current) => { 43 | previous[current] = grouped[current].reduce(reduce, reduceInitial); 44 | return previous; 45 | }, {}); 46 | } 47 | 48 | function mapReduce(arr, map, reduce, reduceInitial) { 49 | // Input validation step 50 | if (!Array.isArray(arr)) 51 | throw "First argument must be an array"; 52 | else if (typeof map !== "function") 53 | throw "Second argument must be a function (map function)"; 54 | else if (typeof reduce !== "function") 55 | throw "Third argument must be a function (reduce function)"; 56 | 57 | // Map step 58 | const mapped = mapStep(arr, map); 59 | /* users's map returns an array of {key: "", value: ...} objects, the reduce-concat step flattens it */ 60 | 61 | // Scatter step 62 | const grouped = scatterStep(mapped); 63 | 64 | // Reduce Step 65 | // This is why I'm using reduce and not map: https://stackoverflow.com/questions/14810506/map-function-for-objects-instead-of-arrays 66 | const reduced = reduceStep(grouped, reduce, reduceInitial); 67 | 68 | return reduced; 69 | } 70 | 71 | module.exports.mapReduce = mapReduce; 72 | module.exports.mapStep = mapStep; 73 | module.exports.scatterStep = scatterStep; 74 | module.exports.reduceStep = reduceStep; 75 | 76 | // TEST 77 | /*let strings = ["I want to eat", "I am hungry", "I am sleepy"]; 78 | 79 | let results = mapReduce(strings, (str) => { 80 | return str.split(" ").map((word) => { 81 | return {key:word, value: 1} 82 | }); 83 | }, (prev, current) => { 84 | return prev + current; 85 | }, 0); 86 | 87 | console.log(results);*/ -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/PostgreSQLNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 2/14/17. 3 | */ 4 | 5 | const SEP_CHAR = '.'; 6 | 7 | const pg = require('pg'), 8 | LeafNode = require('./../../LeafNode'), 9 | CompositeNode = require('./../../CompositeNode'); 10 | 11 | const functions = { 12 | find: require('./functions/find'), 13 | count: require('./functions/count'), 14 | insert: require('./functions/insert'), 15 | softInsert: require('./functions/softInsert'), 16 | update: require('./functions/update'), 17 | avg: require('./functions/aggregate')('avg'), 18 | sum: require('./functions/aggregate')('sum'), 19 | max: require('./functions/aggregate')('max'), 20 | min: require('./functions/aggregate')('min') 21 | }; 22 | 23 | global._postgresql_clients = {}; 24 | function getClient(DBName, DBConfigs) { 25 | // This is basically a singleton. If we already have a client, we should use it. 26 | // Otherwise, we create a new one. 27 | if (global._postgresql_clients.hasOwnProperty(DBName)) { 28 | return global._postgresql_clients[DBName]; 29 | } else { 30 | const pool = new pg.Pool(DBConfigs); 31 | global._postgresql_clients[DBName] = pool; 32 | return getClient(DBName, DBConfigs); 33 | } 34 | } 35 | 36 | 37 | class PostgreSQLNode { 38 | constructor(name, children, args) { 39 | this.name = name; 40 | this.args = args; 41 | this.children = children; 42 | } 43 | 44 | getName() { 45 | return this.name; 46 | } 47 | 48 | //noinspection JSAnnotator 49 | eval(context, ops) { 50 | const self = this; 51 | 52 | return new Promise((resolve, reject) => { 53 | //De-tokenize function call: DBName.DBSchema.DBTable.operation 54 | const tokenizedName = this.name.split(SEP_CHAR); 55 | const DBName = tokenizedName[1], 56 | DBSchema = tokenizedName[2], 57 | DBTable = tokenizedName[3], 58 | operation = tokenizedName[4]; 59 | 60 | //Check operation exists 61 | if(!functions.hasOwnProperty(operation)) 62 | return reject(`Operation Error: operation ${operation} does not exist / is not supported`); 63 | 64 | //Create DB connection 65 | //Check configs exist 66 | if (!ops.hasOwnProperty('PostgreSQL')) { 67 | return reject(`Missing configs: PostgreSQL settings are missing`); 68 | } else if (!ops['PostgreSQL'].hasOwnProperty(DBName)) { 69 | return reject(`Missing configs: PostgreSQL settings for DB ${DBName} are missing`); 70 | } else { 71 | const DBConfigs = ops.PostgreSQL[DBName]; 72 | 73 | const pool = getClient(DBName, DBConfigs); 74 | 75 | pool.connect(function(err, client, done) { 76 | if (err) { 77 | return reject(`DB Error: Error connecting to database ${DBName} -> ${err}`); 78 | } 79 | 80 | //Route different functions 81 | functions[operation](DBSchema, DBTable, client, self.args) 82 | .then((payload) => { 83 | done(); 84 | resolve(payload); 85 | }) 86 | .catch(reject); 87 | }); 88 | } 89 | }); 90 | } 91 | } 92 | 93 | module.exports = PostgreSQLNode; -------------------------------------------------------------------------------- /src/Parser/grammer-raw.pegjs: -------------------------------------------------------------------------------- 1 | start = Complex 2 | 3 | Complex = "{" firstNode:Node? nodes:("," Node)* "}" { 4 | var ns = []; 5 | nodes.forEach(function(n) { 6 | ns.push(n[1]); 7 | }); 8 | if (firstNode) 9 | return [firstNode, ...ns]; 10 | else 11 | return []; 12 | } 13 | 14 | Node 15 | = RenameNode 16 | / OptionalNode 17 | / LogicNode 18 | / CachedFunctionNode 19 | / FunctionNode 20 | / CompositeNode 21 | / CastedLeafNode 22 | / LeafNode 23 | 24 | RenameNode = name:Word ":" n:Node { 25 | return {type: 'rename', value:n, name:name} 26 | } 27 | 28 | OptionalNode = "?" n:Node { 29 | return {type: 'optional', value:n} 30 | } 31 | 32 | CastedLeafNode = cast:Word "(" innerNode:LeafNode ")" { 33 | return {cast:cast, innerNode:innerNode} 34 | } 35 | 36 | LeafNode = Word 37 | 38 | CompositeNode = label:Word values:Complex { 39 | return {'label' : label, 'value': values}; 40 | } 41 | 42 | LogicNode = "@" n:FunctionNode f:LogicNode?{ 43 | return {'t':'logic', 'l':n.label, 'a':n.args,'f':f}; 44 | } 45 | 46 | CachedFunctionNode = "*" n:FunctionNode { 47 | return {type: 'cached', value:n} 48 | } 49 | 50 | FunctionNode = label:Word args:ArgSet values:Complex { 51 | return {'label': label, 'args': args, 'value': values}; 52 | } 53 | 54 | ArgSet = "(" tuple:KVTuple? tuples:("," KVTuple)* ")" { 55 | let rs = {}; 56 | Object.assign(rs, tuple); 57 | tuples.forEach(function(t) { 58 | Object.assign(rs, t[1]); //each object in tuples is an array containg comma and them KVTuple 59 | }); 60 | return rs; 61 | } 62 | 63 | //Key Value Tuple 64 | KVTuple = key:Word ":" value:KVValue { 65 | var rs = {}; 66 | rs[key] = value; 67 | return rs; 68 | } 69 | 70 | //Added new intermidiate type to support (key:{subkey:value}) 71 | KVValue = Number / ValueWord / Word / KVCompValue / KVArray 72 | 73 | //Support array parameters 74 | KVArray = "[" el0:KVValue? els:("," value:KVValue)* "]" { 75 | let res = []; 76 | if (el0) { 77 | res[0] = el0; 78 | els.forEach(function(e) { 79 | res.push(e[1]); 80 | }); 81 | } 82 | return res; 83 | } 84 | 85 | //This support having json types in args 86 | KVCompValue = "{}" {return {};} //empty 87 | / "{" key0:Word ":" value0:KVValue values:("," key:Word ":" value:KVValue)* "}" { 88 | let rs = {}; 89 | rs[key0] = value0; 90 | values.forEach(function(t) { 91 | rs[t[1]] = t[3]; 92 | }); 93 | return rs; 94 | } 95 | 96 | Word = chars:[-$!<=>@_0-9a-zA-Z.]+ { 97 | return chars.join(""); 98 | } / str:StringLiteralValue { 99 | return str.slice(1,-1); 100 | } 101 | 102 | ValueWord = StringLiteralValue 103 | 104 | Number = sign:"-"? chars:[-.0-9]+ { 105 | return parseFloat([sign].concat(chars).join("")); 106 | } 107 | 108 | StringLiteralValue 109 | = '"' chars:DoubleStringCharacter* '"' { return '"' +chars.join('') + '"'; } 110 | / "'" chars:SingleStringCharacter* "'" { return '"' +chars.join('') + '"'; } 111 | 112 | DoubleStringCharacter 113 | = !('"' / "\\") char:. { return char; } 114 | / "\\" sequence:EscapeSequence { return sequence; } 115 | 116 | SingleStringCharacter 117 | = !("'" / "\\") char:. { return char; } 118 | / "\\" sequence:EscapeSequence { return sequence; } 119 | 120 | EscapeSequence 121 | = "'" 122 | / '"' 123 | / "\\" 124 | / "b" { return "\b"; } 125 | / "f" { return "\f"; } 126 | / "n" { return "\n"; } 127 | / "r" { return "\r"; } 128 | / "t" { return "\t"; } 129 | / "v" { return "\x0B"; } 130 | -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MySQLDriver/MySQLNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 3/10/17. 3 | */ 4 | const SEP_CHAR = '.'; 5 | 6 | const mysql = require('mysql'); 7 | 8 | const functions = { 9 | find: require('./functions/find'), 10 | count: require('./functions/count'), 11 | sum: require('./functions/sum'), 12 | avg: require('./functions/avg') 13 | }; 14 | 15 | global._mysql_clients = {}; 16 | function getClient(DBName, DBConfigs) { 17 | if (global._mysql_clients.hasOwnProperty(DBName)) { 18 | return global._mysql_clients[DBName]; 19 | } else { 20 | const pool = mysql.createPool(DBConfigs); 21 | global._mysql_clients[DBName] = pool; 22 | return getClient(DBName, DBConfigs); 23 | } 24 | } 25 | 26 | class MySQLNode { 27 | constructor(name, children, args) { 28 | this.name = name; 29 | this.args = args; 30 | this.children = children; 31 | } 32 | 33 | getName() { 34 | return this.name; 35 | } 36 | 37 | //noinspection JSAnnotator 38 | eval(context, ops) { 39 | const self = this; 40 | 41 | return new Promise((resolve, reject) => { 42 | //De-tokenize function call: DBName.DBTable.operation 43 | const tokenizedName = this.name.split(SEP_CHAR); 44 | const DBName = tokenizedName[1], 45 | DBTable = tokenizedName[2], 46 | operation = tokenizedName[3]; 47 | 48 | //Check operation exists 49 | if(!functions.hasOwnProperty(operation)) 50 | return reject(`Operation Error: operation ${operation} does not exist / is not supported`); 51 | 52 | //Create DB connection 53 | //Check configs exist 54 | if (!ops.hasOwnProperty('MySQL')) { 55 | return reject(`Missing configs: MySQL settings are missing`); 56 | } else if (!ops['MySQL'].hasOwnProperty(DBName)) { 57 | return reject(`Missing configs: MySQL settings for DB ${DBName} are missing`); 58 | } else { 59 | const dbConfigs = ops.MySQL[DBName]; 60 | 61 | const pool = getClient(DBName, dbConfigs); 62 | 63 | pool.getConnection((err, dbConnection) => { 64 | if(err) { 65 | return reject(`DB Error: Error connecting to database ${DBName} -> ${err}`); 66 | } 67 | 68 | //Route different functions 69 | functions[operation](DBTable, dbConnection, self.args) 70 | .then((payload) => { 71 | dbConnection.release(); 72 | resolve(payload); 73 | }) 74 | .catch((err) => { 75 | dbConnection.release(); 76 | reject(err); 77 | }); 78 | }); 79 | 80 | // const dbConnection = mysql.createConnection(dbConfigs); 81 | // dbConnection.connect((err) => { 82 | // if(err) { 83 | // return reject(`DB Error: Error connecting to database ${DBName} -> ${err}`); 84 | // } 85 | // 86 | // //Route different functions 87 | // functions[operation](DBTable, dbConnection, self.args) 88 | // .then((payload) => { 89 | // resolve(payload); 90 | // }) 91 | // .catch(reject); 92 | // }); 93 | } 94 | }); 95 | } 96 | } 97 | 98 | module.exports = MySQLNode; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MySQLDriver/whereGenerator.js: -------------------------------------------------------------------------------- 1 | const specialKeys = ['WHERE', 'LIMIT', 'ORDERBY', 'SKIP', "GROUPBY", "SET", "FIELD"]; 2 | 3 | //Import utility functions 4 | const removeSpecialArgs = require('./utils').removeSpecialArgs; 5 | const quoteAsNeeded = require('./utils').quoteAsNeeded; 6 | 7 | 8 | /** 9 | * Generates the "WHERE x=y..." part of the where query 10 | * @param queryArgs 11 | * @returns {string} 12 | */ 13 | function whereClauseGenerator(queryArgs) { 14 | let queryString = ""; 15 | if (Object.keys(queryArgs).length > 0) { 16 | queryString += ` WHERE`; 17 | //Has one is set to true after first condition is added, to append the AND keyword 18 | let hasOne = false; 19 | for (let field in queryArgs) { 20 | if (queryArgs.hasOwnProperty(field)) { 21 | if (hasOne) 22 | queryString += ` AND`; 23 | 24 | //Simple equality operator 25 | if (typeof queryArgs[field] == 'string' || typeof queryArgs[field] == 'number') { 26 | queryString += ` ${field} = ${quoteAsNeeded(queryArgs[field])}`; 27 | 28 | //Complex inequalities 29 | } else if (typeof queryArgs[field] == 'object') { 30 | //If comparators have quotes - remove 31 | const compOp = Object.keys(queryArgs[field])[0]; 32 | const compVal = queryArgs[field][Object.keys(queryArgs[field])[0]]; 33 | queryString += ` ${field} ${compOp} ${quoteAsNeeded(compVal)}`; 34 | } 35 | hasOne = true; 36 | } 37 | } 38 | } 39 | 40 | return queryString; 41 | } 42 | 43 | 44 | /** 45 | * This function takes query arguments and turns them into a PostgreSQL WHERE clause 46 | * @param args the input arguments 47 | */ 48 | function whereGenerator(args) { 49 | let queryString = ""; 50 | 51 | //Check if full query / shorthand 52 | let queryArgs ; 53 | if (args.hasOwnProperty('WHERE') && typeof args['WHERE'] == 'object') 54 | queryArgs = Object.assign({},args['WHERE']); 55 | else 56 | queryArgs = Object.assign({},args); 57 | 58 | //Remove special instructions (LIMIT, SKIP, ETC...) 59 | queryArgs = removeSpecialArgs(queryArgs, specialKeys); 60 | 61 | //WHERE clause 62 | queryString += whereClauseGenerator(queryArgs); 63 | 64 | //GROUP BY clause 65 | if (typeof args['GROUPBY'] == 'string') { //Shorthand 66 | queryString += ' GROUP BY ' + args['GROUPBY']; 67 | } 68 | 69 | //ORDER BY clause 70 | if (typeof args['ORDERBY'] == 'string') { //Shorthand, ASC by default 71 | queryString += ' ORDER BY ' + args['ORDERBY']; 72 | } else if (typeof args['ORDERBY'] == 'object') { //Full syntax 73 | let first = true; //after the first need to add a , 74 | queryString += ' ORDER BY'; 75 | for (let field in args['ORDERBY']) { 76 | if(args['ORDERBY'].hasOwnProperty(field)) { 77 | if (typeof args['ORDERBY'][field] == 'string') { 78 | if (args['ORDERBY'][field] == 'ASC' || args['ORDERBY'][field] == 'DESC') { //Never do it without protection.... 79 | if (!first) 80 | queryString += ','; 81 | queryString += ` ${field} ${args['ORDERBY'][field]}`; 82 | first = false; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | //LIMIT clause 90 | if (typeof args['LIMIT'] == 'string') { 91 | queryString += ' LIMIT ' + args['LIMIT']; 92 | } 93 | 94 | //SKIP clause 95 | if (typeof args['SKIP'] == 'string') { 96 | queryString += ' SKIP ' + args['SKIP']; 97 | } 98 | 99 | return queryString; 100 | } 101 | 102 | module.exports.whereGenerator = whereGenerator; 103 | module.exports.whereClauseGenerator = whereClauseGenerator; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/whereGenerator.js: -------------------------------------------------------------------------------- 1 | const specialKeys = ['WHERE', 'LIMIT', 'ORDERBY', 'SKIP', "GROUPBY", "SET", "FIELD"]; 2 | 3 | //Import utility functions 4 | const removeSpecialArgs = require('./utils').removeSpecialArgs; 5 | const quoteAsNeeded = require('./utils').quoteAsNeeded; 6 | 7 | 8 | /** 9 | * Generates the "WHERE x=y..." part of the where query 10 | * @param queryArgs 11 | * @returns {string} 12 | */ 13 | function whereClauseGenerator(queryArgs) { 14 | let queryString = ""; 15 | if (Object.keys(queryArgs).length > 0) { 16 | queryString += ` WHERE`; 17 | //Has one is set to true after first condition is added, to append the AND keyword 18 | let hasOne = false; 19 | for (let field in queryArgs) { 20 | if (queryArgs.hasOwnProperty(field)) { 21 | if (hasOne) 22 | queryString += ` AND`; 23 | 24 | //Simple equality operator 25 | if (typeof queryArgs[field] == 'string' || typeof queryArgs[field] == 'number') { 26 | queryString += ` ${field} = ${quoteAsNeeded(queryArgs[field])}`; 27 | 28 | //Complex inequalities 29 | } else if (typeof queryArgs[field] == 'object') { 30 | //If comparators have quotes - remove 31 | const compOp = Object.keys(queryArgs[field])[0]; 32 | const compVal = queryArgs[field][Object.keys(queryArgs[field])[0]]; 33 | queryString += ` ${field} ${compOp} ${quoteAsNeeded(compVal)}`; 34 | } 35 | hasOne = true; 36 | } 37 | } 38 | } 39 | 40 | return queryString; 41 | } 42 | 43 | 44 | /** 45 | * This function takes query arguments and turns them into a PostgreSQL WHERE clause 46 | * @param args the input arguments 47 | */ 48 | function whereGenerator(args) { 49 | let queryString = ""; 50 | 51 | //Check if full query / shorthand 52 | let queryArgs ; 53 | if (args.hasOwnProperty('WHERE') && typeof args['WHERE'] == 'object') 54 | queryArgs = Object.assign({},args['WHERE']); 55 | else 56 | queryArgs = Object.assign({},args); 57 | 58 | //Remove special instructions (LIMIT, SKIP, ETC...) 59 | queryArgs = removeSpecialArgs(queryArgs, specialKeys); 60 | 61 | //WHERE clause 62 | queryString += whereClauseGenerator(queryArgs); 63 | 64 | //GROUP BY clause 65 | if (typeof args['GROUPBY'] == 'string') { //Shorthand 66 | queryString += ' GROUP BY ' + args['GROUPBY']; 67 | } 68 | 69 | //ORDER BY clause 70 | if (typeof args['ORDERBY'] == 'string') { //Shorthand, ASC by default 71 | queryString += ' ORDER BY ' + args['ORDERBY']; 72 | } else if (typeof args['ORDERBY'] == 'object') { //Full syntax 73 | let first = true; //after the first need to add a , 74 | queryString += ' ORDER BY'; 75 | for (let field in args['ORDERBY']) { 76 | if(args['ORDERBY'].hasOwnProperty(field)) { 77 | if (typeof args['ORDERBY'][field] == 'string') { 78 | if (args['ORDERBY'][field] == 'ASC' || args['ORDERBY'][field] == 'DESC') { //Never do it without protection.... 79 | if (!first) 80 | queryString += ','; 81 | queryString += ` ${field} ${args['ORDERBY'][field]}`; 82 | first = false; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | //LIMIT clause 90 | if (typeof args['LIMIT'] == 'string') { 91 | queryString += ' LIMIT ' + args['LIMIT']; 92 | } 93 | 94 | //SKIP clause 95 | if (typeof args['SKIP'] == 'string') { 96 | queryString += ' SKIP ' + args['SKIP']; 97 | } 98 | 99 | return queryString; 100 | } 101 | 102 | module.exports.whereGenerator = whereGenerator; 103 | module.exports.whereClauseGenerator = whereClauseGenerator; -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 8/31/17. 3 | */ 4 | "use strict"; 5 | 6 | const assert = require('assert'), 7 | utils = require('../src/Nodes/utils'); 8 | 9 | describe('Utils', () => { 10 | describe('resolve', () => { 11 | it ('should resolve top level object', () => { 12 | const testObj = {a:12}; 13 | assert.equal(utils.resolve('a', testObj), testObj.a); 14 | }); 15 | 16 | it ('should resolve low level object', () => { 17 | const testObj = {a:{b: 12}}; 18 | assert.equal(utils.resolve('a.b', testObj), testObj.a.b); 19 | }); 20 | 21 | it ('should throw error on non-existent top level object', (done) => { 22 | try { 23 | const testObj = {a:12}; 24 | let v = utils.resolve('c', testObj); 25 | done(`Didn't fail. Returned value ${v}`); 26 | } catch (e) { 27 | assert.equal(e, `Name c does not exist in context { 28 | "a": 12 29 | }`); 30 | done(); 31 | } 32 | }); 33 | 34 | it ('should throw error accessing property of non-existent top level object', (done) => { 35 | try { 36 | const testObj = {a:12}; 37 | let v = utils.resolve('c.d', testObj); 38 | done(`Didn't fail. Returned value ${v}`); 39 | } catch (e) { 40 | assert.equal(e, `Name c.d does not exist in context { 41 | "a": 12 42 | }`); 43 | done(); 44 | } 45 | }); 46 | }); 47 | 48 | describe('createMixedContext', () => { 49 | it('should return inner context value if only in inner context', () => { 50 | let context = utils.createMixedContext({}, {a:1}); 51 | assert.equal(context.a, 1); 52 | }); 53 | 54 | it('should return inner context value if only in outer context', () => { 55 | let context = utils.createMixedContext({a:1}, {}); 56 | assert.equal(context.a, 1); 57 | }); 58 | 59 | it('should give precedence to inner context over outer context, even if property exists in both', () => { 60 | let context = utils.createMixedContext({a: 2}, {a:1}); 61 | assert.equal(context.a, 1); 62 | }); 63 | 64 | it('should default to outer context if property not in inner context', () => { 65 | let context = utils.createMixedContext({a:1}, {b:3}); 66 | assert.equal(context.a, 1); 67 | }); 68 | 69 | it ('should return undefined if property doesnt exist in both contexts', () => { 70 | let context = utils.createMixedContext({a:1}, {b:3}); 71 | assert.equal(context.c, undefined); 72 | }); 73 | 74 | describe('in operator', () => { 75 | it('should return true if in inner context', () => { 76 | let context = utils.createMixedContext({}, {a:1}); 77 | assert.equal(true, 'a' in context); 78 | }); 79 | 80 | it('should return true if in outer context', () => { 81 | let context = utils.createMixedContext({a:1}, {}); 82 | assert.equal(true, 'a' in context); 83 | }); 84 | 85 | it('should return false if not in any context', () => { 86 | let context = utils.createMixedContext({a:1}, {b:2}); 87 | assert.equal(false, 'c' in context); 88 | }); 89 | }); 90 | 91 | describe('hasOwnProperty operator', () => { 92 | it('should return true if in inner context', () => { 93 | let context = utils.createMixedContext({}, {a:1}); 94 | assert.equal(true, context.hasOwnProperty('a')); 95 | }); 96 | 97 | it('should return true if in outer context', () => { 98 | let context = utils.createMixedContext({a:1}, {}); 99 | assert.equal(true, context.hasOwnProperty('a')); 100 | }); 101 | 102 | it('should return false if not in any context', () => { 103 | let context = utils.createMixedContext({a:1}, {b:2}); 104 | assert.equal(false, context.hasOwnProperty('c')); 105 | }); 106 | }); 107 | 108 | }); 109 | }); -------------------------------------------------------------------------------- /test/functionNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 3/1/17. 3 | */ 4 | const assert = require('assert'), 5 | LeafNode = require('../src/Nodes/LeafNode'), 6 | CompositeNode = require('../src/Nodes/CompositeNode'), 7 | FunctionNode = require('../src/Nodes/FunctionNode'); 8 | 9 | describe('Function Nodes', () => { 10 | 11 | describe('quoted', () => { 12 | it('should identify a string quoted with double quotes', ()=> { 13 | assert.deepEqual(FunctionNode.quoted('"asd"'), true); 14 | }); 15 | 16 | it('should identify a string quoted with single quotes', ()=> { 17 | assert.deepEqual(FunctionNode.quoted("'aa'"), true); 18 | }); 19 | 20 | it('should not identify a not quoted string', () => { 21 | assert.deepEqual(FunctionNode.quoted("asdfs"), false); 22 | }); 23 | 24 | it('should not identify a partially quoted string', () => { 25 | assert.deepEqual(FunctionNode.quoted("'sdfsd"), false); 26 | assert.deepEqual(FunctionNode.quoted("sdfsd'"), false); 27 | assert.deepEqual(FunctionNode.quoted('"sdfsd'), false); 28 | assert.deepEqual(FunctionNode.quoted('"sdfsd'), false); 29 | assert.deepEqual(FunctionNode.quoted('sdfsdd"'), false); 30 | }); 31 | }); 32 | 33 | describe('removeQuotes', () => { 34 | it('should remove quotes', () => { 35 | assert.deepEqual(FunctionNode.removeQuotes('"asdasda"'), 'asdasda'); 36 | assert.deepEqual(FunctionNode.removeQuotes("'asdasda'"), 'asdasda'); 37 | }); 38 | 39 | it('shouldnt change string with no quotes', () => { 40 | assert.deepEqual(FunctionNode.removeQuotes("asdasda"), 'asdasda'); 41 | }); 42 | 43 | it('should ignore single quote', () => { 44 | assert.deepEqual(FunctionNode.removeQuotes("'asdasda"), '\'asdasda'); 45 | assert.deepEqual(FunctionNode.removeQuotes("\"asdasda"), '\"asdasda'); 46 | assert.deepEqual(FunctionNode.removeQuotes("asdasda'"), 'asdasda\''); 47 | assert.deepEqual(FunctionNode.removeQuotes("asdasda\""), 'asdasda\"'); 48 | assert.deepEqual(FunctionNode.removeQuotes("asdasda\"a"), 'asdasda\"a'); 49 | assert.deepEqual(FunctionNode.removeQuotes("asdasda\'a"), 'asdasda\'a'); 50 | }); 51 | }); 52 | 53 | describe('Recursive replace', () => { 54 | it('should replace a single constant string', () => { 55 | assert.deepEqual(FunctionNode.recursiveReplace({a: '"b"'}, {}), {a:'b'}); 56 | }); 57 | 58 | it('should replace multiple constants', ()=> { 59 | assert.deepEqual(FunctionNode.recursiveReplace({a: '"b"', c: '"bbb"', num:-5}, {}), {a:'b', c:'bbb', num:-5}); 60 | }); 61 | 62 | it('should replace a single constant number', () => { 63 | assert.deepEqual(FunctionNode.recursiveReplace({a: 123}, {}), {a:123}); 64 | }); 65 | 66 | it('should replace variables with context value', () => { 67 | assert.deepEqual(FunctionNode.recursiveReplace({a:'a'}, {a:2}), {a:2}); 68 | }); 69 | 70 | it('should properly handle mix of variables and constants', () => { 71 | assert.deepEqual(FunctionNode.recursiveReplace({a:'a', b:'"3"'}, {a:2}), {a:2, b:3}); 72 | }); 73 | 74 | it('should traverse down objects, replacing from the context tree', () => { 75 | assert.deepEqual(FunctionNode.recursiveReplace({b:{a:'a'}}, {a:2}), {b:{a:2}}); 76 | }); 77 | 78 | it('should replace template (mustache templates)', () => { 79 | assert.deepEqual(FunctionNode.recursiveReplace({a:'"a={{a}}"'}, {a:2}), {a:"a=2"}); 80 | assert.deepEqual(FunctionNode.recursiveReplace({a:'"{{a}}"'}, {a:2}), {a:"2"}); 81 | }); 82 | 83 | it('should handle deep referencing (dot notation) in objects', () => { 84 | assert.deepEqual(FunctionNode.recursiveReplace({a:'a.b'}, {a:{b:2}}), {a:2}); 85 | }); 86 | 87 | describe('arrays', () => { 88 | it ('should support array of constants', () => { 89 | assert.deepEqual(FunctionNode.recursiveReplace({arr:['"a"','"b"', 5]}, {}), {arr:['a','b', 5]}); 90 | }); 91 | 92 | it ('should support array of keys', () => { 93 | assert.deepEqual(FunctionNode.recursiveReplace({arr:['a','b']}, {a: 1, b: "bbb"}), {arr:[1,'bbb']}); 94 | }); 95 | }); 96 | }); 97 | }); -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MapReduce/test/MapReduceNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 9/1/17. 3 | */ 4 | const chai = require("chai"); 5 | chai.use(require("chai-as-promised")); 6 | const { expect, assert } = chai; 7 | const MapReduceNode = require('./../MapReduceNode'); 8 | const LeafNode = require('./../../../LeafNode'), 9 | ArrayNode = require('./../../../ArrayNode'); 10 | 11 | module.exports = () => { 12 | describe("MapReduceNode", () => { 13 | describe("initial validation", () => { 14 | it("should throw error when initialized with 0 children", () => { 15 | assert.throws(() => { 16 | new MapReduceNode("sdf", [], {}) 17 | }, `MapReduce Error: node needs 1 child. 0 found.`); 18 | }); 19 | 20 | it("should throw error when initialized without pipe name", () => { 21 | assert.throws(() => { 22 | new MapReduceNode("pipe", [new LeafNode("a")], {}) 23 | }, `MapReduce Error: No pipe name. Should be MapReduce.pipeName(){}`); 24 | }); 25 | }); 26 | 27 | describe("runtime validations", () => { 28 | const invalidNode = new MapReduceNode('MapReduce.pipe', [new LeafNode("a")], {}); 29 | const invalidContext = {a:1}; 30 | 31 | it("should throw error if there are no MapReduce configurations", () => { 32 | return expect(invalidNode.eval(invalidContext, {})).to.be.rejectedWith(`MapReduce Error: Options object doesn't have MapReduce configurations / configurations are invalid.`); 33 | }); 34 | 35 | it("should throw error if MapReduce configurations doesn't have specific pipe", () => { 36 | return expect(invalidNode.eval(invalidContext, {MapReduce: {}})).to.be.rejectedWith(`MapReduce Error: Options object doesn't have MapReduce configurations / configurations are invalid.`); 37 | }); 38 | 39 | it("should throw error if MapReduce configurations doesn't have map function", () => { 40 | return expect(invalidNode.eval(invalidContext, {MapReduce: {pipe: { 41 | reduce: () => {}, 42 | reduceInitial: 0 43 | }}})).to.be.rejectedWith(`MapReduce Error: pipe does not have "map" function / parameter is not a valid function`); 44 | }); 45 | 46 | it("should throw error if MapReduce configuration's map isn't a function", () => { 47 | return expect(invalidNode.eval(invalidContext, {MapReduce: {pipe: { 48 | map: 1, 49 | reduce: () => {}, 50 | reduceInitial: 0 51 | }}})).to.be.rejectedWith(`MapReduce Error: pipe does not have "map" function / parameter is not a valid function`); 52 | }); 53 | 54 | it("should throw error if MapReduce configurations doesn't have reduce function", () => { 55 | return expect(invalidNode.eval(invalidContext, {MapReduce: {pipe: { 56 | map: () => {}, 57 | reduceInitial: 0 58 | }}})).to.be.rejectedWith(`MapReduce Error: pipe does not have "reduce" function / parameter is not a valid function`); 59 | }); 60 | 61 | it("should throw error if MapReduce configuration's reduce isn't a function", () => { 62 | return expect(invalidNode.eval(invalidContext, {MapReduce: {pipe: { 63 | map: () => {}, 64 | reduce: 1, 65 | reduceInitial: 0 66 | }}})).to.be.rejectedWith(`MapReduce Error: pipe does not have "reduce" function / parameter is not a valid function`); 67 | }); 68 | 69 | it("should throw error if MapReduce configurations doesn't have reduceInitial", () => { 70 | return expect(invalidNode.eval(invalidContext, {MapReduce: {pipe: { 71 | map: () => {}, 72 | reduce: () => {} 73 | }}})).to.be.rejectedWith(`MapReduce Error: pipe does not have "reduceInitial" value`); 74 | }); 75 | 76 | it("should throw an error if MapReduce internal result is not an array", () => { 77 | return expect(invalidNode.eval(invalidContext, {MapReduce: {pipe: { 78 | map: () => {}, 79 | reduce: () => {}, 80 | reduceInitial: 0 81 | }}})).to.be.rejectedWith(`MapReduce Error: internal results is not an array, and thus cannot be MapReduced. Type is: ${typeof 1}.`); 82 | }); 83 | }); 84 | }); 85 | }; -------------------------------------------------------------------------------- /test/generative/gen-parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/27/17. 3 | */ 4 | "use strict"; 5 | 6 | const { describe, it } = require('mocha'); 7 | const { check, gen } = require('mocha-testcheck'); 8 | const { expect, assert } = require('chai'); 9 | require('mocha-testcheck').install(); 10 | 11 | const LeafNode = require('../../src/Nodes/LeafNode'), 12 | CompositeNode = require('../../src/Nodes/CompositeNode'), 13 | RenameNode = require('../../src/Nodes/RenameNode'), 14 | OptionalNode = require('../../src/Nodes/OptionalNode'), 15 | CastedLeafNode = require('../../src/Nodes/CastedLeafNode'), 16 | FunctionNode = require('../../src/Nodes/FunctionNode'); 17 | 18 | const parse = require('../../src/Parser/Parser').parse; 19 | 20 | describe('Generative - Parser', () => { 21 | describe('Leaf Nodes', () => { 22 | check.it('should detect simple alpha-numeric leaf nodes that are un-quoted',{result: true, times: 50}, gen.array(gen.alphaNumString), async (leafs) => { 23 | // Filter out empty strings and remove spaces 24 | leafs = leafs.map(a => a.replace(" ", "")).filter(a => a.length > 0); 25 | 26 | const queryString = `{ 27 | ${leafs.join(",\n")} 28 | }`; 29 | const val = await parse(queryString); 30 | 31 | assert.equal(val.length, leafs.length); 32 | 33 | for (let i = 0; i < leafs.length; i++) { 34 | assert.equal(leafs[i], val[i].getName()); 35 | } 36 | }); 37 | 38 | check.it('should detect double-quoted freestyle ascii leaf nodes',{result: true, times: 50}, gen.array(gen.asciiString), async (leafs) => { 39 | leafs = leafs.map(a => a.replace(" ", "")); 40 | leafs = leafs.map(a => a.replace(/"/g, '')); 41 | leafs = leafs.map(a => a.replace(/\\/g, '')); 42 | // Filter out empty strings 43 | leafs = leafs.filter(a => a.length > 0); 44 | 45 | const queryString = `{ 46 | ${leafs.map(l=> '"'+l+'"').join(",\n")} 47 | }`; 48 | const val = await parse(queryString); 49 | 50 | assert.equal(val.length, leafs.length); 51 | 52 | for (let i = 0; i < leafs.length; i++) { 53 | assert.equal(leafs[i], val[i].getName()); 54 | } 55 | }); 56 | 57 | check.it('should detect single-quoted freestyle ascii leaf nodes',{result: true, times: 50}, gen.array(gen.asciiString), async (leafs) => { 58 | leafs = leafs.map(a => a.replace(/'/g, '')); 59 | leafs = leafs.map(a => a.replace(/\\/g, '')); 60 | leafs = leafs.map(a => a.replace(/ /g, '')); 61 | leafs = leafs.map(a => a.replace(/"/g, '')); 62 | // Filter out empty strings 63 | leafs = leafs.filter(a => a.length > 0); 64 | 65 | const queryString = `{ 66 | ${leafs.map(l=> '\''+l+'\'').join(",\n")} 67 | }`; 68 | const val = await parse(queryString); 69 | 70 | assert.equal(val.length, leafs.length); 71 | 72 | for (let i = 0; i < leafs.length; i++) { 73 | assert.equal(leafs[i], val[i].getName()); 74 | } 75 | }); 76 | }); 77 | 78 | describe('Function Nodes', () => { 79 | check.it('accepts string parameters', {result: true, times: 50}, gen.asciiString, gen.asciiString, gen.alphaNumString, gen.alphaNumString, async (param1, param2, key1, key2) => { 80 | 81 | param1 = param1.replace(/'/g, '').replace(/\\/g, '').replace(/"/g, ''); 82 | param2 = param2.replace(/'/g, '').replace(/\\/g, '').replace(/"/g, ''); 83 | 84 | //make sure keys aren't empty 85 | key1 += (key1.length === 0) ? "a" : ""; 86 | key2 += (key2.length === 0) ? "b" : ""; 87 | 88 | const queryString = `{ 89 | RapidAPI.Name.Function(${key1}:"${param1}", ${key2}:"${param2}") { 90 | a 91 | } 92 | }`; 93 | const val = await parse(queryString); 94 | assert.equal(val.length, 1); // Exactly 1 root node 95 | assert.equal(val[0].hasOwnProperty('args'), true); // Check type. Only function nodes have args (it can be sub-type) 96 | assert.equal(val[0].args[key1], `"${param1}"`); //Check simple arg 97 | assert.equal(val[0].args[key2], `"${param2}"`); //Check simple arg 98 | }); 99 | }); 100 | }); -------------------------------------------------------------------------------- /src/Parser/grammer.pegjs: -------------------------------------------------------------------------------- 1 | //To compile (while in directory) 2 | //npm install -g pegjs 3 | //pegjs grammer.pegjs 4 | 5 | 6 | start = Complex 7 | 8 | Complex = "{" firstNode:Node? nodes:("," Node)* "}" { 9 | var ns = []; 10 | nodes.forEach(function(n) { 11 | ns.push(n[1]); 12 | }); 13 | if (firstNode) 14 | return [firstNode, ...ns]; 15 | else 16 | return []; 17 | } 18 | 19 | Node 20 | = FlatObjectNode 21 | / RenameNode 22 | / OptionalNode 23 | / LogicNode 24 | / CachedFunctionNode 25 | / FunctionNode 26 | / CompositeNode 27 | / CastedLeafNode 28 | / LeafNode 29 | 30 | RenameNode = name:Word ":" innerNode:Node { 31 | const RenameNode = require('./../Nodes/RenameNode'); 32 | return new RenameNode(name, innerNode); 33 | } 34 | 35 | OptionalNode = "?" innerNode:Node { 36 | const OptionalNode = require('./../Nodes/OptionalNode') 37 | return new OptionalNode(innerNode); 38 | } 39 | 40 | FlatObjectNode = "-" innerNode:Node { 41 | const FlatObjectNode = require('./../Nodes/FlatObjectNode') 42 | return new FlatObjectNode(innerNode); 43 | } 44 | 45 | CastedLeafNode = cast:Word "(" innerNode:LeafNode ")" { 46 | const CastedLeafNode = require('./../Nodes/CastedLeafNode'); 47 | return new CastedLeafNode(cast, innerNode); 48 | } 49 | 50 | LeafNode = label:Word { 51 | const LeafNode = require('./../Nodes/LeafNode'), 52 | CompositeNode = require('./../Nodes/CompositeNode'), 53 | FunctionNode = require('./../Nodes/FunctionNode'); 54 | return new LeafNode(label); 55 | //return label; 56 | } 57 | 58 | CompositeNode = label:Word values:Complex { 59 | const LeafNode = require('./../Nodes/LeafNode'), 60 | CompositeNode = require('./../Nodes/CompositeNode'), 61 | FunctionNode = require('./../Nodes/FunctionNode'); 62 | return new CompositeNode(label, values); 63 | //return {'label' : label, 'value': values}; 64 | } 65 | 66 | LogicNode = "@" n:FunctionNode f:LogicNode?{ 67 | const LogicNode = require('./../Nodes/LogicNode'); 68 | return new LogicNode(n.getName(), n.children, n.args, f); 69 | //return {'t':'logic', 'l':n.label, 'a':n.args}; 70 | } 71 | 72 | CachedFunctionNode = "*" innerNode:FunctionNode { 73 | const CachedFunctionNode = require('./../Nodes/CachedFunctionNode'); 74 | return new CachedFunctionNode(innerNode); 75 | // return {type: 'cached', value:n} 76 | } 77 | 78 | FunctionNode = label:Word args:ArgSet values:Complex? { 79 | const LeafNode = require('./../Nodes/LeafNode'), 80 | CompositeNode = require('./../Nodes/CompositeNode'), 81 | FunctionNode = require('./../Nodes/FunctionNode'); 82 | return new FunctionNode(label, values, args); 83 | //return {'label': label, 'args': args, 'value': values}; 84 | } 85 | 86 | ArgSet = "(" tuple:KVTuple? tuples:("," KVTuple)* ")" { 87 | let rs = {}; 88 | Object.assign(rs, tuple); 89 | tuples.forEach(function(t) { 90 | Object.assign(rs, t[1]); //each object in tuples is an array containg comma and them KVTuple 91 | }); 92 | return rs; 93 | } 94 | 95 | //Key Value Tuple 96 | KVTuple = key:Word ":" value:KVValue { 97 | var rs = {}; 98 | rs[key] = value; 99 | return rs; 100 | } 101 | 102 | //Added new intermidiate type to support (key:{subkey:value}) 103 | KVValue = Number / ValueWord / Word / KVCompValue / KVArray 104 | 105 | //Support array parameters 106 | KVArray = "[" el0:KVValue? els:("," value:KVValue)* "]" { 107 | let res = []; 108 | if (el0) { 109 | res[0] = el0; 110 | els.forEach(function(e) { 111 | res.push(e[1]); 112 | }); 113 | } 114 | return res; 115 | } 116 | 117 | 118 | //This support having json types in args 119 | KVCompValue = "{}" {return {};} //empty 120 | / "{" key0:Word ":" value0:KVValue values:("," key:Word ":" value:KVValue)* "}" { 121 | let rs = {}; 122 | rs[key0] = value0; 123 | values.forEach(function(t) { 124 | rs[t[1]] = t[3]; 125 | }); 126 | return rs; 127 | } 128 | 129 | Word = chars:[-$!<=>@_0-9a-zA-Z.]+ { 130 | return chars.join(""); 131 | } / str:StringLiteralValue { 132 | return str.slice(1,-1); 133 | } 134 | 135 | ValueWord = StringLiteralValue 136 | 137 | Number = sign:"-"? chars:[-.0-9]+ { 138 | return parseFloat([sign].concat(chars).join("")); 139 | } 140 | 141 | StringLiteralValue 142 | = '"' chars:DoubleStringCharacter* '"' { return '"' +chars.join('') + '"'; } 143 | / "'" chars:SingleStringCharacter* "'" { return '"' +chars.join('') + '"'; } 144 | 145 | DoubleStringCharacter 146 | = !('"' / "\\") char:. { return char; } 147 | / "\\" sequence:EscapeSequence { return sequence; } 148 | 149 | SingleStringCharacter 150 | = !("'" / "\\") char:. { return char; } 151 | / "\\" sequence:EscapeSequence { return sequence; } 152 | 153 | EscapeSequence 154 | = "'" 155 | / '"' 156 | / "\\" 157 | / "b" { return "\b"; } 158 | / "f" { return "\f"; } 159 | / "n" { return "\n"; } 160 | / "r" { return "\r"; } 161 | / "t" { return "\t"; } 162 | / "v" { return "\x0B"; } -------------------------------------------------------------------------------- /src/Nodes/LogicNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 9/22/18. 3 | */ 4 | const SEP_CHAR = '.'; 5 | 6 | const FunctionNode = require('./FunctionNode'); // Uses recursive replace from there 7 | const ObjectNode = require('./ObjectNode'); 8 | const { createMixedContext } = require('./utils'); 9 | 10 | function __if(args, _) { 11 | const val1 = args['val1']; 12 | const val2 = args['val2']; 13 | const comparison = args['comparison'] || "=="; 14 | 15 | switch (comparison) { 16 | case "==": 17 | return val1 == val2; 18 | break; 19 | case "!=": 20 | return val1 != val2; 21 | break; 22 | case ">": 23 | return val1 > val2; 24 | break; 25 | case "<": 26 | return val1 < val2; 27 | break; 28 | case ">=": 29 | return val1 >= val2; 30 | break; 31 | case "<=": 32 | return val1 <= val2; 33 | break; 34 | case "set": 35 | return !!val1; 36 | break; 37 | case "prefixes": //val1 prefixes val2 38 | if (`${val1}`.length > `${val2}`.length) return false; 39 | return `${val1}` === `${val2}`.substr(0, val1.length); 40 | break; 41 | case "notSet": 42 | return !val1; 43 | break; 44 | case "in": 45 | if (!Array.isArray(val2)) 46 | throw `ComparisonError: for 'in' comparison, val1 should be an array. Got ${typeof val2} (value: ${val2}).`; 47 | return val2.indexOf(val1) >= 0; 48 | break; 49 | case "notIn": 50 | if (!Array.isArray(val2)) 51 | throw `ComparisonError: for 'in' comparison, val1 should be an array. Got ${typeof val2} (value: ${val2}).`; 52 | return val2.indexOf(val1) < 0; 53 | break; 54 | case "prefixIn": 55 | if (!Array.isArray(val2)) 56 | throw `ComparisonError: for 'in' comparison, val1 should be an array. Got ${typeof val2} (value: ${val2}).`; 57 | return val2.map(v2 => `${v2}` === `${val1}`.substr(0, v2.length)).filter(v2 => !!v2).length > 0; 58 | break; 59 | case "prefixNotIn": 60 | if (!Array.isArray(val2)) 61 | throw `ComparisonError: for 'in' comparison, val1 should be an array. Got ${typeof val2} (value: ${val2}).`; 62 | return val2.map(v2 => `${v2}` === `${val1}`.substr(0, v2.length)).filter(v2 => !!v2).length === 0; 63 | break; 64 | } 65 | } 66 | 67 | function __prefix(args) { 68 | return __if({ 69 | val1 : args['prefix'], 70 | val2 : args['string'], 71 | comparison : 'prefixes' 72 | }) 73 | } 74 | 75 | function __equal(args) { 76 | return __if({ 77 | val1 : args['val1'], 78 | val2 : args['val2'], 79 | comparison : '==' 80 | }) 81 | } 82 | 83 | const __ops = { 84 | "if" : __if, 85 | "prefix" : __prefix, 86 | "equal" : __equal, 87 | "else" : () => true, 88 | "elseif" : __if 89 | }; 90 | 91 | class LogicNode { 92 | constructor(name, children, args, followOnNode) { 93 | this.name = name; 94 | this.args = args; 95 | this.children = children; 96 | 97 | if (followOnNode && !(followOnNode.getName() === "@elseif" || followOnNode.getName() === "@else")) 98 | throw `Logic Node following an @if node must be either @elseif or @else, got: ${followOnNode.getName()}`; 99 | 100 | this.followOnNode = followOnNode; //Follow on node will be an @elseif or @else node 101 | } 102 | 103 | getName() { 104 | return `@${this.name.slice(0,-1)}`; //Get rid of preceding dot 105 | } 106 | 107 | /** 108 | * Process arguments and replace variables based on context 109 | * @param context 110 | * @returns {{}} 111 | */ 112 | getProcessedArgs(context) { 113 | //Process args in context 114 | //If they have " " -> string literal (remove quotes) 115 | //If they don't -> fetch from context 116 | return require('./FunctionNode').recursiveReplace(this.args, context); //Weird issues with using the FunctionNode object. To be explored 117 | } 118 | 119 | async eval(context, ops) { 120 | 121 | const operation = this.getName().slice(1); 122 | 123 | try { 124 | 125 | const result = __ops[operation](this.getProcessedArgs(context), null); 126 | 127 | if (!result) { 128 | if (this.followOnNode) { 129 | return await this.followOnNode.eval(context, ops); 130 | } 131 | return null; 132 | } 133 | 134 | let innerContext = createMixedContext(context, { 135 | [this.getName()] : {} 136 | }); 137 | let innerNode = new ObjectNode(this.getName(), this.children); 138 | return await innerNode.eval(innerContext, ops); 139 | 140 | } catch (e) { 141 | throw `Error in ${this.getName()}: ${e}`; 142 | } 143 | } 144 | } 145 | 146 | LogicNode.logicFunctions = __ops; 147 | 148 | module.exports = LogicNode; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/HttpDriver/HttpNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 5/25/17. 3 | */ 4 | 5 | const SEP_CHAR = '.'; 6 | const NODE_NAME = 'HttpNode'; 7 | const OBJECT_TYPE = 'object'; 8 | 9 | const _request = require('request'), 10 | queryString = require("query-string"); 11 | const limit = require("simple-rate-limiter"); 12 | 13 | const dns = require('dns'), 14 | dnscache = require('dnscache')({ 15 | "enable" : true, 16 | "ttl" : 360000, 17 | "cachesize" : 1000 18 | }); 19 | 20 | 21 | 22 | 23 | global.request = null; 24 | function getRequestClient(ops) { 25 | if (global.request !== null) 26 | return global.request; 27 | 28 | if (ops.hasOwnProperty('Http')) { 29 | if (ops.Http.hasOwnProperty('rateLimit')) { 30 | if (ops.Http.rateLimit.hasOwnProperty('count') && ops.Http.rateLimit.hasOwnProperty('period')) { 31 | global.request = limit(_request).to(ops.Http.rateLimit.count).per(ops.Http.rateLimit.period); 32 | return getRequestClient(ops); 33 | } 34 | } 35 | } 36 | 37 | global.request = _request; 38 | return getRequestClient(ops); 39 | 40 | } 41 | 42 | const functions = { 43 | get: null, 44 | post: null, 45 | put: null, 46 | delete: null 47 | }; 48 | 49 | class HttpNode { 50 | constructor(name, children, args) { 51 | this.name = name; 52 | this.args = args; 53 | this.children = children; 54 | } 55 | 56 | getName() { 57 | return `${this.name}`; 58 | } 59 | 60 | get signature() { 61 | return `Http.${this.operation.toUpperCase()} - ${this.urlWithParameters}`; 62 | } 63 | 64 | get queryParameters() { 65 | return this.args['params'] || {} 66 | } 67 | 68 | get urlWithParameters() { 69 | return `${(this.args['url'] || "")}${Object.keys(this.queryParameters).length ? "?" : ""}${queryString.stringify(this.queryParameters)}`; 70 | } 71 | 72 | get tokenizedName() { 73 | return this.name.split(SEP_CHAR); 74 | } 75 | 76 | get operation() { 77 | return this.tokenizedName[1]; 78 | } 79 | 80 | eval(context, ops) { 81 | 82 | const self = this; 83 | 84 | return new Promise((resolve, reject) => { 85 | const tokenizedName = this.tokenizedName; 86 | const operation = this.operation; 87 | 88 | // ops.Http is default HTTP parameters 89 | // self.args is patameters at time of call 90 | if(ops.Http === undefined) 91 | ops.Http = {}; 92 | if(!ops.Http.headers) 93 | ops.Http.headers = {}; 94 | 95 | 96 | if(self.args === undefined) 97 | self.args = {}; 98 | 99 | if(!functions.hasOwnProperty(operation)) 100 | return reject(`Operation Error: operation ${operation} does not exist / is not supported`); 101 | 102 | const params = self.queryParameters, 103 | url = self.urlWithParameters, 104 | body = (operation === 'get') ? (null) : (self.args['body'] || ""), 105 | form = (operation === 'get') ? (null) : (self.args['form'] || null), 106 | json = (operation === 'get') ? (null) : (self.args['json'] || null), 107 | headers = { ...ops.Http['headers'], ...self.args['headers'] } || {}, 108 | bearer = (self.args['bearer']) ? self.args['bearer'] : ops.Http['bearer'] || null, 109 | basic = (self.args['basic']) ? self.args['basic'] : ops.Http['basic'] || null, 110 | stopOnError = self.args.hasOwnProperty('stopOnError') ? self.args['stopOnError'] : true; 111 | 112 | if (bearer !== null) { 113 | headers['Authorization'] = `Bearer ${bearer}`; 114 | } 115 | 116 | if (basic !== null) { 117 | headers['Authorization'] = `Basic ${new Buffer(basic['username'] + ":" + basic['password']).toString("base64")}`; 118 | } 119 | 120 | getRequestClient(ops)(url, { 121 | method : operation, 122 | headers : headers, 123 | body : body, 124 | form : form, 125 | json : json 126 | }, (error, response, body) => { 127 | if (error) 128 | return reject(`HttpError: got error from ${url}: ${error}`); 129 | 130 | if(!response) 131 | return reject(`HttpError: no response from ${url}`); 132 | 133 | if(response.statusCode > 299 && stopOnError) 134 | return reject(`HttpError: got non-2xx response from ${url}: \ncode: ${response.statusCode}, \ncontent: ${response}, \nmessage: ${body}`); 135 | 136 | if(typeof body !== OBJECT_TYPE) { 137 | try { 138 | return resolve(JSON.parse(body)); 139 | } catch (e) { 140 | return resolve({body:body}); 141 | } 142 | } 143 | 144 | return resolve(body); 145 | }); 146 | }); 147 | } 148 | } 149 | 150 | module.exports = HttpNode; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/PostgreSQLDriver/test/whereGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 2/16/17. 3 | */ 4 | "use strict"; 5 | 6 | const assert = require('assert'), 7 | whereGenerator = require('./../whereGenerator').whereGenerator, 8 | whereClauseGenerator = require('./../whereGenerator').whereClauseGenerator; 9 | 10 | module.exports = () => { 11 | describe('whereClauseGenerator', () => { 12 | it('should support empty queries', () => { 13 | assert.equal(whereClauseGenerator({}), ""); 14 | }); 15 | 16 | it('should support single condition', () => { 17 | assert.equal(whereClauseGenerator({'a':'b'}), " WHERE a = 'b'"); 18 | }); 19 | 20 | it('should support multiple conditions', () => { 21 | assert.equal(whereClauseGenerator({'a':'b', 'c':'d'}), " WHERE a = 'b' AND c = 'd'"); 22 | }); 23 | 24 | it('should support complex conditions', () => { 25 | assert.equal(whereClauseGenerator({'a':'b', 'c':{'>':'d'}}), " WHERE a = 'b' AND c > 'd'"); 26 | }); 27 | }); 28 | 29 | 30 | describe('Where generator', ()=> { 31 | describe('Simple shorthand queries', () => { 32 | it('should support empty queries', () => { 33 | assert.equal(whereGenerator({}), ""); 34 | }); 35 | it('should support queries with a single condition', () => { 36 | assert.equal(whereGenerator({a:'1'}), " WHERE a = 1"); 37 | assert.equal(whereGenerator({a:'a'}), " WHERE a = 'a'"); 38 | }); 39 | 40 | it('should support queries with multiple parameters', () => { 41 | assert.equal(whereGenerator({a:'1', b:'ccc'}), " WHERE a = 1 AND b = 'ccc'") 42 | }); 43 | 44 | it('should support queries with complex parameters', () => { 45 | assert.equal(whereGenerator({a: {'>': '2'}}), " WHERE a > 2"); 46 | assert.equal(whereGenerator({a: {'>=': '2'}}), " WHERE a >= 2"); //Multi character operation 47 | }) 48 | }); 49 | 50 | describe('Simple full queries', () => { 51 | it('should support empty queries', () => { 52 | assert.equal(whereGenerator({WHERE:{}}), ""); 53 | }); 54 | it('should support queries with a single condition', () => { 55 | assert.equal(whereGenerator({WHERE:{a:'1'}}), " WHERE a = 1"); 56 | assert.equal(whereGenerator({WHERE:{a:'a'}}), " WHERE a = 'a'"); 57 | }); 58 | 59 | it('should support queries with multiple parameters', () => { 60 | assert.equal(whereGenerator({WHERE:{a:'1', b:'ccc'}}), " WHERE a = 1 AND b = 'ccc'") 61 | }); 62 | 63 | it('should support queries with complex parameters', () => { 64 | assert.equal(whereGenerator({WHERE:{a: {'>': '2'}}}), " WHERE a > 2"); 65 | assert.equal(whereGenerator({WHERE:{a: {'>=': '2'}}}), " WHERE a >= 2"); //Multi character operation 66 | }) 67 | }); 68 | 69 | describe('Complex queries', () => { 70 | describe('LIMIT', () => { 71 | it('should add a LIMIT clause for an empty query', () => { 72 | assert.equal(whereGenerator({LIMIT:"5"}), " LIMIT 5"); 73 | }); 74 | 75 | it('should add a LIMIT clause for a non empty shorthand query', () => { 76 | assert.equal(whereGenerator({a:'1', LIMIT:"5"}), " WHERE a = 1 LIMIT 5"); 77 | }); 78 | 79 | it('should add a LIMIT clause for a non empty full query', () => { 80 | assert.equal(whereGenerator({WHERE: {a:'1'}, LIMIT:"5"}), " WHERE a = 1 LIMIT 5"); 81 | }); 82 | }); 83 | 84 | describe('SKIP', () => { 85 | it('should add a SKIP clause for an empty query', () => { 86 | assert.equal(whereGenerator({SKIP:"5"}), " SKIP 5"); 87 | }); 88 | 89 | it('should add a SKIP clause for a non empty shorthand query', () => { 90 | assert.equal(whereGenerator({a:'1', SKIP:"5"}), " WHERE a = 1 SKIP 5"); 91 | }); 92 | 93 | it('should add a SKIP clause for a non empty full query', () => { 94 | assert.equal(whereGenerator({WHERE: {a:'1'}, SKIP:"5"}), " WHERE a = 1 SKIP 5"); 95 | }); 96 | }); 97 | 98 | describe('ORDER BY', () => { 99 | it('should support shorthand ORDER BY clause', () => { 100 | assert.equal(whereGenerator({ORDERBY:"year"}), " ORDER BY year"); 101 | }); 102 | 103 | it('should support full ORDER BY clause with 1 field', () => { 104 | assert.equal(whereGenerator({ORDERBY:{"murders":'DESC'}}), " ORDER BY murders DESC"); 105 | }); 106 | 107 | it('should support full ORDER BY clause with multiple fields', () => { 108 | assert.equal(whereGenerator({ORDERBY:{"murders":'DESC', 'regrets': 'ASC'}}), " ORDER BY murders DESC, regrets ASC"); //Thesis - ppl with more murders (i.e. serial murders) have less regrets. 109 | }); 110 | }) 111 | }); 112 | }); 113 | }; -------------------------------------------------------------------------------- /src/Nodes/FunctionNodes/MapReduce/test/mapReduce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 8/31/17. 3 | */ 4 | const { expect, assert } = require('chai'); 5 | const {mapReduce, scatterStep, reduceStep, mapStep} = require('./../mapReduce'); 6 | const identityFunction = a => a; 7 | 8 | module.exports = () => { 9 | describe('Map Reduce Logic', () => { 10 | describe('Map Step', () => { 11 | it ('Should support empty arrays', () => { 12 | assert.deepEqual([], mapStep([], identityFunction)); 13 | }); 14 | 15 | it('shouldnt change array by mapping to identity function', () => { 16 | let arr = ['avc', {a:1}, 13, null, undefined, 0]; 17 | assert.deepEqual(arr, mapStep(arr, identityFunction)); 18 | }); 19 | 20 | /** 21 | * Checks that it actually performs function enough times 22 | */ 23 | it('should perform map function for each item', () => { 24 | let count = 0; 25 | function inc (a) { 26 | count += a; 27 | } 28 | let arr = [1,2,3]; 29 | 30 | mapStep(arr, inc); 31 | 32 | assert.deepEqual(6, count); 33 | 34 | }); 35 | 36 | it('should return mapped array', () => { 37 | let arr = ["a", "b", "c"]; 38 | function map(a) { 39 | return a+a; 40 | } 41 | assert.deepEqual(mapStep(arr, map), ["aa", "bb", "cc"]); 42 | }); 43 | 44 | it('should flatten arrays', () => { 45 | let arr = ["a", "b", "c"]; 46 | function map(a) { 47 | return [a+a, a+a]; 48 | } 49 | assert.deepEqual(mapStep(arr, map), ["aa", "aa", "bb", "bb", "cc", "cc"]); 50 | }); 51 | }); 52 | 53 | describe('Scatter Step', () => { 54 | it('should scatter array', () => { 55 | let arr = [ 56 | {key: "a", value: 1}, 57 | {key: "a", value: 2}, 58 | {key: "a", value: "aa"}, 59 | {key: "b", value: 10}, 60 | {key: "b", value: null}, 61 | {key: "c", value: {}} 62 | ]; 63 | 64 | let scattered = { 65 | a: [1,2,"aa"], 66 | b: [10, null], 67 | c: [{}] 68 | }; 69 | 70 | assert.deepEqual(scatterStep(arr), scattered); 71 | }); 72 | }); 73 | 74 | describe('Reduce Step', () => { 75 | it ('should reduce array', () => { 76 | let src = { 77 | a: [1,2,3], 78 | b: [4,5,6], 79 | c: [7] 80 | }; 81 | 82 | let reduced = { 83 | a: 6, 84 | b: 15, 85 | c: 7 86 | }; 87 | 88 | function reduce(prev, current) { 89 | return prev + current; 90 | } 91 | 92 | assert.deepEqual(reduceStep(src, reduce, 0), reduced); 93 | 94 | }); 95 | }); 96 | 97 | describe ('Map Reduce', () => { 98 | 99 | it('Should throw error if not array (int)', (done) => { 100 | try { 101 | mapReduce(1); 102 | done('Did not throw an error'); 103 | } catch (e) { 104 | assert.equal(e, "First argument must be an array"); 105 | done(); 106 | } 107 | }); 108 | 109 | it('Should throw error if not array (object)', (done) => { 110 | try { 111 | mapReduce({}); 112 | done('Did not throw an error'); 113 | } catch (e) { 114 | assert.equal(e, "First argument must be an array"); 115 | done(); 116 | } 117 | }); 118 | 119 | it('Should throw error if not array (null)', (done) => { 120 | try { 121 | mapReduce(null); 122 | done('Did not throw an error'); 123 | } catch (e) { 124 | assert.equal(e, "First argument must be an array"); 125 | done(); 126 | } 127 | }); 128 | 129 | it ('should throw an error if map is not a function', (done) => { 130 | try { 131 | mapReduce([], []); 132 | done('Did not throw an error'); 133 | } catch (e) { 134 | assert.equal(e, "Second argument must be a function (map function)"); 135 | done(); 136 | } 137 | }); 138 | 139 | it ('should throw an error if reduce is not a function', (done) => { 140 | try { 141 | mapReduce([], identityFunction, 1); 142 | done('Did not throw an error'); 143 | } catch (e) { 144 | assert.equal(e, "Third argument must be a function (reduce function)"); 145 | done(); 146 | } 147 | }); 148 | 149 | it('Should count words in array of strings', () => { 150 | let strings = ["I want to eat", "I am hungry", "I am sleepy"]; 151 | let results = mapReduce(strings, (str) => { 152 | return str.split(" ").map((word) => { 153 | return {key:word, value: 1} 154 | }); 155 | }, (prev, current) => { 156 | return prev + current; 157 | }, 0); 158 | let expectedResults = { 159 | "I": 3, 160 | "want": 1, 161 | "to": 1, 162 | "eat": 1, 163 | "am": 2, 164 | "hungry": 1, 165 | "sleepy": 1 166 | }; 167 | assert.deepEqual(results, expectedResults); 168 | }); 169 | }); 170 | }); 171 | }; -------------------------------------------------------------------------------- /test/type-casted-leaf-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 7/14/17. 3 | */ 4 | "use strict"; 5 | 6 | const assert = require("assert"); 7 | const LeafNode = require("../src/Nodes/LeafNode"); 8 | const CastedLeafNode = require("../src/Nodes/CastedLeafNode"); 9 | 10 | describe("CastedLeafNode", () => { 11 | it("should throw error on unsupported type cast", (done) => { 12 | try { 13 | let a = new CastedLeafNode("DumbUnsupportedType", new LeafNode("a")); 14 | done("Didn't throw error"); 15 | } catch (e) { 16 | done(); 17 | } 18 | }); 19 | 20 | it('shouldnt throw error on supported type casts, and be case insensitive', () => { 21 | for (let t in CastedLeafNode.typeConverters) { 22 | let a; 23 | if (CastedLeafNode.typeConverters.hasOwnProperty(t)) { 24 | a = new CastedLeafNode(t, new LeafNode("a")); 25 | a = new CastedLeafNode(t.toLowerCase(), new LeafNode("a")); 26 | a = new CastedLeafNode(t.toUpperCase(), new LeafNode("a")); 27 | } 28 | } 29 | }); 30 | 31 | it('should convert types on eval - int', async () => { 32 | let i = await (new CastedLeafNode('int', new LeafNode("a"))).eval({a:"12"}); 33 | assert.equal(i, 12); 34 | 35 | i = await (new CastedLeafNode('int', new LeafNode("a"))).eval({a:-12}); 36 | assert.equal(i, -12); 37 | }); 38 | 39 | it('should convert types on eval - float', async () => { 40 | let i = await (new CastedLeafNode('float', new LeafNode("a"))).eval({a:"12.5"}); 41 | assert.equal(i, 12.5); 42 | 43 | i = await (new CastedLeafNode('float', new LeafNode("a"))).eval({a:-12.01}); 44 | assert.equal(i, -12.01); 45 | }); 46 | 47 | it('should convert types on eval - date', async () => { 48 | let i = await (new CastedLeafNode('date', new LeafNode("a"))).eval({a:"2017-01-01"}); 49 | assert.equal(i.getTime(), (new Date("2017-01-01")).getTime()); 50 | }); 51 | }); 52 | 53 | describe("CastedLeafNode.typeConverters", () => { 54 | 55 | describe("int", () => { 56 | const converter = CastedLeafNode.typeConverters.int; 57 | 58 | it('should return ints as is', () => { 59 | assert.equal(converter(1),1); 60 | assert.equal(converter(0),0); 61 | assert.equal(converter(-1),-1); 62 | }); 63 | 64 | it('should return floored floats', () => { 65 | assert.equal(converter(1.5),1); 66 | assert.equal(converter(1.2),1); 67 | assert.equal(converter(-1.2),-1); 68 | }); 69 | 70 | it('should parse strings', () => { 71 | assert.equal(converter("1.2"),1); 72 | assert.equal(converter("1"),1); 73 | assert.equal(converter("-1"),-1); 74 | }); 75 | 76 | it('should throw error on invalid strings', (done) => { 77 | try { 78 | converter("This is not a string"); //aka 'this is not a pipe' 79 | converter("SupraorbitalGland"); 80 | done(`Didn't throw an error`); 81 | } catch(e) { 82 | done(); 83 | } 84 | }); 85 | 86 | it('should throw error on invalid type - object', (done) => { 87 | try { 88 | converter({a:'b'}); //aka 'this is not a pipe' 89 | done(`Didn't throw an error`); 90 | } catch(e) { 91 | done(); 92 | } 93 | }); 94 | 95 | it('should throw error on invalid type - boolean', (done) => { 96 | try { 97 | converter(true); 98 | done(`Didn't throw an error`); 99 | } catch(e) { 100 | done(); 101 | } 102 | }); 103 | 104 | it('should throw error on invalid type - array', (done) => { 105 | try { 106 | converter(['0', 0, '7']); 107 | done(`Didn't throw an error`); 108 | } catch(e) { 109 | done(); 110 | } 111 | }); 112 | 113 | }); 114 | 115 | describe("float", () => { 116 | const converter = CastedLeafNode.typeConverters.float; 117 | 118 | it('should return floats as is', () => { 119 | assert.equal(converter(1),1.0); 120 | assert.equal(converter(0),0); 121 | assert.equal(converter(-1),-1); 122 | assert.equal(converter(-1.6),-1.6); 123 | }); 124 | 125 | it('should parse strings', () => { 126 | assert.equal(converter("1.2"),1.2); 127 | assert.equal(converter("1"),1); 128 | assert.equal(converter("-1"),-1); 129 | }); 130 | 131 | it('should throw error on invalid strings', (done) => { 132 | try { 133 | converter("This is not a string"); //aka 'this is not a pipe' 134 | converter("SupraorbitalGland"); 135 | done(`Didn't throw an error`); 136 | } catch(e) { 137 | done(); 138 | } 139 | }); 140 | 141 | it('should throw error on invalid types - object', (done) => { 142 | try { 143 | converter({a:'b'}); //aka 'this is not a pipe' 144 | done(`Didn't throw an error`); 145 | } catch(e) { 146 | done(); 147 | } 148 | }); 149 | 150 | it('should throw error on invalid types - boolean', (done) => { 151 | try { 152 | converter(true); 153 | done(`Didn't throw an error`); 154 | } catch(e) { 155 | done(); 156 | } 157 | }); 158 | 159 | it('should throw error on invalid types - array', (done) => { 160 | try { 161 | converter(['0', 0, '7']); 162 | done(`Didn't throw an error`); 163 | } catch(e) { 164 | done(); 165 | } 166 | }); 167 | 168 | }); 169 | 170 | describe("date", () => { 171 | const converter = CastedLeafNode.typeConverters.date; 172 | 173 | it('should convert dates', () => { 174 | assert.equal(converter(1).getTime(),(new Date(1)).getTime()); 175 | assert.equal(converter("2017-01-01").getTime(),(new Date("2017-01-01")).getTime()); 176 | }); 177 | 178 | it('should throw an error on invalid types - invalid string', (done) => { 179 | try { 180 | converter("This is not a string"); //aka 'this is not a pipe' 181 | done(`Didn't throw an error`); 182 | } catch(e) { 183 | done(); 184 | } 185 | }); 186 | 187 | it('should throw an error on invalid types - object', (done) => { 188 | try { 189 | converter({a:'b'}); //aka 'this is not a pipe' 190 | done(`Didn't throw an error`); 191 | } catch(e) { 192 | done(); 193 | } 194 | }); 195 | 196 | it('should throw an error on invalid types - boolean', (done) => { 197 | try { 198 | converter(true); //aka 'this is not a pipe' 199 | done(`Didn't throw an error`); 200 | } catch(e) { 201 | done(); 202 | } 203 | }); 204 | 205 | it('should throw an error on invalid types - arrau', (done) => { 206 | try { 207 | converter(['0', 0, '7']); //aka 'this is not a pipe' 208 | done(`Didn't throw an error`); 209 | } catch(e) { 210 | done(); 211 | } 212 | }); 213 | }); 214 | 215 | }); -------------------------------------------------------------------------------- /src/Nodes/FunctionNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 12/19/16. 3 | */ 4 | "use strict"; 5 | const LeafNode = require('./LeafNode'), 6 | ObjectNode = require('./ObjectNode'), 7 | ArrayNode = require('./ArrayNode'), 8 | CompositeNode = require('./CompositeNode'), 9 | LogicNode = require('./LogicNode'); 10 | 11 | const { createMixedContext, resolve } = require('./utils'); 12 | 13 | const Mustache = require('mustache'); 14 | 15 | const supportedTypes = { 16 | "RapidAPI" : require('./FunctionNodes/RapidAPIDriver/RapidAPINode'), 17 | "PostgreSQL" : require('./FunctionNodes/PostgreSQLDriver/PostgreSQLNode'), 18 | "MySQL" : require('./FunctionNodes/MySQLDriver/MySQLNode'), 19 | "Http" : require('./FunctionNodes/HttpDriver/HttpNode'), 20 | "Redis" : require('./FunctionNodes/RedisDriver/RedisNode'), 21 | "MongoDB" : require('./FunctionNodes/MongoDBDriver/MongoDBNode'), 22 | "MapReduce" : require('./FunctionNodes/MapReduce/MapReduceNode'), 23 | "Csv" : require('./FunctionNodes/CsvDriver/CsvNode'), 24 | "Logic" : require('./LogicNode'), 25 | ...LogicNode.logicFunctions // This is a work around for the parser, as it first initializes logic nodes as function nodes (if(...){...}) and then converts them to logic nodes and it sees prefixing '@'. Better solution TBD 26 | }; 27 | 28 | const SEP_CHAR = '.'; 29 | 30 | /** 31 | * Check if a string is wrapped in quotes 32 | * @param arg the string to be checked 33 | * @returns {boolean} 34 | */ 35 | function quoted(arg) { 36 | return ((arg[0] == `"` && arg[arg.length - 1] == `"`) || (arg[0] == `'` && arg[arg.length - 1] == `'`)); 37 | } 38 | 39 | /** 40 | * If key is surrounded by quotes, this will remove them 41 | * @param key 42 | * @returns {Array.|string|Blob|ArrayBuffer} 43 | */ 44 | function removeQuotes(key) { 45 | return quoted(key) ? key.slice(1, -1) : key; 46 | } 47 | 48 | 49 | /** 50 | * Replaces template ({{var}}) with values from the context 51 | * @param value the template 52 | * @param context 53 | */ 54 | function replaceVariables(value, context) { 55 | return Mustache.render(value, context); 56 | } 57 | 58 | /** 59 | * Processes the arg tree, keeping constants and replacing vars from the context 60 | * @param args arg tree to be processed 61 | * @param context context to take variable values from 62 | * @returns {{}} 63 | */ 64 | function recursiveReplace(args, context) { 65 | let processedArgs = Array.isArray(args) ? [] : {}; 66 | for (let key in args) { 67 | if (args.hasOwnProperty(key)) { 68 | let arg = args[key]; 69 | 70 | //remove quotes from key if needed 71 | key = removeQuotes(key); 72 | 73 | //If string, process directly. Else, recurse down the rabbit hole. 74 | switch (typeof arg) { 75 | case 'string': 76 | //Check for quotes: 77 | processedArgs[key] = quoted(arg) ? replaceVariables(arg.slice(1, -1), context) : resolve(arg, context); 78 | //If literal - Remove quotes, render template and add to processed args 79 | // If Variable - get from context 80 | break; 81 | case 'number': 82 | processedArgs[key] = arg; 83 | break; 84 | case 'object': 85 | processedArgs[key] = recursiveReplace(arg, context); 86 | break; 87 | default: 88 | throw `Type Error: type ${typeof arg} for argument ${key} is not supported`; 89 | break; 90 | } 91 | } 92 | } 93 | return processedArgs; 94 | } 95 | 96 | class FunctionNode { 97 | /** 98 | * Construct a new functional node, determining it's type at compile time and throwing an error if type doesn't exist. 99 | * @param name node name 100 | * @param children node's children (array) 101 | * @param args node's arguments (object) 102 | */ 103 | constructor(name, children, args) { 104 | this.name = name; 105 | this.args = args; 106 | this.children = children; 107 | 108 | //Check if type is supported 109 | if (supportedTypes.hasOwnProperty(this.name.split(SEP_CHAR)[0])) { 110 | this.type = this.name.split(SEP_CHAR)[0]; 111 | this.name = this.name.split(SEP_CHAR).splice(1).join(SEP_CHAR); 112 | } else { 113 | throw `Type Error: Function node type ${this.name.split(SEP_CHAR)[0]} not supported.`; 114 | } 115 | } 116 | 117 | /** 118 | * Process arguments and replace variables based on context 119 | * @param context 120 | * @returns {{}} 121 | */ 122 | getProcessedArgs(context) { 123 | //Process args in context 124 | //If they have " " -> string literal (remove quotes) 125 | //If they don't -> fetch from context 126 | return recursiveReplace(this.args, context); 127 | } 128 | 129 | /** 130 | * 131 | * @returns {string|*} 132 | */ 133 | getName() { 134 | return `${this.type}.${this.name}`; 135 | } 136 | 137 | 138 | //noinspection JSAnnotator 139 | /** 140 | * 141 | * @param context 142 | * @param ops 143 | * @param cachedResult (optional) if set, will not perform query, but use that result 144 | * @returns {Promise} 145 | */ 146 | async eval(context, ops) { 147 | try { 148 | const processedArgs = this.getProcessedArgs(context); 149 | 150 | const materialValuePayload = await this.performFunction(processedArgs, context, ops); 151 | return await this.continueTree(context, ops, materialValuePayload); 152 | 153 | } catch (e) { 154 | throw `Error in ${this.getName()}: ${e}`; 155 | } 156 | 157 | } 158 | 159 | async performFunction(processedArgs, context, ops) { 160 | 161 | const exStart = process.hrtime(); 162 | 163 | //Determine material node type and materialize itself 164 | const MaterialClass = supportedTypes[this.type]; 165 | const materialNode = new MaterialClass(this.getName(), this.children, processedArgs); 166 | const val = await materialNode.eval(context, ops); 167 | 168 | const exEnd = process.hrtime(exStart); 169 | ops.logger.log(`Executing: ${materialNode.signature || materialNode.getName()} (took ${exEnd[0]}s ${exEnd[1]/1000000}ms)`); 170 | 171 | return val; 172 | } 173 | 174 | /** 175 | * Appends payload from function node to context and continues tree execution 176 | * @param materialNode 177 | * @param context 178 | * @param ops 179 | * @param payload 180 | * @returns {Promise.<*>} 181 | */ 182 | async continueTree(context, ops, payload) { 183 | //Create context and add payload to it 184 | let ctx = Object.assign({}, context); //TODO optimize to use mixedContext wrapper 185 | ctx[this.getName()] = payload; 186 | 187 | //Process down the tree... 188 | if(typeof payload === 'string') { 189 | return await (new LeafNode(this.getName())).eval(ctx); 190 | } else if(typeof payload === 'object') { 191 | let innerContext = createMixedContext(context, { 192 | [this.getName()] : payload 193 | }); 194 | // let innerContext = Object.assign({}, context); 195 | // innerContext[this.getName()] = payload; 196 | 197 | let innerNode = new CompositeNode(this.getName(), this.children); 198 | 199 | return await innerNode.eval(innerContext, ops); 200 | 201 | } else { //"You don't het another chance, life ain't a Nintendo game" - Eminem 202 | throw `APIError: got invalid data type ${typeof payload} which is not supported by function nodes` ; 203 | } 204 | } 205 | } 206 | 207 | FunctionNode.recursiveReplace = recursiveReplace; 208 | FunctionNode.quoted = quoted; 209 | FunctionNode.removeQuotes = removeQuotes; 210 | 211 | module.exports = FunctionNode; 212 | 213 | //PLAYGROUND: 214 | 215 | /*let fn = new FunctionNode('RapidAPI.GoogleTranslate.translate', [], { 216 | 'apiKey': '"AIzaSyCDogEcpeA84USVXMS471PDt3zsG-caYDM"', 217 | 'string': '"hello world, what a great day"', 218 | 'targetLanguage': '"de"' 219 | }); 220 | 221 | let ops = { 222 | RapidAPI : { 223 | projectName : 'Iddo_demo_app_1', 224 | apiKey: '4c6e5cc0-8426-4c95-9e3b-60adba0e50f6' 225 | } 226 | }; 227 | 228 | fn.eval({}, ops) 229 | .then((res) => {console.log(JSON.stringify(res))}) 230 | .catch((err) => {console.warn(JSON.stringify(err))});*/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RapidQL 2 | [![](https://i.imgur.com/9iuHMLS.png)](http://rapidql.com) 3 | 4 | **RapidQL** let's developer query multiple API resources at a time, combining them to create 1 unified Query. 5 | 6 | [![NPM version](https://badge.fury.io/js/rapidql.svg)](https://www.npmjs.com/package/rapidql) 7 | [![](https://circleci.com/gh/iddogino/rapidql.svg?style=shield&circle-token=70838eabb9e7255c543594d9c12d44db3e9b979e)](https://circleci.com/gh/iddogino/rapidql) 8 | 9 | ## Full Documentation 10 | See the [RapidQL documentation](https://docs.rapidql.com) 11 | 12 | ## Initialization 13 | 14 | After requiring the RapidQL package, you can initialize it. You may also pass options, such as RapidAPI credentials or default HTTP Parameters 15 | ```javascript 16 | const RapidQL = require('RapidQL'); 17 | let rql = new RapidQL({ 18 | Http : { 19 | X-RapidAPI-Header: '***********' 20 | }, 21 | RapidAPI : { 22 | projectName : '########', 23 | apiKey: '##########' 24 | } 25 | }); 26 | ``` 27 | ## Querying 28 | 29 | You can perform queries using the method rql.query(). It takes 2 arguments: 30 | 31 | 1. The query string 32 | 2. *[optional]* A base context. You may use the base context for parameter passing (any parameters such as user name used in the query should be passed through the base context. The query string should be completely static). 33 | 34 | Queries return promises. If the promise rejects, the returned value will be the error message. If the query resolves, the returned value will be the query result. 35 | ```javascript 36 | //Running this query on this base context will return the object {a:1} 37 | rql.query(` 38 | { 39 | a 40 | } 41 | `, { 42 | a: 1, 43 | b: 2 44 | }).then(console.log).catch(console.warn); 45 | ``` 46 | 47 | ## Logging 48 | 49 | Similarly to querying, you can directly log the results of your query using the rql.log() method. It takes 2 arguments: 50 | 51 | 1. The query string 52 | 2. *[optional]* A base context. You may use the base context for parameter passing (any parameters such as user name used in the query should be passed through the base context. The query string should be completely static). 53 | 54 | Queries return promises. If the promise rejects, the returned value will be the error message. If the query resolves, the returned value will be the query result. 55 | ```javascript 56 | //Running this query on this base context will return the object {a:1} 57 | rql.log(` 58 | { 59 | a 60 | } 61 | `, { 62 | a: 1, 63 | b: 2 64 | }) 65 | ``` 66 | ## HTTP Requests 67 | With RapidQL, you can also connect to any HTTP url using Http.get(). You may also use patch, post, and put requests. 68 | ```javascript 69 | const RapidQL = require('RapidQL'); 70 | const rql = new RapidQL({}); 71 | 72 | rql.log(`{ 73 | Http.get(url:"http://httpbin.org/ip") { 74 | origin 75 | } 76 | }`); 77 | ``` 78 | 79 | An HTTP request in RQL can take many parameters beyond the url. The params include: 80 | 81 | - __url__: a fully qualified URI 82 | - __body__: entity body for PATCH, POST and PUT requests (not usable on GET requests) 83 | - __form__: data to pass for a multipart/form-data request (not usable on GET requests) 84 | - __json__: a boolean that when true, sets body to JSON representation of value and adds the 85 | - __Content-type__: application/json header (not usable on GET requests) 86 | - __headers__: HTTP headers (default: {}) 87 | - __bearer__: an OAuth bearer token 88 | - __basic__: credentials for basic auth. 89 | 90 | ### Setting Default HTTP Parameters 91 | When initializing your RapidQL instance, you can provide default parameters for HTTP requests by supplying an Http object as an option. This Http can set default parameters for headers, bearer, and basic. These parameters will then be sent with every HTTP request. 92 | ```javascript 93 | const RapidQL = require('RapidQL'); 94 | const rql = new RapidQL({ 95 | Http: { 96 | headers : { 97 | 'X-RapidAPI-Key' : '*****************', 98 | 'default' : 'this header will always be sent, unless defined otherwise at time of call' 99 | }, 100 | basic : { 101 | username : 'my_username', 102 | password : 'my_password' 103 | } 104 | } 105 | }); 106 | 107 | rql.log(`{ 108 | Http.post( 109 | url:"http://httpbin.org/ip" 110 | ){ 111 | 112 | } 113 | }`) 114 | ``` 115 | 116 | ### Escaping Strings 117 | If you need to have a variable string within on of your queries (Ex/ URL parameters for an API) you're able to escape the string by using the following notation: {{variable_name}}. An example of how to use this is the following: 118 | ```javascript 119 | const RapidQL = require('RapidQL'); 120 | const rql = new RapidQL({}); 121 | 122 | rql.log(`{ 123 | Http.post( 124 | url:"http://httpbin.org/status/{{status_code}}" 125 | ){ 126 | 127 | } 128 | }`, { 129 | status_code: 400 130 | }) 131 | ``` 132 | 133 | ## Sample queries 134 | 135 | Get user from a database and do validation of both email and phone number 136 | ```javascript 137 | rql.log(`{ 138 | MySQL.public.users.find(username:input) { 139 | email, 140 | phoneNumber, 141 | name, 142 | - Telesign:Http.post( 143 | url: 'https://telesign-telesign-score-v1.p.rapidapi.com/score/{{phoneNumber}}', 144 | headers : { 145 | 'Content-Type' : 'application/x-www-form-urlencoded' 146 | }, 147 | params : { 148 | 'account_lifecycle_event' : 'create' 149 | } 150 | ){ 151 | phone_number_risk : risk 152 | }, 153 | - Mailboxlayer:Http.get( 154 | url: 'https://apilayer-mailboxlayer-v1.p.rapidapi.com/check', 155 | headers : { 156 | }, 157 | params : { 158 | smtp: '1', 159 | catch_all: '0', 160 | email: email, 161 | access_key: '************************' 162 | } 163 | ){ 164 | email_score:score 165 | } 166 | } 167 | }` , { 168 | input : 'rapidapi' 169 | }) 170 | ``` 171 | 172 | ## DB Queries 173 | RapidQL may also be used for querying databases. Database queries and API queries may be combined to create advance data gathering logic. 174 | 175 | ### Set Up 176 | To add a database connection to your rql instance, you need to add it's connection details in the RapidQL initialization: 177 | ```javascript 178 | const RapidQL = require('RapidQL'); 179 | const rql = new RapidQL({ 180 | PostgreSQL: { 181 | Sample: { 182 | user: 'admin', //required 183 | database: 'compose', //required 184 | password: '#########', //required 185 | host: 'aws-us-east-1-portal.23.dblayer.com', // required 186 | port: 17052, //required 187 | max: 10, // optional - max connections 188 | idleTimeoutMillis: 30000 // optional - how long a client is allowed to remain idle before being closed 189 | } 190 | } 191 | }); 192 | ``` 193 | 194 | Once the RapidQL instance is connected to the DB, you may query it. The object you're querying will have the following schema: 195 | ```javascript 196 | DBType.DBName.Schema.Table.Operation 197 | ``` 198 | 199 | Where: 200 | 201 | - **DBType** : the type of DB you're querying (PostgreSQL, MySQL, Redis, etc...) 202 | - **DBName** : the name you used when in configuring the DB (as you can be connected to multiple DBs of each type) 203 | - **Schema** : the schema you wish to work with 204 | - **Table** : name of the table to be queried 205 | 206 | For example, `PostgreSQL.Sample.public.users.select` will query the `Sample` PostgreSQL DB (same sample we used in configuration above), and perform a `select` query on the `users` table in the `public` schema. 207 | 208 | ### Select 209 | The most basic way to perform select queries is by passing equality comparisons: 210 | ```javascript 211 | PostgreSQL.Sample.public.users.select(location: "US") 212 | ``` 213 | 214 | This will find all users where location is 'US'. 215 | 216 | For more complex conditions use: 217 | ```javascript 218 | PostgreSQL.Sample.public.users.select(birthyear: {"<=": "1997"}) 219 | ``` 220 | 221 | This will find users whose birth year is smaller than or equal to 1997. Using `.select(location:"US") is shorthand for .select(location:{"=":"US"})` You can have multiple conditions, mixing between comparison styles: 222 | ```javascript 223 | PostgreSQL.Sample.public.users.select(location: 'US', birthyear: {"<=": "1997"}) 224 | ``` 225 | 226 | ### Complex queries (SKIP, LIMIT, ORDER BY) 227 | 228 | `PostgreSQL.Sample.public.users.select(location: "US")` is shorthand for `PostgreSQL.Sample.public.users.select(WHERE:{"location": "US"})`. Using the full syntax you may add skip, limit and order by clauses. 229 | ```javascript 230 | PostgreSQL.Sample.public.users.select(WHERE:{"location": "US"}, LIMIT:"3", SKIP:"1", ORDERBY: {birthyear:"DESC"}) 231 | ``` 232 | 233 | ***Note case sensitivity.*** 234 | 235 | ### Count 236 | Count works just like select, only it returns the `count` value. 237 | ```javascript 238 | { 239 | PostgreSQL.GCELogs.public.blockcalls.count(LIMIT:"10", GROUPBY:"package", ORDERBY:{count:"DESC"}) { 240 | package, 241 | count 242 | } 243 | } 244 | ``` 245 | 246 | ## Running in commandline 247 | Install RapidQL from NPM with the `-g` flag to use from command line. Than, you can use: 248 | 249 | rql queryFile.rql 250 | 251 | To run the query in `queryFile.rql`. RapidQL will also look for 2 optional hidden files: 252 | 253 | - `.rqlconfig` - json file containing your configurations (DB / RpaidAPI connection details). 254 | - `.rqlcontext` - base context for RQL (define variables used in the query). 255 | 256 | ## RQL Server 257 | Install RapidQL from NPM with the `-g` flag to use from command line. 258 | To use RQL from platforms other than nodejs, you can either use it as a command line executable (see above), or run it as a server. Running `rql-server` will set up an HTTP server, accepting RQL queries and returning their results in JSON format. 259 | 260 | ### Parameters: 261 | 262 | - **-p** / **--port** : set the port rql will listen on. 3000 by default. 263 | - **-c** / **--conf** : set the config file for rql to pull configurations from. By default - .rqlconfig at the same path. 264 | 265 | ### API: 266 | -------------------------------------------------------------------------------- /test/logic-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by iddo on 9/25/18. 3 | */ 4 | "use strict"; 5 | 6 | const {assert} = require('chai'); 7 | const { describe, it } = require('mocha'); 8 | const LeafNode = require("../src/Nodes/LeafNode"); 9 | const LogicNode = require("../src/Nodes/LogicNode"); 10 | 11 | describe("LogicNode", () => { 12 | let _context = { 13 | a: "aaa", 14 | b: "bbb", 15 | c: 10, 16 | d: 5, 17 | obj: { 18 | aaa: "aaa" 19 | } 20 | }; 21 | 22 | describe ("__if logic", () => { 23 | const __if = LogicNode.logicFunctions.if; 24 | const prettyIf = (val1, val2, comparison) => __if({val1,val2,comparison}); 25 | 26 | describe("==", () => { 27 | it ("should use comparison by default", () => { 28 | assert.isOk(prettyIf("aa","aa")); 29 | assert.isNotOk(prettyIf("aa","bb")); 30 | }); 31 | 32 | it ("should return true on positive comparison", () => { 33 | assert.isOk(prettyIf("aa","aa", "==")); 34 | }); 35 | 36 | it ("should return false on negative comparison", () => { 37 | assert.isNotOk(prettyIf("aa","abba", "==")); 38 | }); 39 | }); 40 | 41 | describe("!=", () => { 42 | let comp = "!="; 43 | it ("should return true on positive comparison", () => { 44 | assert.isOk(prettyIf("aa","bb", comp)); 45 | }); 46 | 47 | it ("should return false on negative comparison", () => { 48 | assert.isNotOk(prettyIf("aa","aa", comp)); 49 | }); 50 | }); 51 | 52 | describe(">", () => { 53 | let comp = ">"; 54 | it ("should return true on positive comparison", () => { 55 | assert.isOk(prettyIf(5,3, comp)); 56 | }); 57 | 58 | it ("should return false on negative comparison", () => { 59 | assert.isNotOk(prettyIf(4,83, comp)); 60 | }); 61 | }); 62 | 63 | describe(">=", () => { 64 | let comp = ">="; 65 | it ("should return true on positive comparison", () => { 66 | assert.isOk(prettyIf(5,3, comp)); 67 | }); 68 | 69 | it ("should return false on negative comparison", () => { 70 | assert.isNotOk(prettyIf(4,83, comp)); 71 | }); 72 | }); 73 | 74 | describe("<", () => { 75 | let comp = "<"; 76 | it ("should return true on positive comparison", () => { 77 | assert.isOk(prettyIf(3,35, comp)); 78 | }); 79 | 80 | it ("should return false on negative comparison", () => { 81 | assert.isNotOk(prettyIf(48,8, comp)); 82 | }); 83 | }); 84 | 85 | describe("<=", () => { 86 | let comp = "<="; 87 | it ("should return true on positive comparison", () => { 88 | assert.isOk(prettyIf(3,35, comp)); 89 | }); 90 | 91 | it ("should return false on negative comparison", () => { 92 | assert.isNotOk(prettyIf(48,8, comp)); 93 | }); 94 | }); 95 | 96 | describe("prefixes", () => { 97 | let comp = "prefixes"; 98 | it ("should return true on positive comparison", () => { 99 | assert.isOk(prettyIf("+1","+14158496404", comp)); 100 | }); 101 | 102 | it ("should return false on negative comparison", () => { 103 | assert.isNotOk(prettyIf("+972","+14158496404", comp)); 104 | }); 105 | }); 106 | 107 | describe("in", () => { 108 | let comp = "in"; 109 | it ("should return true on positive comparison", () => { 110 | assert.isOk(prettyIf("+1",["+1","+2","+3"], comp)); 111 | }); 112 | 113 | it ("should return false on negative comparison", () => { 114 | assert.isNotOk(prettyIf("+4",["+1","+2","+3"], comp)); 115 | }); 116 | }); 117 | 118 | describe("notIn", () => { 119 | let comp = "notIn"; 120 | it ("should return true on positive comparison", () => { 121 | assert.isOk(prettyIf("+4",["+1","+2","+3"], comp)); 122 | }); 123 | 124 | it ("should return false on negative comparison", () => { 125 | assert.isNotOk(prettyIf("+1",["+1","+2","+3"], comp)); 126 | }); 127 | }); 128 | 129 | describe("prefixIn", () => { 130 | let comp = "prefixIn"; 131 | it ("should return true on positive comparison", () => { 132 | assert.isOk(prettyIf("+14158496404",["+1","+2","+3"], comp)); 133 | }); 134 | 135 | it ("should return false on negative comparison", () => { 136 | assert.isNotOk(prettyIf("+972544266822",["+1","+2","+3"], comp)); 137 | }); 138 | }); 139 | 140 | describe("prefixNotIn", () => { 141 | let comp = "prefixNotIn"; 142 | it ("should return true on positive comparison", () => { 143 | assert.isOk(prettyIf("+972544266822",["+1","+2","+3"], comp)); 144 | }); 145 | 146 | it ("should return false on negative comparison", () => { 147 | assert.isNotOk(prettyIf("+14158496404",["+1","+2","+3"], comp)); 148 | }); 149 | }); 150 | }); 151 | 152 | describe("general behaviour", () => { 153 | 154 | 155 | it ("should return null when condition is false", async () => { 156 | let ln = new LogicNode("if.", [ 157 | new LeafNode('a') 158 | ], { 159 | comparison: '"=="', 160 | val1: '"aaa"', 161 | val2: '"bbb"' 162 | }); 163 | 164 | let res = await ln.eval(_context, {}); 165 | assert.deepEqual(res, null); 166 | }); 167 | 168 | it ("should return inner object when evaluation is true", async () => { 169 | let ln = new LogicNode("if.", [ 170 | new LeafNode('a') 171 | ], { 172 | comparison: '"=="', 173 | val1: '"aaa"', 174 | val2: '"aaa"' 175 | }); 176 | 177 | let res = await ln.eval(_context, {}); 178 | assert.deepEqual(res, {a: "aaa"}); 179 | }); 180 | 181 | it ("should use variable data for comparison as well as literals", async () => { 182 | let ln = new LogicNode("if.", [ 183 | new LeafNode('a') 184 | ], { 185 | comparison: '"=="', 186 | val1: '"aaa"', 187 | val2: 'a' 188 | }); 189 | 190 | let res = await ln.eval(_context, {}); 191 | assert.deepEqual(res, {a: "aaa"}); 192 | }); 193 | 194 | it ("should support variable data from deep object for comparison", async () => { 195 | let ln = new LogicNode("if.", [ 196 | new LeafNode('a') 197 | ], { 198 | comparison: '"=="', 199 | val1: 'obj.aaa', 200 | val2: 'a' 201 | }); 202 | 203 | let res = await ln.eval(_context, {}); 204 | assert.deepEqual(res, {a: "aaa"}); 205 | }); 206 | 207 | }); 208 | 209 | describe("follow on nodes (elseif, else)", () => { 210 | it ("should not run @else node if @if node is true", async () => { 211 | let ln = new LogicNode("if.", [ 212 | new LeafNode('a') 213 | ], { 214 | comparison: '"=="', 215 | val1: '"aaa"', 216 | val2: '"aaa"' 217 | }, new LogicNode( "else.", [ 218 | new LeafNode('b') 219 | ], { 220 | 221 | })); 222 | 223 | let res = await ln.eval(_context, {}); 224 | assert.deepEqual(res, {a: "aaa"}); 225 | }); 226 | 227 | it ("should default to @else node if @if node is false", async () => { 228 | let ln = new LogicNode("if.", [ 229 | new LeafNode('a') 230 | ], { 231 | comparison: '"=="', 232 | val1: '"aaa"', 233 | val2: '"bbb"' 234 | }, new LogicNode( "else.", [ 235 | new LeafNode('b') 236 | ], { 237 | 238 | })); 239 | 240 | let res = await ln.eval(_context, {}); 241 | assert.deepEqual(res, {b: "bbb"}); 242 | }); 243 | 244 | it ("should default to @elseif node if @if node is false and @elseif is true", async () => { 245 | let ln = new LogicNode("if.", [ 246 | new LeafNode('a') 247 | ], { 248 | comparison: '"=="', 249 | val1: '"aaa"', 250 | val2: '"bbb"' 251 | }, new LogicNode( "elseif.", [ 252 | new LeafNode('b') 253 | ], { 254 | comparison: '"=="', 255 | val1: '"aaa"', 256 | val2: '"aaa"' 257 | }, new LogicNode( "else.", [ 258 | new LeafNode('c') 259 | ], { 260 | 261 | }))); 262 | 263 | let res = await ln.eval(_context, {}); 264 | assert.deepEqual(res, {b: "bbb"}); 265 | }); 266 | 267 | it ("should default to @else node if @if node is false and @else is false", async () => { 268 | let ln = new LogicNode("if.", [ 269 | new LeafNode('a') 270 | ], { 271 | comparison: '"=="', 272 | val1: '"aaa"', 273 | val2: '"bbb"' 274 | }, new LogicNode( "elseif.", [ 275 | new LeafNode('b') 276 | ], { 277 | comparison: '"=="', 278 | val1: '"aaa"', 279 | val2: '"bbb"' 280 | }, new LogicNode( "else.", [ 281 | new LeafNode('c') 282 | ], { 283 | 284 | }))); 285 | 286 | let res = await ln.eval(_context, {}); 287 | assert.deepEqual(res, {c: 10}); 288 | }); 289 | }); 290 | 291 | }); --------------------------------------------------------------------------------