├── .gitignore ├── .travis.yml ├── LICENSE ├── index.js ├── package.json ├── readme.markdown ├── reduce.js └── test ├── fixtures.js └── queries.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '5.5.0' 5 | script: npm t 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, James Kyburz (the "Original Author") 2 | All rights reserved. 3 | 4 | MIT +no-false-attribs License 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the "Software"), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | Distributions of all or part of the Software intended to be used 19 | by the recipients as they would use the unmodified Software, 20 | containing modifications that substantially alter, remove, or 21 | disable functionality of the Software, outside of the documented 22 | configuration mechanisms provided by the Software, shall be 23 | modified such that the Original Author's bug reporting email 24 | addresses and urls are either replaced with the contact information 25 | of the parties responsible for the changes, or removed entirely. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 34 | OTHER DEALINGS IN THE SOFTWARE. 35 | 36 | 37 | Except where noted, this license applies to any and all software 38 | programs and associated documentation files created by the 39 | Original Author, when distributed with the Software. 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var parser = require('graphql-parser') 2 | var reduce = require('./reduce') 3 | 4 | module.exports = parse 5 | 6 | function parse (query, cb) { 7 | var json = parser.parse(`{ ${query} }`).fields[0] 8 | var errorOccured 9 | 10 | var error = (err) => { 11 | if (errorOccured) return 12 | errorOccured = true 13 | err = new Error(err) 14 | return cb(err) 15 | } 16 | 17 | var tokens = [] 18 | 19 | parseEntity(json, json, json.name) 20 | 21 | if (errorOccured) return 22 | 23 | function map (part) { 24 | return Object.keys(tokens) 25 | .map((x) => tokens[x][part]) 26 | .filter((Boolean)) 27 | } 28 | 29 | function join (part, delimiter) { 30 | return map(part).join(delimiter) 31 | } 32 | 33 | var fields = join('return', ', ') 34 | var match = join('match', ' ') 35 | 36 | if (!map('return').length) return error('no fields specified') 37 | 38 | var cql = `${match}\nreturn ${fields}` 39 | 40 | return cb(null, { 41 | cql: cql, 42 | reduce: reduce(tokens) 43 | }) 44 | 45 | function parseEntity (root, parent) { 46 | if (!root || !root.fields) return 47 | 48 | var name = root.name 49 | var alias = root.alias || name 50 | 51 | var match = null 52 | var parentName = null 53 | 54 | if (root !== parent) { 55 | var relationship = root.params.filter((x) => x.name === 'relationship')[0] 56 | if (!relationship) return error(`missing relationship parameter for ${name}`) 57 | var relationshipValue = relationship.value.value 58 | if (relationshipValue === '*') relationshipValue = '' 59 | relationshipValue = `__${alias}r${relationshipValue}` 60 | parentName = parent.alias || parent.name 61 | match = `optional match(${parentName})<-[${relationshipValue}]->(${alias}:${name})` 62 | } else { 63 | match = `match(${alias}:${name})` 64 | } 65 | 66 | var entity = { 67 | meta: '', 68 | name: name, 69 | alias: alias, 70 | match: match, 71 | matchFragment: match, 72 | matchParameters: [], 73 | properties: [], 74 | parent: parentName 75 | } 76 | 77 | if (tokens.filter((x) => x.alias === alias).length) return error(`duplicate ${alias} please use as to alias`) 78 | tokens.push(entity) 79 | 80 | ;(root.params).forEach((item) => { 81 | var key = item.name 82 | if (key === 'relationship') return 83 | entity.matchParameters.push(`${key}: ${param(item.value)}`) 84 | entity.match = `${entity.matchFragment.slice(0, -1)} {${entity.matchParameters.join(', ')}})` 85 | }) 86 | 87 | ;(root.fields).forEach(parseField) 88 | 89 | function parseProperties (properties) { 90 | properties.fields.forEach(parseField) 91 | } 92 | 93 | function parseField (item) { 94 | if (item.name === 'properties') return parseProperties(item) 95 | if (item.name === 'labels') entity.meta += '|labels' 96 | if (item.name === 'relationships') entity.meta += '|relationships' 97 | if (item.name === 'graph') entity.meta += '|graph' 98 | if (item.fields.length) { 99 | parseEntity(item, root) 100 | } else { 101 | entity.properties.push(`${alias}.${item.name}`) 102 | var relationShip = entity.parent ? `, __${alias}r` : '' 103 | entity.return = `id(${alias}) as __${alias}id, ${entity.properties.join(', ')}${relationShip}` 104 | } 105 | } 106 | } 107 | } 108 | 109 | function param (item) { 110 | var value = item.type === 'Literal' ? item.value : `{${item.name}}` 111 | return typeof value === 'string' && value[0] !== '{' ? `'${value}'` : value 112 | } 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql2cypher", 3 | "description": "simple graphql to cypher", 4 | "keywords": "graphql cypherquery", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/jameskyburz/graphql2cypher.git" 8 | }, 9 | "author": { 10 | "name": "James Kyburz", 11 | "email": "james.kyburz@gmail.com" 12 | }, 13 | "main": "index.js", 14 | "version": "3.0.6", 15 | "scripts": { 16 | "test": "tape test/* | faucet", 17 | "prepublish": "standard", 18 | "autotest": "watch 'npm t' ." 19 | }, 20 | "dependencies": { 21 | "graphql-parser": "2.1.0" 22 | }, 23 | "devDependencies": { 24 | "faucet": "0.0.1", 25 | "standard": "10.0.3", 26 | "tape": "4.8.0", 27 | "watch": "1.0.2" 28 | }, 29 | "engines": { 30 | "node": ">= 4.0.0" 31 | }, 32 | "license": "MIT" 33 | } 34 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # This repository is no longer maintained. 2 | 3 | # graphql2cypher 4 | 5 | Naive parser from graphql to cypher query. 6 | 7 | [![js-standard-style](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/feross/standard) 8 | [![build status](https://api.travis-ci.org/JamesKyburz/graphql2cypher.svg)](https://travis-ci.org/JamesKyburz/graphql2cypher) 9 | [![Greenkeeper badge](https://badges.greenkeeper.io/JamesKyburz/graphql2cypher.svg)](https://greenkeeper.io/) 10 | 11 | main.js: 12 | 13 | ```javascript 14 | tape('user entity with address', (t) => { 15 | t.plan(2) 16 | parse(` 17 | user(id: ) { 18 | properties { 19 | name, 20 | address(edge: ":address", addressId: ) { 21 | properties { 22 | line 23 | } 24 | } 25 | } 26 | } 27 | `, (err, r) => { 28 | t.error(err) 29 | t.equals(r.cql, expected) 30 | }) 31 | var expected = ` 32 | match(user:user {id: {id}}) optional match(user)<-[:address]->(address:address {addressId: {addressId}}) 33 | return * 34 | ` 35 | }) 36 | ``` 37 | 38 | To see how reduce works to see how it looks check out [fixtures](https://github.com/JamesKyburz/graphql2cypher/blob/master/test/fixtures.js), 39 | It works together with the module [cypherquery](https://github.com/JamesKyburz/cypherquery) 40 | 41 | # relationships, labels and graph 42 | 43 | This requires that the statement sent to cypher has `resultType` `['row', 'graph']`] 44 | 45 | # relationships 46 | 47 | You can ask for relationships in graphql 48 | 49 | ```javascript 50 | ` 51 | user(id: ) { 52 | relationships, 53 | ... 54 | ` 55 | ``` 56 | 57 | # labels 58 | 59 | You can ask for labels in graphql 60 | 61 | ```javascript 62 | ` 63 | user(id: ) { 64 | labels 65 | ... 66 | ` 67 | ``` 68 | 69 | # graph 70 | You can ask for the raw graph as returned by cypher 71 | 72 | ```javascript 73 | ` 74 | user(id: ) { 75 | graph 76 | ... 77 | ` 78 | ``` 79 | 80 | # install 81 | 82 | With [npm](https://npmjs.org) do: 83 | 84 | ``` 85 | npm install graphql2cypher 86 | ``` 87 | 88 | # test 89 | 90 | ``` 91 | npm test 92 | ``` 93 | 94 | # license 95 | 96 | MIT 97 | -------------------------------------------------------------------------------- /reduce.js: -------------------------------------------------------------------------------- 1 | module.exports = reduce 2 | 3 | function reduce (tokens) { 4 | var entityPointer = {} 5 | var nulls = 0 6 | 7 | return (results) => { 8 | var result = {} 9 | if (!results.results.length) return results 10 | results = results.results[0] 11 | var columns = results.columns 12 | var rows = results.data.map((x) => x.row) 13 | var graphs = results.data.map((x) => x.graph) 14 | var added = {} 15 | var graph = graphs.reduce((sum, item) => { 16 | item = item || { nodes: [], relationships: [] } 17 | item.nodes.forEach((node) => { 18 | sum[node.id] = { 19 | labels: node.labels, 20 | relationships: [] 21 | } 22 | }) 23 | return sum 24 | }, {}) 25 | 26 | graphs.forEach((item) => { 27 | if (!item || !item.relationships) return 28 | item.relationships.forEach((relationship) => { 29 | var start = graph[relationship.startNode] 30 | var end = graph[relationship.endNode] 31 | add(start) 32 | add(end) 33 | function add (type) { 34 | if (type && !type.relationships.filter((x) => x.id === relationship.id).length) type.relationships.push(relationship) 35 | } 36 | }) 37 | }) 38 | 39 | var nodes = tokens.reduce((sum, token) => { 40 | sum[token.alias] = token 41 | token.results = [] 42 | return sum 43 | }, {}) 44 | 45 | for (var i = 0; i < rows.length; i++) { 46 | var row = rows[i] 47 | var value 48 | for (var j = 0; j < row.length; j++) { 49 | var column = columns[j] 50 | if (/__.*id$/.test(column)) { 51 | var entity = column.slice(2, -2) 52 | if (!row[j]) row[j] = nulls++ 53 | var id = row[j] 54 | var node = nodes[entity] 55 | if (!added[id] && !node.parent) { 56 | added = {} 57 | } 58 | if (!added[id]) { 59 | var data = { 60 | properties: {} 61 | } 62 | if (node.parent) { 63 | entityPointer[node.parent][entity] = entityPointer[node.parent][entity] || [] 64 | entityPointer[node.parent][entity].push(data) 65 | entityPointer[entity] = data 66 | } else { 67 | result[entity] = result[entity] || [] 68 | result[entity].push(data) 69 | entityPointer[entity] = data 70 | } 71 | for (var k = 1; k < row.length; k++) { 72 | var key = columns[k] 73 | var part = key.split('.') 74 | if (part[0] !== entity) continue 75 | value = row[k] 76 | if (value != null) data.properties[part[1]] = value 77 | } 78 | var meta = graph[id] 79 | if (/labels|graph/.test(node.meta) && meta) { 80 | data.labels = meta.labels 81 | } 82 | if (/relationships|graph/.test(node.meta) && meta) { 83 | data.id = id 84 | data.relationships = meta.relationships 85 | } 86 | if (/graph/.test(node.meta) && meta) { 87 | result[tokens[0].alias][0].graphs = graphs 88 | data.id = id 89 | } 90 | if (!Object.keys(data.properties).length && node.parent) { 91 | entityPointer[node.parent][entity].pop() 92 | } 93 | } 94 | added[row[j]] = true 95 | } 96 | } 97 | } 98 | return result[tokens[0].alias] 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/fixtures.js: -------------------------------------------------------------------------------- 1 | exports.peterNoLabels = 2 | [{ 3 | 'properties': { 4 | 'name': 'Peter' 5 | }, 6 | 'beer': [ 7 | { 8 | 'properties': { 9 | 'name': 'IPA XX' 10 | }, 11 | 'awards': [ 12 | { 13 | 'properties': { 14 | 'name': 'Best beer 2014' 15 | } 16 | }, 17 | { 18 | 'properties': { 19 | 'name': 'Best beer 2015' 20 | } 21 | } 22 | ] 23 | } 24 | ] 25 | }] 26 | 27 | exports.peterAndPaulNoLabels = 28 | [{ 29 | 'properties': { 30 | 'name': 'Peter' 31 | }, 32 | 'beer': [ 33 | { 34 | 'properties': { 35 | 'name': 'IPA XX' 36 | } 37 | } 38 | ] 39 | }, 40 | { 41 | 'properties': { 42 | 'name': 'Paul' 43 | }, 44 | 'beer': [ 45 | { 46 | 'properties': { 47 | 'name': 'IPA XX' 48 | } 49 | } 50 | ] 51 | }] 52 | 53 | exports.peterLabelsAndRelationships = 54 | [{ 55 | 'properties': { 56 | 'name': 'Peter' 57 | }, 58 | 'labels': [ 59 | 'person' 60 | ], 61 | 'id': 3757, 62 | 'relationships': [ 63 | { 64 | 'id': '28', 65 | 'type': 'likes', 66 | 'startNode': '3757', 67 | 'endNode': '3758', 68 | 'properties': {} 69 | } 70 | ], 71 | 'beer': [ 72 | { 73 | 'properties': { 74 | 'name': 'IPA XX' 75 | }, 76 | 'labels': [ 77 | 'beer', 78 | 'beverage' 79 | ], 80 | 'id': 3758, 81 | 'relationships': [ 82 | { 83 | 'id': '28', 84 | 'type': 'likes', 85 | 'startNode': '3757', 86 | 'endNode': '3758', 87 | 'properties': {} 88 | }, 89 | { 90 | 'id': '30', 91 | 'type': 'award', 92 | 'startNode': '3759', 93 | 'endNode': '3758', 94 | 'properties': {} 95 | }, 96 | { 97 | 'id': '31', 98 | 'type': 'award', 99 | 'startNode': '3760', 100 | 'endNode': '3758', 101 | 'properties': {} 102 | } 103 | ], 104 | 'awards': [ 105 | { 106 | 'properties': { 107 | 'name': 'Best beer 2014' 108 | }, 109 | 'labels': [ 110 | 'award' 111 | ], 112 | 'id': 3759, 113 | 'relationships': [ 114 | { 115 | 'id': '30', 116 | 'type': 'award', 117 | 'startNode': '3759', 118 | 'endNode': '3758', 119 | 'properties': {} 120 | } 121 | ] 122 | }, 123 | { 124 | 'properties': { 125 | 'name': 'Best beer 2015' 126 | }, 127 | 'labels': [ 128 | 'award' 129 | ], 130 | 'id': 3760, 131 | 'relationships': [ 132 | { 133 | 'id': '31', 134 | 'type': 'award', 135 | 'startNode': '3760', 136 | 'endNode': '3758', 137 | 'properties': {} 138 | } 139 | ] 140 | } 141 | ] 142 | } 143 | ] 144 | }] 145 | 146 | exports.peterLabelsOnly = 147 | [{ 148 | 'labels': ['person'], 149 | 'properties': { 150 | 'name': 'Peter' 151 | }, 152 | 'beer': [ 153 | { 154 | 'labels': ['beer', 'beverage'], 155 | 'properties': { 156 | 'name': 'IPA XX' 157 | }, 158 | 'awards': [ 159 | { 160 | 'labels': ['award'], 161 | 'properties': { 162 | 'name': 'Best beer 2014' 163 | } 164 | }, 165 | { 166 | 'labels': ['award'], 167 | 'properties': { 168 | 'name': 'Best beer 2015' 169 | } 170 | } 171 | ] 172 | } 173 | ] 174 | }] 175 | 176 | exports.peterGraph = 177 | [{ 178 | 'properties': { 179 | 'name': 'Peter' 180 | }, 181 | 'labels': [ 182 | 'person' 183 | ], 184 | 'id': 3757, 185 | 'relationships': [ 186 | { 187 | 'id': '28', 188 | 'type': 'likes', 189 | 'startNode': '3757', 190 | 'endNode': '3758', 191 | 'properties': {} 192 | } 193 | ], 194 | 'graphs': [ 195 | { 196 | 'nodes': [ 197 | { 198 | 'id': '3757', 199 | 'labels': [ 200 | 'person' 201 | ], 202 | 'properties': { 203 | 'name': 'Peter' 204 | } 205 | }, 206 | { 207 | 'id': '3758', 208 | 'labels': [ 209 | 'beer', 210 | 'beverage' 211 | ], 212 | 'properties': { 213 | 'name': 'IPA XX' 214 | } 215 | }, 216 | { 217 | 'id': '3759', 218 | 'labels': [ 219 | 'award' 220 | ], 221 | 'properties': { 222 | 'name': 'Best beer 2014' 223 | } 224 | } 225 | ], 226 | 'relationships': [ 227 | { 228 | 'id': '28', 229 | 'type': 'likes', 230 | 'startNode': '3757', 231 | 'endNode': '3758', 232 | 'properties': {} 233 | }, 234 | { 235 | 'id': '30', 236 | 'type': 'award', 237 | 'startNode': '3759', 238 | 'endNode': '3758', 239 | 'properties': {} 240 | } 241 | ] 242 | }, 243 | { 244 | 'nodes': [ 245 | { 246 | 'id': '3760', 247 | 'labels': [ 248 | 'award' 249 | ], 250 | 'properties': { 251 | 'name': 'Best beer 2015' 252 | } 253 | }, 254 | { 255 | 'id': '3757', 256 | 'labels': [ 257 | 'person' 258 | ], 259 | 'properties': { 260 | 'name': 'Peter' 261 | } 262 | }, 263 | { 264 | 'id': '3758', 265 | 'labels': [ 266 | 'beer', 267 | 'beverage' 268 | ], 269 | 'properties': { 270 | 'name': 'IPA XX' 271 | } 272 | } 273 | ], 274 | 'relationships': [ 275 | { 276 | 'id': '28', 277 | 'type': 'likes', 278 | 'startNode': '3757', 279 | 'endNode': '3758', 280 | 'properties': {} 281 | }, 282 | { 283 | 'id': '31', 284 | 'type': 'award', 285 | 'startNode': '3760', 286 | 'endNode': '3758', 287 | 'properties': {} 288 | } 289 | ] 290 | } 291 | ], 292 | 'beer': [ 293 | { 294 | 'properties': { 295 | 'name': 'IPA XX' 296 | }, 297 | 'labels': [ 298 | 'beer', 299 | 'beverage' 300 | ], 301 | 'id': 3758, 302 | 'relationships': [ 303 | { 304 | 'id': '28', 305 | 'type': 'likes', 306 | 'startNode': '3757', 307 | 'endNode': '3758', 308 | 'properties': {} 309 | }, 310 | { 311 | 'id': '30', 312 | 'type': 'award', 313 | 'startNode': '3759', 314 | 'endNode': '3758', 315 | 'properties': {} 316 | }, 317 | { 318 | 'id': '31', 319 | 'type': 'award', 320 | 'startNode': '3760', 321 | 'endNode': '3758', 322 | 'properties': {} 323 | } 324 | ], 325 | 'awards': [ 326 | { 327 | 'properties': { 328 | 'name': 'Best beer 2014' 329 | }, 330 | 'labels': [ 331 | 'award' 332 | ], 333 | 'id': 3759, 334 | 'relationships': [ 335 | { 336 | 'id': '30', 337 | 'type': 'award', 338 | 'startNode': '3759', 339 | 'endNode': '3758', 340 | 'properties': {} 341 | } 342 | ] 343 | }, 344 | { 345 | 'properties': { 346 | 'name': 'Best beer 2015' 347 | }, 348 | 'labels': [ 349 | 'award' 350 | ], 351 | 'id': 3760, 352 | 'relationships': [ 353 | { 354 | 'id': '31', 355 | 'type': 'award', 356 | 'startNode': '3760', 357 | 'endNode': '3758', 358 | 'properties': {} 359 | } 360 | ] 361 | } 362 | ] 363 | } 364 | ] 365 | }] 366 | 367 | exports.peterOK = 368 | { 369 | 'results': [ 370 | { 371 | 'columns': [ 372 | '__pid', 373 | 'p.name', 374 | '__beerid', 375 | 'beer.name', 376 | '__beerr', 377 | '__awardsid', 378 | 'awards.name', 379 | '__awardsr' 380 | ], 381 | 'data': [ 382 | { 383 | 'row': [ 384 | 3757, 385 | 'Peter', 386 | 3758, 387 | 'IPA XX', 388 | {}, 389 | 3759, 390 | 'Best beer 2014', 391 | {} 392 | ], 393 | 'graph': { 394 | 'nodes': [ 395 | { 396 | 'id': '3757', 397 | 'labels': [ 398 | 'person' 399 | ], 400 | 'properties': { 401 | 'name': 'Peter' 402 | } 403 | }, 404 | { 405 | 'id': '3758', 406 | 'labels': [ 407 | 'beer', 408 | 'beverage' 409 | ], 410 | 'properties': { 411 | 'name': 'IPA XX' 412 | } 413 | }, 414 | { 415 | 'id': '3759', 416 | 'labels': [ 417 | 'award' 418 | ], 419 | 'properties': { 420 | 'name': 'Best beer 2014' 421 | } 422 | } 423 | ], 424 | 'relationships': [ 425 | { 426 | 'id': '28', 427 | 'type': 'likes', 428 | 'startNode': '3757', 429 | 'endNode': '3758', 430 | 'properties': {} 431 | }, 432 | { 433 | 'id': '30', 434 | 'type': 'award', 435 | 'startNode': '3759', 436 | 'endNode': '3758', 437 | 'properties': {} 438 | } 439 | ] 440 | } 441 | }, 442 | { 443 | 'row': [ 444 | 3757, 445 | 'Peter', 446 | 3758, 447 | 'IPA XX', 448 | {}, 449 | 3760, 450 | 'Best beer 2015', 451 | {} 452 | ], 453 | 'graph': { 454 | 'nodes': [ 455 | { 456 | 'id': '3760', 457 | 'labels': [ 458 | 'award' 459 | ], 460 | 'properties': { 461 | 'name': 'Best beer 2015' 462 | } 463 | }, 464 | { 465 | 'id': '3757', 466 | 'labels': [ 467 | 'person' 468 | ], 469 | 'properties': { 470 | 'name': 'Peter' 471 | } 472 | }, 473 | { 474 | 'id': '3758', 475 | 'labels': [ 476 | 'beer', 477 | 'beverage' 478 | ], 479 | 'properties': { 480 | 'name': 'IPA XX' 481 | } 482 | } 483 | ], 484 | 'relationships': [ 485 | { 486 | 'id': '28', 487 | 'type': 'likes', 488 | 'startNode': '3757', 489 | 'endNode': '3758', 490 | 'properties': {} 491 | }, 492 | { 493 | 'id': '31', 494 | 'type': 'award', 495 | 'startNode': '3760', 496 | 'endNode': '3758', 497 | 'properties': {} 498 | } 499 | ] 500 | } 501 | } 502 | ] 503 | } 504 | ], 505 | 'errors': [] 506 | } 507 | 508 | exports.peterAndPaulOK = 509 | { 510 | 'results': [ 511 | { 512 | 'columns': [ 513 | '__pid', 514 | 'p.name', 515 | '__beerid', 516 | 'beer.name' 517 | ], 518 | 'data': [ 519 | { 520 | 'row': [ 521 | 3757, 522 | 'Peter', 523 | 3758, 524 | 'IPA XX' 525 | ] 526 | }, 527 | { 528 | 'row': [ 529 | 4757, 530 | 'Paul', 531 | 3758, 532 | 'IPA XX' 533 | ] 534 | } 535 | ] 536 | } 537 | ], 538 | 'errors': [] 539 | } 540 | -------------------------------------------------------------------------------- /test/queries.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var fixtures = require('./fixtures') 3 | 4 | var parse = require('../') 5 | 6 | test('test entity with 1 field', (t) => { 7 | t.plan(2) 8 | var expected = trim` 9 | match(user:user {id: {id}}) 10 | return id(user) as __userid, user.name 11 | ` 12 | 13 | parse(` 14 | user(id: ) { 15 | properties { 16 | name 17 | } 18 | } 19 | `, (err, r) => { 20 | t.error(err) 21 | t.equals(r.cql, expected) 22 | }) 23 | }) 24 | 25 | test('user entity with address', (t) => { 26 | t.plan(2) 27 | var expected = trim` 28 | match(user:user {id: {id}}) optional match(user)<-[__addressr:address]->(address:address {addressId: {addressId}}) 29 | return id(user) as __userid, user.name, id(address) as __addressid, address.line, __addressr 30 | ` 31 | parse(` 32 | user(id: ) { 33 | properties { 34 | name, 35 | address(relationship: ":address", addressId: ) { 36 | properties { 37 | line 38 | } 39 | } 40 | } 41 | } 42 | `, (err, r) => { 43 | t.error(err) 44 | t.equals(r.cql, expected) 45 | }) 46 | }) 47 | 48 | test('deep query', (t) => { 49 | t.plan(2) 50 | var expected = trim` 51 | match(p:person {id: {id}}) optional match(p)<-[__fr:friend]->(f:friend) optional match(f)<-[__foffr:friend]->(foff:friend) optional match(foff)<-[__foffoffr:friend]->(foffoff:friend) 52 | return id(p) as __pid, p.name, id(f) as __fid, f.name, __fr, id(foff) as __foffid, foff.name, __foffr, id(foffoff) as __foffoffid, foffoff.name, __foffoffr 53 | ` 54 | parse(` 55 | person(id: ) as p { 56 | properties { 57 | name, 58 | friend(relationship: ":friend") as f { 59 | properties { 60 | name, 61 | friend(relationship: ":friend") as foff{ 62 | properties { 63 | name, 64 | friend(relationship: ":friend") as foffoff{ 65 | name 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | `, (err, r) => { 74 | t.error(err) 75 | t.equals(r.cql, expected) 76 | }) 77 | }) 78 | 79 | test('root edges', (t) => { 80 | t.plan(2) 81 | var expected = trim` 82 | match(r:root) optional match(r)<-[__c1r:child]->(c1:child) optional match(c1)<-[__c1c1r:child]->(c1c1:child) optional match(r)<-[__c2r:child]->(c2:child) 83 | return id(r) as __rid, r.name, id(c1) as __c1id, c1.name, __c1r, id(c1c1) as __c1c1id, c1c1.name, __c1c1r, id(c2) as __c2id, c2.name, __c2r 84 | ` 85 | parse(` 86 | root() as r { 87 | properties { 88 | name, 89 | child(relationship: ":child") as c1 { 90 | properties { 91 | name, 92 | child(relationship: ":child") as c1c1 { 93 | properties { 94 | name 95 | } 96 | } 97 | } 98 | }, 99 | child(relationship: ":child") as c2 { 100 | properties { 101 | name 102 | } 103 | } 104 | } 105 | } 106 | `, (err, r) => { 107 | t.error(err) 108 | t.equals(r.cql, expected) 109 | }) 110 | }) 111 | 112 | test('fields must be specified', (t) => { 113 | t.plan(1) 114 | parse(` 115 | root() {} 116 | `, (err) => { 117 | t.equals(err.message, 'no fields specified') 118 | }) 119 | }) 120 | 121 | test('relationship must be specified', (t) => { 122 | t.plan(1) 123 | parse(` 124 | root() { 125 | properties { 126 | child() { 127 | properties { 128 | x 129 | } 130 | } 131 | } 132 | } 133 | `, (err) => { 134 | t.equals(err.message, 'missing relationship parameter for child') 135 | }) 136 | }) 137 | 138 | test('cannot have duplicate names', (t) => { 139 | t.plan(1) 140 | parse(` 141 | root() { 142 | properties { 143 | root(relationship: "x") { 144 | properties { 145 | x 146 | } 147 | } 148 | } 149 | } 150 | `, (x) => { 151 | t.equals(x.message, 'duplicate root please use as to alias') 152 | } 153 | ) 154 | }) 155 | 156 | test('multiple parameters', (t) => { 157 | t.plan(2) 158 | var expected = trim` 159 | match(root:root {id: {id}, name: {name}, prop: 42, prop2: \'42\'}) optional match(root)<-[__childrchild]->(child:child {childId: {childId}, name: {name}, prop: 42, prop2: \'42\'}) 160 | return id(root) as __rootid, root.field, id(child) as __childid, child.field, __childr 161 | ` 162 | parse(` 163 | root(id: , name: , prop: 42, prop2: "42") { 164 | properties { 165 | field, 166 | child(relationship: "child", childId: , name: , prop: 42, prop2: "42") { 167 | properties { 168 | field 169 | } 170 | } 171 | } 172 | } 173 | `, (err, r) => { 174 | t.error(err) 175 | t.equals(r.cql, expected) 176 | }) 177 | }) 178 | 179 | test('reduce peter with no labels or relationships', (t) => { 180 | t.plan(2) 181 | var results = fixtures.peterOK 182 | var expected = fixtures.peterNoLabels 183 | parse(` 184 | person() as p { 185 | properties { 186 | name, 187 | beer(relationship: ":likes") { 188 | properties { 189 | name, 190 | award(relationship: ":award") as awards { 191 | properties { 192 | name 193 | } 194 | } 195 | } 196 | } 197 | } 198 | }`, (err, r) => { 199 | t.error(err) 200 | t.deepEqual(r.reduce(results), expected) 201 | }) 202 | }) 203 | 204 | test('reduce peter with labels', (t) => { 205 | t.plan(2) 206 | var results = fixtures.peterOK 207 | var expected = fixtures.peterLabelsOnly 208 | parse(` 209 | person() as p { 210 | labels, 211 | properties { 212 | name, 213 | beer(relationship: ":likes") { 214 | labels, 215 | properties { 216 | name, 217 | award(relationship: ":award") as awards { 218 | labels, 219 | properties { 220 | name 221 | } 222 | } 223 | } 224 | } 225 | } 226 | }`, (err, r) => { 227 | t.error(err) 228 | t.deepEqual(r.reduce(results), expected) 229 | }) 230 | }) 231 | 232 | test('reduce peter with labels and relationships', (t) => { 233 | t.plan(2) 234 | var results = fixtures.peterOK 235 | var expected = fixtures.peterLabelsAndRelationships 236 | parse(` 237 | person() as p { 238 | labels, 239 | relationships, 240 | properties { 241 | name, 242 | beer(relationship: ":likes") { 243 | labels, 244 | relationships, 245 | properties { 246 | name, 247 | award(relationship: ":award") as awards { 248 | labels, 249 | relationships, 250 | properties { 251 | name 252 | } 253 | } 254 | } 255 | } 256 | } 257 | }`, (err, r) => { 258 | t.error(err) 259 | t.deepEqual(r.reduce(results), expected) 260 | }) 261 | }) 262 | 263 | test('reduce peter with graph', (t) => { 264 | t.plan(2) 265 | var results = fixtures.peterOK 266 | var expected = fixtures.peterGraph 267 | parse(` 268 | person() as p { 269 | graph, 270 | properties { 271 | name, 272 | beer(relationship: ":likes") { 273 | graph, 274 | properties { 275 | name, 276 | award(relationship: ":award") as awards { 277 | graph, 278 | properties { 279 | name 280 | } 281 | } 282 | } 283 | } 284 | } 285 | }`, (err, r) => { 286 | t.error(err) 287 | t.deepEqual(r.reduce(results), expected) 288 | }) 289 | }) 290 | 291 | test('two people liking same beer', (t) => { 292 | t.plan(2) 293 | var results = fixtures.peterAndPaulOK 294 | var expected = fixtures.peterAndPaulNoLabels 295 | parse(` 296 | person() as p { 297 | properties { 298 | name, 299 | beer(relationship: ":likes") { 300 | properties { 301 | name 302 | } 303 | } 304 | } 305 | }`, (err, r) => { 306 | t.error(err) 307 | t.deepEqual(r.reduce(results), expected) 308 | }) 309 | }) 310 | 311 | function trim (strings) { 312 | var cql = strings.join('') 313 | return cql.split(/\n/g).filter(Boolean).slice(0, -1).map((x) => x.replace(/^\s*/g, '')).join('\n') 314 | } 315 | --------------------------------------------------------------------------------