├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── modules │ └── queryEngine_spec.js └── testQueries.js ├── docs └── schema.md ├── package.json └── source └── js ├── dao ├── baseDAO.js └── graphNode.js ├── modules ├── __mocks__ │ └── db.js ├── db.js └── queryEngine.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | data/db 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alex Jansen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphNoQL 2 | Facebook's GraphQL backend implemented with Node+Mongo 3 | 4 | # Brief 5 | 6 | `./source/js/modules/queryEngine.js` is the primary module in this project, which 7 | takes in a query object (defined in `./docs/schema.md`)", and a callback. It then 8 | makes a bunch of mongo queries and returns the requested tree to a callback. 9 | 10 | # Example 11 | 12 | We have tweaked Facebook's GraphQL format to be JSON compatible. The biggest 13 | change is that we got rid of calls, and use string id's for root node queries, 14 | and special **Edge** objects for querying edges. 15 | 16 | Let's look at an example query: 17 | 18 | ```js 19 | { 20 | nodeidtest: { // Give a Node ID for us to start from 21 | id: true, // Define which fields we want to return 22 | test: true, 23 | name: true, 24 | friends: { // A special object for the friends set 25 | get: { // rules for how to traverse edge 26 | all: true, 27 | }, // A model of the data to return 28 | node: { 29 | id: true, 30 | test: true, 31 | name: true 32 | } 33 | } 34 | } 35 | } 36 | ``` 37 | This query would start at the root node with `id === 'nodeidtest'`, getting the 38 | `id`, `test`, and `name` fields. Then it would traverse the `friends` edge, 39 | finding related nodes according the rules set by the `edge.get` hash. For each 40 | found node, it would return the fields defined in `edge.node` if this node 41 | include edges, it will continue making queries until it has found everything 42 | requested. 43 | 44 | And example response might look like: 45 | ```js 46 | { 47 | nodeidtest: { 48 | id: "nodeidtest", 49 | test: "nodeidtest.test", 50 | name: "nodeidtest.name", 51 | friends: [ 52 | { 53 | id: "friend1", 54 | test: "friend1.test", 55 | name: "friend1.name" 56 | }, { 57 | id: "friend2", 58 | test: "friend2.test", 59 | name: "friend2.name" 60 | } 61 | ] 62 | } 63 | } 64 | ``` 65 | 66 | Here's the Mongo documents which would generate that response form that query: 67 | 68 | ####NodeIdTest 69 | ```js 70 | { 71 | id: "nodeidtest", 72 | test: "nodeidtest.test", 73 | name: "nodeidtest.name", 74 | friends: [ 75 | { 76 | cursor: 1, 77 | node: { 78 | id: "friend1" 79 | } 80 | }, 81 | { 82 | cursor: 2, 83 | node: { 84 | id: "friend2" 85 | } 86 | } 87 | ] 88 | } 89 | ``` 90 | ####Friend1 91 | ```js 92 | { 93 | id: "friend1", 94 | test: "friend1.test", 95 | name: "friend1.name", 96 | friends: [ 97 | { 98 | cursor: 1, 99 | node: { 100 | id: "nodeidtest" 101 | } 102 | } 103 | ] 104 | } 105 | ``` 106 | ####Friend2 107 | ```js 108 | { 109 | id: "friend2", 110 | test: "friend2.test", 111 | name: "friend2.name", 112 | friends: [ 113 | { 114 | cursor: 1, 115 | node: { 116 | id: "nodeidtest" 117 | } 118 | } 119 | ] 120 | }, 121 | ``` 122 | 123 | -------------------------------------------------------------------------------- /__tests__/modules/queryEngine_spec.js: -------------------------------------------------------------------------------- 1 | describe("The Query Engine", function() { 2 | var queryEngine = require.requireActual('../../source/js/modules/queryEngine'), 3 | fixtures = require.requireActual('../testQueries'), 4 | testId = "nodeidtest", 5 | testQuery = fixtures.testQuery, 6 | testDb = fixtures.NodeDB; 7 | 8 | it("recognizes an edge", function() { 9 | var positive = { 10 | get: { 11 | first: 10 12 | }, 13 | node: { 14 | id: true, 15 | friends: true 16 | } 17 | }, 18 | negative = { 19 | num_friends: true, 20 | last_added: true 21 | }; 22 | expect(queryEngine.isEdge(positive)).toEqual(true); 23 | expect(queryEngine.isEdge(negative)).toEqual(false); 24 | }); 25 | 26 | it("queries the database for the root NodeIDs in query", function() { 27 | queryEngine(testQuery, function(obj) { 28 | expect(obj[testId].test).toEqual("nodeidtest.test"); 29 | }); 30 | }); 31 | 32 | it("maps node queries onto a set of connections", function() { 33 | console.log(testQuery); 34 | queryEngine(testQuery, function(obj) { 35 | console.log(JSON.stringify(obj).split(',').join('\n')); 36 | expect(obj[testId].friends.length).toEqual(1); 37 | expect(obj[testId].friends[0].test).toEqual("friend1.test"); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /__tests__/testQueries.js: -------------------------------------------------------------------------------- 1 | var fixtures = { 2 | testQuery: { 3 | nodeidtest: { 4 | id: true, 5 | test: true, 6 | name: true, 7 | friends: { 8 | get: { 9 | first: 1, 10 | }, 11 | node: { 12 | id: true, 13 | test: true, 14 | name: true 15 | } 16 | } 17 | } 18 | }, 19 | NodeDB: { 20 | nodeidtest: { 21 | id: "nodeidtest", 22 | test: "nodeidtest.test", 23 | name: "nodeidtest.name", 24 | friends: [ 25 | { 26 | cursor: 1, 27 | node: { 28 | id: "friend1" 29 | } 30 | }, 31 | { 32 | cursor: 2, 33 | node: { 34 | id: "friend2" 35 | } 36 | } 37 | ] 38 | }, 39 | friend1: { 40 | id: "friend1", 41 | test: "friend1.test", 42 | name: "friend1.name", 43 | friends: [ 44 | { 45 | cursor: 1, 46 | node: { 47 | id: "nodeidtest" 48 | } 49 | }, 50 | { 51 | cursor: 2, 52 | node: { 53 | id: "friend2" 54 | } 55 | } 56 | ] 57 | }, 58 | friend2: { 59 | id: "friend2", 60 | test: "friend2.test", 61 | name: "friend2.name", 62 | friends: [ 63 | { 64 | cursor: 1, 65 | node: { 66 | id: "nodeidtest" 67 | } 68 | }, 69 | { 70 | cursor: 2, 71 | node: { 72 | id: "friend1" 73 | } 74 | } 75 | ] 76 | }, 77 | } 78 | }; 79 | 80 | 81 | 82 | module.exports = fixtures; 83 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | #GraphNoQL Schema 2 | 3 | Goal: Simply and poweruflly query a directed graph structure in MongoDB. 4 | Surface that database via a Relay-able API. 5 | 6 | ### Nodes JSON 7 | ```sh 8 | { 9 | _id: NodeID, 10 | attributes: mixed, 11 | edge_type: Array 12 | } 13 | ``` 14 | 15 | Nodes hold most of the data that we use in our app. Any piece of data could be 16 | represented as a node, from a User to a Video to a Server event. 17 | 18 | 19 | ### Connection JSON 20 | ```sh 21 | { 22 | cursor: CursorID, 23 | node: { 24 | id: NodeID, 25 | attributes: mixed 26 | } 27 | } 28 | ``` 29 | 30 | 31 | Connections are small objects that point at a node. They can carry any amount of 32 | data, but only require an id:NodeID field. The CursorID is used to identify a 33 | connection within a list. 34 | 35 | ### Query JSON 36 | ```sh 37 | { 38 | NodeID: { 39 | attributes: true, 40 | edge: { 41 | get: { //query options 42 | options: true 43 | }, 44 | node: { //object model 45 | cursor: true, 46 | node: { 47 | attributes: true 48 | } 49 | } 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | #####NodeID 56 | A uniq node identifier 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GraphNoQL", 3 | "version": "1.0.0", 4 | "description": "Facebook's GraphQL concept implemented in Node+Mongo", 5 | "main": "server.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/lutherism/GraphNoQL.git" 15 | }, 16 | "keywords": [ 17 | "GraphQL" 18 | ], 19 | "author": "lutherism", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/lutherism/GraphNoQL/issues" 23 | }, 24 | "homepage": "https://github.com/lutherism/GraphNoQL", 25 | "devDependencies": { 26 | "jest-cli": "^0.2.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /source/js/dao/baseDAO.js: -------------------------------------------------------------------------------- 1 | var BaseDAO, 2 | extend = require('extend'); 3 | 4 | BaseDAO = extend.call({}, { 5 | 6 | }); 7 | 8 | module.exports = BaseDAO; 9 | -------------------------------------------------------------------------------- /source/js/dao/graphNode.js: -------------------------------------------------------------------------------- 1 | var GraphNode, 2 | BaseDAO = require('dao/baseDAO'); 3 | 4 | GraphNode = BaseDAO.extend({ 5 | 6 | }); 7 | modules.exports = GraphNode; 8 | -------------------------------------------------------------------------------- /source/js/modules/__mocks__/db.js: -------------------------------------------------------------------------------- 1 | var testDb = require.requireActual('../../../../__tests__/testQueries').NodeDB; 2 | 3 | module.exports = { 4 | get: function(query, callback) { 5 | callback(testDb[query.id]); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /source/js/modules/db.js: -------------------------------------------------------------------------------- 1 | modules.exports = { 2 | get: function() { 3 | 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /source/js/modules/queryEngine.js: -------------------------------------------------------------------------------- 1 | var queryEngine, 2 | db = require('./db'); 3 | 4 | function isEdge(value) { //duck type edge objects 5 | if (value && 6 | typeof value === 'object') { 7 | var length = 0; 8 | for (var i in value) { 9 | length+=1; 10 | } 11 | return !!( 12 | length === 2 && 13 | typeof value.get === 'object' && 14 | typeof value.node === 'object' 15 | ); 16 | } else { 17 | return false; 18 | } 19 | 20 | } 21 | 22 | function map(obj, fn) { 23 | var newObj = {}; 24 | for(var i in obj) { 25 | newObj[i] = fn(obj[i], i, obj); 26 | } 27 | return newObj; 28 | } 29 | 30 | function filter(obj, filter) { 31 | var ret = {}; 32 | map(filter, function(v, k) { 33 | if (v === true) ret[k] = obj[k]; 34 | }); 35 | return ret; 36 | } 37 | 38 | function reduce(obj, fn, a) { 39 | map(obj, function(v, k) { 40 | a = fn(a, v, k); 41 | }); 42 | } 43 | 44 | function hasAll(obj, compare) { 45 | var ret = true; 46 | for (var i in compare) { 47 | ret = ret && Object.prototype.hasOwnProperty.call(obj, i); 48 | } 49 | return ret; 50 | } 51 | 52 | 53 | /* 54 | * Fetches the given NodeID, and recursively fetches any subsequent edges in node 55 | * Once done building tree, calls callback with tree as arg 56 | */ 57 | function queryNode(id, node, callback) { 58 | var edges = {}, 59 | attributes = {}, 60 | hasEdges = false; 61 | map(node, function(v, k) { 62 | if (isEdge(v)) { 63 | edges[k] = v; 64 | hasEdges = true; 65 | } else { 66 | attributes[k] = v; 67 | } 68 | }); 69 | 70 | db.get({id: id}, function(model) { 71 | var ret = filter(model, attributes); 72 | if (hasEdges) { 73 | map(edges, function(v, k) { 74 | 75 | traverseEdges(model[k], v, function(connections) { 76 | ret[k] = connections; 77 | 78 | if (hasAll(ret, edges)) { 79 | callback(ret); 80 | } 81 | }); 82 | }); 83 | } else { 84 | callback(ret); 85 | } 86 | }); 87 | } 88 | 89 | function traverseEdges(edge, query, callback) { 90 | var options = query.get, 91 | node = query.node, 92 | connections = [], 93 | after = options.after || 0, 94 | first = options.first || edge.length; 95 | 96 | edge = edge.slice(after, first); 97 | 98 | edge.map(function(connection, i) { 99 | queryNode(connection.node.id, node, function(model) { 100 | connections[i] = model; 101 | if (hasAll(connections, edge)) { 102 | callback(connections); 103 | } 104 | }); 105 | }); 106 | }; 107 | //take in schema query obejcts, spit out mongo queries 108 | 109 | queryEngine = function queryEngine(queries, callback) { 110 | var results = {}; 111 | map(queries, function(node, id) { 112 | queryNode(id, node, function(result) { 113 | results[id] = result; 114 | if (hasAll(queries, results)) { 115 | callback(results); 116 | } 117 | }) 118 | }); 119 | }; 120 | 121 | queryEngine.isEdge = isEdge; 122 | 123 | module.exports = queryEngine; 124 | -------------------------------------------------------------------------------- /source/js/server.js: -------------------------------------------------------------------------------- 1 | var server = require('http'), 2 | url = require('url'), 3 | mongoose = require('mongoose'), 4 | queryEngine = require('modules/queryEngine'); 5 | 6 | mongoose.connect('mongodb://localhost:27017'); 7 | console.log('connecting to DB...'); 8 | mongoose.connection.on('open', function() { 9 | console.log('connected to DB'); 10 | var NodeSchema = mongoose.schema(nodeschema), 11 | Node = mongoose.Model(NodeSchema); 12 | http.createServer(function(req, resp) { 13 | if (req.method === 'GET') { 14 | queryEngine( 15 | JSON.parse(url.parse(req.url, true).query.q), 16 | function(err, tree) { 17 | resp.writeHead(200, { 18 | 'Content-Type': 'application/json', 19 | 'Allow-Access-Origin': '*' 20 | }); 21 | resp.write(JSON.stringify(tree)); 22 | } 23 | ); 24 | } else if (req.method === 'POST') { 25 | req.read(function(data) { 26 | Node.update(data.id, data, function (err, models) { 27 | if (err) { 28 | resp.writeHead(500); 29 | resp.write(err); 30 | } else { 31 | resp.writeHead(200, { 32 | 'Content-Type': 'application/json', 33 | 'Allow-Access-Origin': '*' 34 | }); 35 | resp.write(JSON.stringify(models[0])); 36 | } 37 | resp.close(); 38 | }); 39 | }); 40 | } 41 | }).listen(8008); 42 | --------------------------------------------------------------------------------