├── 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 |
5 |
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 |
5 |
6 |
7 |
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 | 
--------------------------------------------------------------------------------
/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 | [](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 | [](https://www.npmjs.com/package/rapidql)
7 | [](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 | });
--------------------------------------------------------------------------------