├── .brackets.json ├── .gitignore ├── LICENSE ├── README.md ├── gulpfile.js ├── lib ├── adapter.js ├── connection.js └── processor.js ├── package.json └── test ├── .gitignore ├── README.md ├── adapter.test.js ├── integration └── runner.js ├── models ├── pets_1.js ├── profiles_1.js ├── users_1.js ├── users_profiles_graph.js └── users_users_graph.js └── test-example.json /.brackets.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": { 3 | "javascript": { 4 | "linting.prefer": ["JSHint"], 5 | "linting.usePreferredOnly": true 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | /jsdocs/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Taneli Leppä 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 | ![image_squidhome@2x.png](http://i.imgur.com/RIvu9.png) 2 | 3 | # sails-arangodb 4 | 5 | Provides easy access to `ArangoDB` from Sails.js & Waterline. 6 | 7 | Take a look at 8 | sails-arangodb-demo for more up-to-date examples. 9 | 10 | This module is a Waterline/Sails adapter, an early implementation of a 11 | rapidly-developing, tool-agnostic data standard. Its goal is to 12 | provide a set of declarative interfaces, conventions, and 13 | best-practices for integrating with all sorts of data sources. 14 | Not just database s-- external APIs, proprietary web services, or even hardware. 15 | 16 | Strict adherence to an adapter specification enables the (re)use of 17 | built-in generic test suites, standardized documentation, reasonable 18 | expectations around the API for your users, and overall, a more 19 | pleasant development experience for everyone. 20 | 21 | This adapter has been developed pretty quickly and may contain bugs. 22 | 23 | ### Installation 24 | 25 | To install this adapter, run: 26 | 27 | ```sh 28 | $ npm install sails-arangodb 29 | ``` 30 | 31 | ### Usage 32 | 33 | This adapter exposes the following methods: 34 | 35 | ###### `find()` 36 | 37 | ###### `create()` 38 | 39 | ###### `update()` 40 | 41 | ###### `destroy()` 42 | 43 | ###### `createGraph()` # Create a Named Graph 44 | 45 | ###### `neighbors()` # Experimental, method signature is subject to change 46 | 47 | ###### `createEdge()` # Experimental, method signature is subject to change 48 | 49 | ###### `deleteEdge()` # Experimental, method signature is subject to change 50 | 51 | ### Connection 52 | 53 | Check out **Connections** in the Sails docs, or see the `config/connections.js` file in a new Sails project for information on setting up adapters. 54 | 55 | in connection.js 56 | ```javascript 57 | 58 | localArangoDB: { 59 | adapter: 'sails-arangodb', 60 | 61 | host: 'localhost', 62 | port: 8529, 63 | 64 | user: 'root', 65 | password: 'CgdYW3zBLy5yCszR', 66 | 67 | database: '_system' 68 | 69 | collection: 'examplecollection' // ArangoDB specific 70 | } 71 | ``` 72 | 73 | ### Schema for Graphs 74 | 75 | #### Defining a Named Graph in the Schema 76 | ``` 77 | /*jshint node: true, esversion: 6*/ 78 | 'use strict'; 79 | 80 | const Waterline = require('waterline'); 81 | 82 | const UsersProfilesGraph = Waterline.Collection.extend({ 83 | identity: 'users_profiles_graph', 84 | schema: true, 85 | connection: 'arangodb', 86 | 87 | attributes: { 88 | // this is a named graph 89 | $edgeDefinitions: [ 90 | { 91 | collection: 'users_profiles', 92 | from: ['users_1'], 93 | to: ['profiles_1'] 94 | } 95 | ] 96 | } 97 | }); 98 | module.exports = UsersProfilesGraph; 99 | 100 | ``` 101 | If a model has an attribute called `$edgeDefinitions` then the model becomes a named 102 | graph. Any further attributes are ignored. 103 | 104 | [See tests](tests/) for further examples. 105 | 106 | ### Unit Testing 107 | 108 | To run unit-tests every time you save a change to a file, simply: 109 | ``` 110 | $ gulp 111 | ``` 112 | 113 | One off run of sails-arangodb specific tests (same as above): 114 | ``` 115 | $ gulp test # or mocha 116 | ``` 117 | 118 | (Important: you must create a test.json file for your local db instance first - see [test/README.md](test/README.md)) 119 | 120 | To run the waterline adapter compliance tests: 121 | ``` 122 | $ gulp waterline 123 | ``` 124 | 125 | Generate api jsdocs: 126 | ``` 127 | $ gulp docs 128 | ``` 129 | (these are also generated in the default 'watch' mode above) 130 | 131 | --- 132 | 133 | # Older doc 134 | 135 | ### Example model definitions 136 | 137 | ```javascript 138 | /** 139 | * User Model 140 | * 141 | * The User model represents the schema of authentication data 142 | */ 143 | module.exports = { 144 | 145 | // Enforce model schema in the case of schemaless databases 146 | schema: true, 147 | tableName: 'User', 148 | attributes: { 149 | id: { 150 | type: 'string', 151 | primaryKey: true, 152 | columnName: '_key' 153 | }, 154 | username: { 155 | type: 'string', 156 | unique: true 157 | }, 158 | email: { 159 | type: 'email', 160 | unique: true 161 | }, 162 | profile: { 163 | collection: 'Profile', 164 | via: 'user', 165 | edge: 'userCommented' 166 | } 167 | } 168 | }; 169 | ``` 170 | ```javascript 171 | 172 | // api/models/Profile.js 173 | module.exports = { 174 | tableName: 'profile', 175 | attributes: { 176 | id: { 177 | type: 'string', 178 | primaryKey: true, 179 | columnName: '_key' 180 | }, 181 | user: { 182 | model: "User", 183 | required: true 184 | }, 185 | familyName: { 186 | type: 'string' 187 | }, 188 | givenName: { 189 | type: 'string' 190 | }, 191 | profilePic: { 192 | type: 'string' 193 | } 194 | } 195 | } 196 | 197 | // api/models/User.js 198 | module.exports = { 199 | tableName: 'user', 200 | attributes: { 201 | id: { 202 | type: 'string', 203 | primaryKey: true, 204 | columnName: '_key' 205 | }, 206 | username: { 207 | type: 'string' 208 | }, 209 | profile: { 210 | collection: 'profile', 211 | via: 'user', 212 | edge: 'profileOf' 213 | } 214 | } 215 | }; 216 | ; 217 | ``` 218 | 219 | 220 | ### License 221 | 222 | **[MIT](./LICENSE)** 223 | © 2016 Gabriel Letarte ([gabriel-letarte](http://github.com/gabriel-letarte)) & [thanks to] 224 | Taneli Leppä ([rosmo](http://github.com/rosmo)) & [thanks to] 225 | [vjsrinath](http://github.com/vjsrinath) & [thanks to] 226 | [balderdashy](http://github.com/balderdashy), [Mike McNeil](http://michaelmcneil.com), [Balderdash](http://balderdash.co) & contributors 227 | 228 | This adapter has been developed using [vjsrinath](http://github.com/vjsrinath)'s sails-orientdb as a template. 229 | 230 | [Sails](http://sailsjs.org) is free and open-source under the [MIT License](http://sails.mit-license.org/). 231 | 232 | 233 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*jslint browser: true, node: true, vars: true, esversion: 6*/ 2 | 'use strict'; 3 | 4 | var gulp = require('gulp'), 5 | mocha = require('gulp-mocha'), 6 | gulpLoadPlugins = require('gulp-load-plugins'), 7 | plugins = gulpLoadPlugins(), 8 | jsdoc = require('gulp-jsdoc3'), 9 | del = require('del'), 10 | watch = require('gulp-watch'), 11 | batch = require('gulp-batch'), 12 | spawn = require('child_process').spawn; 13 | 14 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 15 | 16 | var watching = false; 17 | 18 | gulp.task('mocha', function () { 19 | return gulp.src(['test/**/*.js', '!test/models/*.js', '!test/integration/runner.js'], { read: false }) 20 | .pipe(mocha({ 21 | reporter: 'spec', 22 | globals: { 23 | should: require('should').noConflict() 24 | } 25 | })) 26 | .once('error', function (err) { 27 | console.log('mocha errored... '); 28 | if (watching) { 29 | this.emit('end'); 30 | } else { 31 | process.exit(1); 32 | } 33 | }) 34 | .once('end', function () { 35 | console.log('mocha ended...'); 36 | if (watching) { 37 | this.emit('end'); 38 | } else { 39 | process.exit(0); 40 | } 41 | }); 42 | }); 43 | 44 | gulp.task('waterline', function (done) { 45 | var cp = spawn('node', ['test/integration/runner', '-R', 'spec', '-b'], {stdio: 'inherit'}); 46 | cp.on('close', (code) => { 47 | console.log('waterline adapter tests completed rc:', code); 48 | done(); 49 | }); 50 | }); 51 | 52 | gulp.task('test', ['mocha']); 53 | gulp.task('testwdocs', ['mocha', 'docs']); 54 | 55 | gulp.task('watch', function () { 56 | watching = true; 57 | watch([ 58 | 'lib/**', 59 | 'test/**' 60 | ], { 61 | ignoreInitial: false, 62 | verbose: false, 63 | readDelay: 1500 // filter duplicate changed events from Brackets 64 | }, batch(function (events, done) { 65 | gulp.start('testwdocs', done); 66 | })); 67 | }); 68 | gulp.task('default', ['watch']); 69 | 70 | gulp.task('docs', function (cb) { 71 | del(['./jsdocs/**']); 72 | gulp.src(['lib/*.js', './README.md']) 73 | .pipe(jsdoc( 74 | { 75 | opts: { 76 | destination: './jsdocs' 77 | }, 78 | plugins: [ 79 | 'plugins/markdown' 80 | ], 81 | templates: { 82 | 'cleverLinks': false, 83 | 'monospaceLinks': false, 84 | 'default': { 85 | 'outputSourceFiles': true 86 | }, 87 | 'path': 'ink-docstrap', 88 | 'theme': 'cerulean', 89 | 'navType': 'vertical', 90 | 'linenums': true, 91 | 'dateFormat': 'MMMM Do YYYY, h:mm:ss a' 92 | } 93 | }, 94 | cb 95 | )); 96 | }); 97 | -------------------------------------------------------------------------------- /lib/adapter.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, esversion:6 */ 2 | 'use strict'; 3 | 4 | var Connection = require('./connection'); 5 | var Processor = require('./processor'); 6 | var Q = require('q'); 7 | var _ = require('lodash'); 8 | var u = require('util'); 9 | var aqb = require('aqb'); 10 | var debug = require('debug')('sails-arangodb:adapter'); 11 | debug.log = console.log.bind(console); 12 | 13 | debug('loaded'); 14 | 15 | /** 16 | * sails-arangodb 17 | * 18 | * Most of the methods below are optional. 19 | * 20 | * If you don't need / can't get to every method, just implement what you have 21 | * time for. The other methods will only fail if you try to call them! 22 | * 23 | * For many adapters, this file is all you need. For very complex adapters, you 24 | * may need more flexiblity. In any case, it's probably a good idea to start 25 | * with one file and refactor only if necessary. If you do go that route, it's 26 | * conventional in Node to create a `./lib` directory for your private 27 | * submodules and load them at the top of the file with other dependencies. e.g. 28 | * var update = `require('./lib/update')`; 29 | * @module 30 | * @name adapter 31 | */ 32 | module.exports = (function() { 33 | 34 | // You'll want to maintain a reference to each connection 35 | // that gets registered with this adapter. 36 | var connections = {}; 37 | 38 | // You may also want to store additional, private data 39 | // per-connection (esp. if your data store uses persistent 40 | // connections). 41 | // 42 | // Keep in mind that models can be configured to use different databases 43 | // within the same app, at the same time. 44 | // 45 | // i.e. if you're writing a MariaDB adapter, you should be aware that one 46 | // model might be configured as `host="localhost"` and another might be 47 | // using 48 | // `host="foo.com"` at the same time. Same thing goes for user, database, 49 | // password, or any other config. 50 | // 51 | // You don't have to support this feature right off the bat in your 52 | // adapter, but it ought to get done eventually. 53 | // 54 | var getConn = function(config, collections) { 55 | debug('getConn() get connection'); 56 | return Connection.create(config, collections); 57 | }; 58 | 59 | var adapter = { 60 | 61 | // Set to true if this adapter supports (or requires) things like data 62 | // types, validations, keys, etc. 63 | // If true, the schema for models using this adapter will be 64 | // automatically synced when the server starts. 65 | // Not terribly relevant if your data store is not SQL/schemaful. 66 | // 67 | // If setting syncable, you should consider the migrate option, 68 | // which allows you to set how the sync will be performed. 69 | // It can be overridden globally in an app (config/adapters.js) 70 | // and on a per-model basis. 71 | // 72 | // IMPORTANT: 73 | // `migrate` is not a production data migration solution! 74 | // In production, always use `migrate: safe` 75 | // 76 | // drop => Drop schema and data, then recreate it 77 | // alter => Drop/add columns as necessary. 78 | // safe => Don't change anything (good for production DBs) 79 | // 80 | syncable: false, 81 | 82 | // Primary key format is string (_key|_id) 83 | pkFormat: 'string', 84 | 85 | /** 86 | * get the db connection 87 | * @function 88 | * @name getConnection 89 | * @param {object} config configuration 90 | * @param {array} collections list of collections 91 | */ 92 | getConnection: getConn, 93 | 94 | /** 95 | * This method runs when a model is initially registered at 96 | * server-start-time. This is the only required method. 97 | * @function 98 | * @name registerConnection 99 | * @param {object} connection DB Connection 100 | * @param {array} collection Array of collections 101 | * @param {function} cb callback 102 | */ 103 | registerConnection: function(connection, collections, cb) { 104 | debug('registerConnection() connection:', connection); 105 | 106 | if (!connection.identity) 107 | return cb(new Error('Connection is missing an identity.')); 108 | if (connections[connection.identity]) 109 | return cb(new Error('Connection is already registered.')); 110 | // Add in logic here to initialize connection 111 | // e.g. connections[connection.identity] = new Database(connection, 112 | // collections); 113 | 114 | getConn(connection, collections).then(function(helper) { 115 | connections[connection.identity] = helper; 116 | cb(); 117 | }); 118 | }, 119 | 120 | /** 121 | * Fired when a model is unregistered, typically when the server is 122 | * killed. Useful for tearing-down remaining open connections, etc. 123 | * @function 124 | * @name teardown 125 | * @param {object} conn Connection 126 | * @param {function} cb callback 127 | */ 128 | // Teardown a Connection 129 | teardown: function(conn, cb) { 130 | debug('teardown()'); 131 | 132 | if (typeof conn == 'function') { 133 | cb = conn; 134 | conn = null; 135 | } 136 | if (!conn) { 137 | connections = {}; 138 | return cb(); 139 | } 140 | if (!connections[conn]) 141 | return cb(); 142 | delete connections[conn]; 143 | cb(); 144 | }, 145 | 146 | // Return attributes 147 | describe: function(connection, collection, cb) { 148 | debug('describe()'); 149 | // Add in logic here to describe a collection (e.g. DESCRIBE TABLE 150 | // logic) 151 | 152 | connections[connection].collection.getProperties(collection, 153 | function(res, err) { 154 | cb(err, res); 155 | }); 156 | 157 | }, 158 | 159 | /** 160 | * 161 | * REQUIRED method if integrating with a schemaful (SQL-ish) database. 162 | * 163 | */ 164 | define: function(connection, collection, definition, cb) { 165 | debug('define()'); 166 | // Add in logic here to create a collection (e.g. CREATE TABLE 167 | // logic) 168 | var deferred = Q.defer(); 169 | connections[connection].createCollection(collection, function(db) { 170 | deferred.resolve(db); 171 | }); 172 | return deferred.promise; 173 | }, 174 | 175 | /** 176 | * 177 | * REQUIRED method if integrating with a schemaful (SQL-ish) database. 178 | * 179 | */ 180 | drop: function(connection, collection, relations, cb) { 181 | // Add in logic here to delete a collection (e.g. DROP TABLE logic) 182 | connections[connection].drop(collection, relations, cb); 183 | }, 184 | 185 | /** 186 | * 187 | * REQUIRED method if users expect to call Model.find(), 188 | * Model.findOne(), or related. 189 | * 190 | * You should implement this method to respond with an array of 191 | * instances. Waterline core will take care of supporting all the other 192 | * different find methods/usages. 193 | * 194 | */ 195 | //Gets the underlying arango instance used by adapter 196 | getDB: function(connectionName, collectionName, cb) { 197 | debug('getDB()'); 198 | return connections[connectionName].getDB(cb); 199 | }, 200 | 201 | /** 202 | * Implements find method 203 | * @function 204 | * @name find 205 | * @param {string} connectionName Name of the connection 206 | * @param {string} collectionName Name of the collection 207 | * @param {object} searchCriteria Search criterial (passed from waterline) 208 | * @param {function} cb callback (err, results) 209 | */ 210 | find: function(connectionName, collectionName, searchCriteria, cb) { 211 | debug('adaptor find() connectionName:', connectionName, 'collectionName:', collectionName, 'searchCriteria:', searchCriteria); 212 | 213 | connections[connectionName].find(collectionName, searchCriteria, function (err, r) { 214 | if (err) { 215 | return cb(err); 216 | } 217 | debug('find results before cast:', r); 218 | var processor = new Processor(connections[connectionName].collections); 219 | var cast_r = processor.cast(collectionName, {_result: r}); 220 | debug('find results after cast:', cast_r); 221 | return cb(null, cast_r); 222 | }); 223 | }, 224 | 225 | //Executes the query using Arango's query method 226 | query: function(connectionName, collectionName, options, cb) { 227 | return connections[connectionName].query(collectionName, options, cb); 228 | }, 229 | 230 | /** 231 | * Create a named graph - dynamically instead of via the defined schema 232 | * @function 233 | * @name createGraph 234 | * @param {string} connectionName Name of the connection 235 | * @param {string} collectionName Name of the collection 236 | * @param {string} graphName Graph name 237 | * @param {array} edgeDefs Array of edge definitions 238 | * @param {function} cb Optional Callback (err, res) 239 | * @returns {Promise} 240 | * 241 | * example of edgeDefs: 242 | * ``` 243 | * [{ 244 | * collection: 'edges', 245 | * from: ['start-vertices'], 246 | * to: ['end-vertices'] 247 | * }, ...] 248 | * ``` 249 | */ 250 | createGraph: function(connectionName, collectionName, graphName, edgeDefs, cb){ 251 | debug('createGraph()'); 252 | 253 | var db = connections[connectionName].db; 254 | 255 | return connections[connectionName].createGraph(db, graphName, edgeDefs, cb); 256 | }, 257 | 258 | /** 259 | * Get neighbours of a document's start _id via either a named graph 260 | * or a list of edges. 261 | * @function 262 | * @name neighbors 263 | * @param {string} connectionName Name of the connection 264 | * @param {string} collectionName Name of the collection 265 | * @param {string} startId Document _id to start graph neighbors search from 266 | * @param {string|array} graphNameOrEdges Graph name or alternatively an 267 | * array of edge collection names 268 | * (anonymous graph) 269 | * @returns {Promise} 270 | */ 271 | neighbors: function(connectionName, collectionName, startId, graphNameOrEdges, cb){ 272 | debug('neighbors()'); 273 | 274 | var db = connections[connectionName].db; 275 | var col = db.collection(collectionName); 276 | 277 | var target = `${graphNameOrEdges}`; 278 | if (_.isArray(graphNameOrEdges)) { 279 | target = graphNameOrEdges.join(','); 280 | } 281 | 282 | var q = ` 283 | FOR n IN ANY '${startId}' 284 | ${_.isArray(graphNameOrEdges) ? target : `GRAPH '${target}'`} 285 | OPTIONS {bfs: true, uniqueVertices: 'global'} 286 | RETURN n 287 | `; 288 | 289 | debug('-------------------'); 290 | debug(startId); 291 | debug(graphNameOrEdges); 292 | debug(q); 293 | debug('-------------------'); 294 | 295 | return db.query(q) 296 | .then((res) => { 297 | var results = res._result; 298 | if (cb) { 299 | cb(null, results); 300 | } 301 | return Promise.resolve(results); 302 | }) 303 | .catch((err) => { 304 | if (cb) { 305 | cb(err); 306 | } 307 | return Promise.reject(err); 308 | }); 309 | }, 310 | 311 | /** 312 | * Create an edge 313 | * 314 | * This method will be bound to WaterlineORM objects, E.g: 315 | * `User.createEdge` 316 | * But it's not using the User in any way... 317 | * 318 | * @function 319 | * @name createEdge 320 | * @param {string} connectionName Connection name 321 | * @param {string} collectionName Collection name 322 | * @param {string} edgeCollectionName Edge collection name 323 | * @param {string} id1 From id (must be _id) 324 | * @param {string} id2 To id (must be _id) 325 | * @param {object} attributes Attributes to be added to the edge 326 | * @param {function} cb Optional Callback (err, res) 327 | * @returns {Promise} 328 | */ 329 | createEdge: function(connectionName, collectionName, edgeCollectionName, id1, id2, attributes, cb){ 330 | debug('createEdge() connectionName:', connectionName, 'collectionName:', collectionName, 331 | 'edgeCollectionName:', edgeCollectionName, 'id1:', id1, 'id2:', id2, 332 | 'attributes:', attributes, 'cb:', cb); 333 | 334 | var db = connections[connectionName].db; 335 | 336 | if (cb === undefined && typeof attributes === 'function'){ 337 | cb = attributes; 338 | attributes = {}; 339 | } 340 | 341 | var data = _.merge(attributes, { 342 | _from: id1, 343 | _to: id2, 344 | }); 345 | 346 | var edges = db.edgeCollection(edgeCollectionName); 347 | return edges.save(data) 348 | .then((res) => { 349 | if (cb) { 350 | cb(null, res); 351 | } 352 | return Promise.resolve(res); 353 | }) 354 | .catch((err) => { 355 | if (cb) { 356 | cb(err); 357 | } 358 | return Promise.reject(err); 359 | }); 360 | }, 361 | 362 | /** 363 | * Delete an edge 364 | * 365 | * @function 366 | * @name deleteEdge 367 | * @param {string} connectionName Connection name 368 | * @param {string} collectionName Collection name 369 | * @param {string} edgeCollectionName Edge collection name 370 | * @param {string} id Edge id (must be _id) 371 | * @param {function} cb Optional Callback (err, res) 372 | * @returns {Promise} 373 | */ 374 | deleteEdge: function(connectionName, collectionName, edgeCollectionName, id, cb){ 375 | debug('deleteEdge()'); 376 | 377 | var db = connections[connectionName].db; 378 | 379 | var edges = db.edgeCollection(edgeCollectionName); 380 | return edges.remove(id) 381 | .then((res) => { 382 | if (cb) { 383 | cb(null, res); 384 | } 385 | return Promise.resolve(res); 386 | }) 387 | .catch((err) => { 388 | if (cb) { 389 | cb(err); 390 | } 391 | return Promise.reject(err); 392 | }); 393 | }, 394 | 395 | /** 396 | * Implements create method 397 | * @function 398 | * @name create 399 | * @param {string} connectionName Connection Name 400 | * @param {string} collectionName Collection Name 401 | * @param {object} data Document data to create 402 | * @param {function} cb Callback (err, data) 403 | */ 404 | create: function(connectionName, collectionName, data, cb) { 405 | debug('create() collectionName:', collectionName, 'data:', data); 406 | var col = connections[connectionName].db.collection(collectionName); 407 | return col.save(data, true, function (err, doc) { 408 | if (err) { 409 | debug('create err:', err); 410 | return cb(err); 411 | } 412 | 413 | var processor = new Processor(connections[connectionName].collections); 414 | debug('create err:', err, 'returning doc.new:', doc.new); 415 | cb(null, processor.cast(collectionName, {_result: doc.new})); 416 | }); 417 | }, 418 | 419 | // Although you may pass .update() an object or an array of objects, 420 | // it will always return an array of objects. 421 | // Any string arguments passed must be the ID of the record. 422 | // If you specify a primary key (e.g. 7 or 50c9b254b07e040200000028) 423 | // instead of a criteria object, any .where() filters will be ignored. 424 | /** 425 | * Implements update method 426 | * @function 427 | * @name update 428 | * @param {string} connectionName Connection Name 429 | * @param {string} collectionName Collection Name 430 | * @param {object} searchCriteria Search Criteria 431 | * @param {object} values Document data to update 432 | * @param {function} cb Callback (err, data) 433 | */ 434 | update: function(connectionName, collectionName, searchCriteria , values , cb) { 435 | debug('update() collection:', collectionName, 'values:', values); 436 | var col = connections[connectionName] 437 | .update(collectionName, searchCriteria, values, function (err, docs) { 438 | if (err) { 439 | debug('update err:', err); 440 | return cb(err); 441 | } 442 | 443 | var processor = new Processor(connections[connectionName].collections); 444 | debug('update err:', err, 'returning docs:', docs); 445 | cb(null, processor.cast(collectionName, {_result: docs})); 446 | }); 447 | }, 448 | 449 | destroy: function(connectionName, collectionName, options, cb) { 450 | debug('destroy() options:', options); 451 | return connections[connectionName] 452 | .destroy(collectionName, options, (err, docs) => { 453 | if (err) { 454 | debug('destroy err:', err); 455 | return cb(err); 456 | } 457 | 458 | var processor = new Processor(connections[connectionName].collections); 459 | debug('destroy err:', err, 'returning docs:', docs); 460 | cb(null, processor.cast(collectionName, {_result: docs})); 461 | }); 462 | }, 463 | 464 | // @TODO: Look into ArangoJS for similar & better functions 465 | _limitFormatter: function(searchCriteria) { 466 | debug('_limitFormatter()'); 467 | var r = ''; 468 | if (searchCriteria.LIMIT){ 469 | r = 'LIMIT '; 470 | if (searchCriteria.SKIP){ 471 | r += searchCriteria.SKIP; 472 | delete searchCriteria.SKIP; 473 | } 474 | r += searchCriteria.LIMIT; 475 | delete searchCriteria.LIMIT; 476 | } 477 | return r; 478 | }, 479 | 480 | _updateStringify: function (values) { 481 | debug('_updateStringify()'); 482 | var r = ''; 483 | r = JSON.stringify(values); 484 | 485 | // remove leading and trailing {}'s 486 | return r.replace(/(^{|}$)/g, ''); 487 | }, 488 | 489 | // @TODO: Prevent injection 490 | _queryParamWrapper: function(param) { 491 | debug('_queryParamWrapper() param:', param); 492 | if (typeof param === 'string'){ 493 | return "'" + param + "'"; 494 | } 495 | else if (typeof param === 'object') { 496 | var s, ii; 497 | if (Object.prototype.toString.call(param) === '[object Array]') { 498 | s = '['; 499 | for (ii=0; ii < param.length; ii++) { 500 | if (ii) s += ','; 501 | s += this._queryParamWrapper(param[ii]); 502 | } 503 | s += ']'; 504 | } 505 | else { 506 | s = '{'; 507 | for (ii in param) { 508 | s += ii + ':'; 509 | s += this._queryParamWrapper(param[ii]); 510 | } 511 | s += '}'; 512 | } 513 | return s; 514 | } 515 | return param; 516 | }, 517 | 518 | quote: function(connection, collection, val) { 519 | debug('quote()'); 520 | return connections[connection].quote(val); 521 | }, 522 | 523 | /** 524 | * Implements join method for .populate() 525 | * @function 526 | * @name join 527 | * @param {string} connection Connection Name 528 | * @param {string} collection Collection Name 529 | * @param {object} criteria Document data to create 530 | * @param {function} cb Callback (err, data) 531 | */ 532 | join: function(connection, collection, criteria, cb) { 533 | debug('join() criteria:', criteria.joins[0].criteria); 534 | connections[connection].join(collection, criteria, function (err, r) { 535 | if (err) { 536 | return cb(err); 537 | } 538 | var processor = new Processor(connections[connection].collections); 539 | var cast_r = processor.cast(collection, {_result: r}); 540 | return cb(null, cast_r); 541 | }); 542 | } 543 | 544 | }; 545 | 546 | // Expose adapter definition 547 | return adapter; 548 | 549 | })(); 550 | -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, esversion:6 */ 2 | 'use strict'; 3 | 4 | /*global console, process*/ 5 | var Arango = require('arangojs'), 6 | Q = require('q'), 7 | async = require('async'), 8 | _ = require('lodash'), 9 | aqb = require('aqb'), 10 | debug = require('debug')('sails-arangodb:connection'); 11 | 12 | debug.log = console.log.bind(console); 13 | 14 | /** 15 | * 16 | * @module 17 | * @name connection 18 | */ 19 | module.exports = (function() { 20 | 21 | var serverUrl = ''; 22 | var defaults = { 23 | createCustomIndex: false, 24 | idProperty: 'id', 25 | caseSensitive: false 26 | }; 27 | 28 | var server; 29 | 30 | var DbHelper = function(db, graph, collections, config) { 31 | this.db = db; 32 | this.graph = graph; 33 | this.collections = collections; 34 | this.config = _.extend(config, defaults); 35 | }; 36 | 37 | /** 38 | * Connect to ArangoDB and use the requested database or '_system' 39 | */ 40 | var getDb = function(connection) { 41 | debug('getDB() connection:', connection); 42 | var userpassword = ''; 43 | if (connection.user && connection.password) { 44 | userpassword = connection.user + ':' + connection.password + '@'; 45 | } 46 | 47 | serverUrl = 'http://' + userpassword + connection.host + ':' + connection.port; 48 | if (!server) { 49 | server = new Arango({ 50 | url: serverUrl, 51 | databaseName: connection.database || '_system' 52 | }); 53 | } 54 | return server; 55 | }; 56 | 57 | var getGraph = function(db, connection) { 58 | debug('getGraph() connection.graph:', connection.graph); 59 | return db.graph(connection.graph); 60 | }; 61 | 62 | var getCollection = function(db, connection) { 63 | return db.collection(connection.collection); 64 | }; 65 | 66 | DbHelper.logError = function(err) { 67 | console.error(err.stack); 68 | }; 69 | 70 | DbHelper.prototype.db = null; 71 | DbHelper.prototype.graph = null; 72 | DbHelper.prototype.collections = null; 73 | DbHelper.prototype.config = null; 74 | DbHelper.prototype._classes = null; 75 | 76 | DbHelper.prototype.getClass = function(collection) { 77 | return this._classes[collection]; 78 | }; 79 | 80 | DbHelper.prototype.ensureIndex = function() { 81 | // to be implemented? 82 | }; 83 | 84 | /*Makes sure that all the collections are synced to database classes*/ 85 | DbHelper.prototype.registerCollections = function() { 86 | var deferred = Q.defer(); 87 | var me = this; 88 | var db = me.db; 89 | var graph = me.graph; 90 | var collections = this.collections; 91 | 92 | async.auto({ 93 | ensureDB: function(next) { 94 | debug('ensureDB()'); 95 | var system_db = Arango({ 96 | url: serverUrl, 97 | databaseName: '_system' 98 | }); 99 | system_db.listDatabases(function(err, dbs) { 100 | if (err) { 101 | DbHelper.logError(err); 102 | process.exit(1); 103 | } 104 | 105 | // Create the DB if needed 106 | if (dbs.indexOf(db.name) === -1) { 107 | system_db.createDatabase(db.name, function(err, result) { 108 | if (err) { 109 | DbHelper.logError(err); 110 | process.exit(1); 111 | } 112 | debug('Created database: ' + db.name); 113 | next(null, db.name); 114 | }); 115 | } 116 | else { 117 | next(null, db.name); 118 | } 119 | }); 120 | }, 121 | 122 | // Get collections from DB 123 | getCollections: ['ensureDB', function(next) { 124 | debug('getCollections()'); 125 | db.collections(function(err, cols) { 126 | if (err){ 127 | DbHelper.logError(err); 128 | process.exit(1); 129 | } 130 | var docCollection = cols.filter(function(c){ 131 | if (c.type == 2){ // @TODO: Use something like ArangoDB.EDGE_COLLECTION 132 | // see https://github.com/gabriel-letarte/arangojs/blob/master/src/collection.js 133 | // export const types = { 134 | // DOCUMENT_COLLECTION: 2, 135 | // EDGE_COLLECTION: 3 136 | // }; 137 | return c; 138 | } 139 | }); 140 | var edgeCollection = cols.filter(function(c){ 141 | if (c.type == 3){ // @TODO: see above 142 | return c; 143 | } 144 | }); 145 | next(null, { 146 | 'docCollection': docCollection, 147 | 'edgeCollection': edgeCollection, 148 | }); 149 | }); 150 | }], 151 | 152 | /** 153 | * Get all existing named graphs 154 | */ 155 | getNamedGraphs: ['ensureDB', function (next) { 156 | debug('getNamedGraphs'); 157 | db.listGraphs() 158 | .then((graphs) => { 159 | debug('graphs:', graphs); 160 | let graphs_hash = {}; 161 | graphs.forEach((g) => { 162 | graphs_hash[g._key] = g; 163 | }); 164 | next(null, graphs_hash); 165 | }) 166 | .catch((err) => { 167 | next(err); 168 | }); 169 | }], 170 | 171 | // Get relations from DB 172 | getEdgeCollections: ['ensureDB', function(next) { 173 | debug('getEdgeCollections()'); 174 | var edgeCollections = []; 175 | _.each(collections, function(v, k) { 176 | _.each(v.attributes, function(vv, kk) { 177 | if (vv.edge) { 178 | vv.from = v.tableName; 179 | edgeCollections.push(vv); 180 | } 181 | }); 182 | }); 183 | next(null, edgeCollections); 184 | }], 185 | 186 | createMissingCollections: ['getCollections', function(next, results) { 187 | debug('createMissingCollections()'); 188 | var currentCollections = results.getCollections.docCollection; 189 | var missingCollections = _.filter(collections, function(v, k) { 190 | debug('createMissingCollections edgeDefinitions:', v.adapter.query._attributes.$edgeDefinitions); 191 | debug('createMissingCollections hasSchema:', v.adapter.query.hasSchema); 192 | return _.find(currentCollections, function(klass) { 193 | return v.adapter.collection == klass.name; 194 | }) === undefined && ( 195 | !v.adapter.query.attributes || 196 | !v.adapter.query.attributes.$edgeDefinitions 197 | ); 198 | }); 199 | if (missingCollections.length > 0) { 200 | async.mapSeries(missingCollections, 201 | function(collection, cb) { 202 | debug('db.collection - CALLED', collection.adapter.collection, 'junctionTable:', collection.meta.junctionTable); 203 | db.collection(collection.adapter.collection).create(function(err){ 204 | if (err) { 205 | debug('err:', err); 206 | return cb(err, null); 207 | } 208 | debug('db.collection - DONE'); 209 | return cb(null, collection); 210 | }); 211 | }, 212 | function(err, created) { 213 | next(err, created); 214 | }); 215 | } else { 216 | next(null, []); 217 | } 218 | }], 219 | 220 | /** 221 | * Create any missing Edges 222 | */ 223 | createMissingEdges: ['getCollections', 'getEdgeCollections', function(next, results) { 224 | debug('createMissingEdges()'); 225 | var classes = results.getCollections; 226 | async.mapSeries(results.getEdgeCollections, 227 | function(collection, cb) { 228 | if (!_.find(classes, function(v) { 229 | return (v == collection.edge); 230 | })) { 231 | debug('db.edgeCollection - CALLED', collection.edge); 232 | db.edgeCollection(collection.edge).create(function(results){ 233 | debug('db.edgeCollection - DONE'); 234 | return cb(null, collection); 235 | }); 236 | } 237 | return cb(null, null); 238 | }, 239 | function(err, created) { 240 | next(err, created); 241 | }); 242 | }], 243 | 244 | /** 245 | * Create any missing Named Graphs 246 | */ 247 | createMissingNamedGraph: ['createMissingCollections', 'getNamedGraphs', function (next, results) { 248 | const currentNamedGraphs = results.getNamedGraphs; 249 | debug('createMissingNamedGraph currentNamedGraphs:', currentNamedGraphs); 250 | const missingNamedGraphs = {}; 251 | _.each(collections, (v, k) => { 252 | debug('createMissingNamedGraph hasSchema:', v.adapter.query.hasSchema, 'k:', k); 253 | if (currentNamedGraphs[k] === undefined && 254 | v.adapter.query.attributes && 255 | v.adapter.query.attributes.$edgeDefinitions 256 | ) { 257 | missingNamedGraphs[k] = v; 258 | } 259 | }); 260 | 261 | const promises = []; 262 | _.each(missingNamedGraphs, (g, k) => { 263 | promises.push(me.createGraph(db, k, g.attributes.$edgeDefinitions)); 264 | }); 265 | Q.all(promises) 266 | .then((r) => { 267 | next(null, r); 268 | }) 269 | .fail((err) => { 270 | next(err); 271 | }); 272 | }], 273 | 274 | addVertexCollections: ['createMissingCollections', function(next, results) { 275 | debug('addVertexCollections()'); 276 | async.mapSeries(results.createMissingCollections, 277 | function(collection, cb) { 278 | graph.addVertexCollection(collection.tableName, function() { 279 | return cb(null, collection); 280 | }); 281 | }, 282 | function(err, created) { 283 | next(null, results); 284 | }); 285 | }], 286 | 287 | addEdgeDefinitions: ['addVertexCollections', 'createMissingEdges', function(complete, results) { 288 | debug('addEdgeDefinitions()'); 289 | async.mapSeries(results.getEdgeCollections, 290 | function(edge, cb) { 291 | graph.addEdgeDefinition({ 292 | from: [edge.from], 293 | collection: edge.edge, 294 | to: [edge.collection] 295 | }, function() { 296 | cb(null, edge); 297 | }); 298 | }, 299 | function(err, created) { 300 | complete(null, results); 301 | }); 302 | }] 303 | }, 304 | function(err, results) { 305 | if (err) { 306 | debug('ASYNC.AUTO - err:', err); 307 | deferred.reject(err); 308 | return; 309 | } 310 | debug('ASYNC.AUTO - DONE results:', results); 311 | 312 | deferred.resolve(results.createMissingCollections); 313 | 314 | }); 315 | 316 | return deferred.promise; 317 | }; 318 | 319 | DbHelper.prototype.quote = function(val) { 320 | return aqb(val).toAQL(); 321 | }; 322 | 323 | /*Query methods starts from here*/ 324 | DbHelper.prototype.query = function(collection, query, cb) { 325 | debug('query() ', query); 326 | this.db.query(query, function(err, cursor) { 327 | if (err) return cb(err); 328 | cursor.all(function(err, vals) { 329 | return cb(err, vals); 330 | }); 331 | }); 332 | }; 333 | 334 | DbHelper.prototype.getDB = function(cb) { 335 | var db = this.db; 336 | return cb(db); 337 | }; 338 | 339 | DbHelper.prototype.optionsToQuery = function(collection, options, qb) { 340 | var self = this; 341 | 342 | debug('optionsToQuery() options:', options); 343 | qb = qb ? qb : aqb.for('d').in(collection); 344 | 345 | 346 | function buildWhere(where, recursed) { 347 | debug('buildWhere where:', where, 'recursed:', recursed); 348 | 349 | var outer_i = 0; 350 | _.each(where, function(v, k) { 351 | 352 | if (outer_i > 0) { 353 | if (whereStr !== '') whereStr += ` && `; 354 | } 355 | outer_i += 1; 356 | 357 | // handle or: [{field1: 'value', field2: 'value'}, ...] 358 | if (k === 'or') { 359 | if (_.isArray(v)) { 360 | whereStr += `${recursed ? '&&' : ''} (`; 361 | _.each(v, function (v_element, i_element) { 362 | if (i_element > 0) { 363 | whereStr += ' || '; 364 | } 365 | buildWhere(v_element, true, i_element); 366 | }); 367 | whereStr += ')'; 368 | return; 369 | } 370 | } 371 | 372 | // like as keyword 373 | if (k === 'like') { 374 | k = Object.keys(v)[0]; 375 | v = { 'like': v[k] }; 376 | } 377 | 378 | // Handle filter operators 379 | debug('options.where before operators: k:', k, 'v:', typeof v, v); 380 | var operator = '=='; 381 | var eachFunction = ''; 382 | var skip = false; 383 | var pre = ''; 384 | 385 | // handle config default caseSensitive option 386 | debug('config caseSensitive:', self.config.caseSensitive); 387 | if (self.config.hasOwnProperty('caseSensitive')) { 388 | if (!self.config.caseSensitive) { 389 | eachFunction = 'LOWER'; 390 | } else { 391 | eachFunction = ''; 392 | } 393 | } 394 | 395 | if (v && typeof v === 'object') { 396 | // handle array of values for IN 397 | if (_.isArray(v)) { 398 | operator = 'IN'; 399 | eachFunction = ''; 400 | } else { 401 | // Handle filter's options 402 | 403 | debug('v caseSensitive:', v.caseSensitive); 404 | if (v.hasOwnProperty('caseSensitive')) { 405 | if (!v.caseSensitive) { 406 | eachFunction = 'LOWER'; 407 | } else { 408 | eachFunction = ''; 409 | } 410 | delete v.caseSensitive; 411 | } 412 | 413 | _.each(v, (vv, kk) => { 414 | debug('optionsToQuery kk:', kk, 'vv:', typeof vv, vv); 415 | v = vv; 416 | switch(kk) { 417 | case 'contains': 418 | operator = 'LIKE'; 419 | v = `%${vv}%`; 420 | break; 421 | 422 | case 'like': 423 | operator = 'LIKE'; 424 | break; 425 | 426 | case 'startsWith': 427 | operator = 'LIKE'; 428 | v = `${vv}%`; 429 | break; 430 | 431 | case 'endsWith': 432 | operator = 'LIKE'; 433 | v = `%${vv}`; 434 | break; 435 | 436 | case 'lessThanOrEqual': 437 | case '<=': 438 | operator = '<='; 439 | pre = `HAS(d, "${k}") AND`; 440 | break; 441 | 442 | case 'lessThan': 443 | case '<': 444 | operator = '<'; 445 | pre = `HAS(d, "${k}") AND`; 446 | break; 447 | 448 | case 'greaterThanOrEqual': 449 | case '>=': 450 | operator = '>='; 451 | break; 452 | 453 | case 'greaterThan': 454 | case '>': 455 | operator = '>'; 456 | break; 457 | 458 | case 'not': 459 | case '!': 460 | if (_.isArray(vv)) { // in waterline v0.11/12 461 | operator = 'NOT IN'; 462 | eachFunction = ''; 463 | } else { 464 | operator = '!='; 465 | } 466 | break; 467 | 468 | case 'nin': // in waterline master (upcoming) 469 | operator = 'NOT IN'; 470 | eachFunction = ''; 471 | break; 472 | 473 | default: 474 | const newWhere = {}; 475 | newWhere[`${k}.${kk}`] = vv; 476 | buildWhere(newWhere, true); // recursive for next level 477 | skip = true; 478 | } 479 | }); 480 | } 481 | } 482 | 483 | if (skip) { 484 | return; // to outer each() loop 485 | } 486 | 487 | switch (k) { 488 | case 'id': 489 | whereStr += `${eachFunction}(d._key) ${operator} ${eachFunction}(${aqb(v).toAQL()})`; 490 | break; 491 | case '_key': 492 | case '_rev': 493 | whereStr += `${eachFunction}(d.${k}) ${operator} ${eachFunction}(${aqb(v).toAQL()})`; 494 | break; 495 | default: 496 | whereStr += `(${pre} ${eachFunction}(d.${k}) ${operator} ${eachFunction}(${aqb(v).toAQL()}))`; 497 | break; 498 | } 499 | 500 | debug('interim whereStr:', whereStr); 501 | 502 | }); 503 | } // function buildWhere 504 | 505 | if (options.where && options.where !== {}) { 506 | var whereStr = ''; 507 | 508 | buildWhere(options.where); 509 | 510 | debug('whereStr:', whereStr); 511 | debug('qb:', qb); 512 | qb = qb.filter(aqb.expr('(' + whereStr + ')')); 513 | } 514 | 515 | // handle sort option 516 | if (options.sort && !_.isEmpty(options.sort)) { 517 | var sortArgs; 518 | debug('sort options:', options.sort); 519 | // as an object {'field': -1|1} 520 | sortArgs = _.map(options.sort, (v, k) => { 521 | return [`d.${k}`, `${v < 0 ? 'DESC' : 'ASC'}`]; 522 | }); 523 | 524 | // force consistent results 525 | sortArgs.push(['d._key', 'ASC']); 526 | 527 | sortArgs = _.flatten(sortArgs); 528 | 529 | debug('sortArgs:', sortArgs); 530 | qb = qb.sort.apply(qb, sortArgs); 531 | } 532 | 533 | if (options.limit !== undefined) { 534 | qb = qb.limit((options.skip ? options.skip : 0), options.limit); 535 | } else if (options.skip !== undefined) { 536 | qb = qb.limit(options.skip, Number.MAX_SAFE_INTEGER); 537 | } 538 | 539 | debug('optionsToQuery() returns:', qb); 540 | return qb; 541 | }; 542 | 543 | // e.g. FOR d IN userTable2 COLLECT Group="all" into g RETURN {age: AVERAGE(g[*].d.age)} 544 | DbHelper.prototype.applyFunctions = function (options, qb) { 545 | // handle functions 546 | var funcs = {}; 547 | _.each(options, function (v, k) { 548 | if (_.includes(['where', 'sort', 'limit', 'skip', 'select', 'joins'], k)) { 549 | return; 550 | } 551 | funcs[k] = v; 552 | }); 553 | debug('applyFunctions() funcs:', funcs); 554 | 555 | if (Object.keys(funcs).length === 0) { 556 | qb = qb.return({'d': 'd'}); 557 | return qb; 558 | } 559 | 560 | var funcs_keys = Object.keys(funcs); 561 | debug('applyFunctions() funcs_keys:', funcs_keys); 562 | 563 | var isGroupBy = false; 564 | var collectObj = {}; 565 | 566 | var retobj = {}; 567 | funcs_keys.forEach(function(func) { 568 | options[func].forEach(function(field) { 569 | if (typeof field !== 'object') { 570 | if (func === 'groupBy') { 571 | isGroupBy = true; 572 | collectObj[field] = `d.${field}`; 573 | retobj[field] = field; 574 | } else { 575 | retobj[field] = aqb.fn(func.toUpperCase())('g[*].d.' + field); 576 | } 577 | } 578 | }); 579 | }); 580 | 581 | if (isGroupBy) { 582 | qb = qb.collect(collectObj).into('g'); 583 | } else { 584 | qb = qb.collect('Group', '"all"').into('g'); 585 | } 586 | 587 | debug('retobj:', retobj); 588 | qb = qb.return({'d': retobj}); 589 | return qb; 590 | }; 591 | 592 | DbHelper.prototype.find = function(collection, options, cb) { 593 | var me = this; 594 | debug('connection find() collection:', collection, 'options:', options); 595 | var qb = this.optionsToQuery(collection, options); 596 | qb = me.applyFunctions(options, qb); 597 | 598 | var find_query = qb.toAQL(); 599 | debug('find_query:', find_query); 600 | 601 | this.db.query(find_query, function(err, cursor) { 602 | debug('connection find() query err:', err); 603 | if (err) return cb(err); 604 | 605 | me._filterSelected(cursor, options) 606 | .then((vals) => { 607 | debug('query find response:', vals.length, 'documents returned for query:', find_query); 608 | debug('vals:', vals); 609 | return cb(null, _.map(vals, function(item) { 610 | return item.d; 611 | })); 612 | }) 613 | .catch((err) => { 614 | console.error('find() error:', err); 615 | cb(err, null); 616 | }); 617 | }); 618 | }; 619 | 620 | //Deletes a collection from database 621 | DbHelper.prototype.drop = function(collection, relations, cb) { 622 | this.db.collection(collection).drop(cb); 623 | }; 624 | 625 | /* 626 | * Updates a document from a collection 627 | */ 628 | DbHelper.prototype.update = function(collection, options, values, cb) { 629 | debug('update options:', options); 630 | var replace = false; 631 | if (options.where) { 632 | replace = options.where.$replace || false; 633 | delete options.where.$replace; 634 | } 635 | 636 | if (replace) { 637 | // provide a new createdAt 638 | values.createdAt = new Date(); 639 | } 640 | 641 | debug('values:', values); 642 | var qb = this.optionsToQuery(collection, options), 643 | doc = aqb(values); 644 | 645 | if (replace) { 646 | qb = qb.replace('d').with_(doc).in(collection); 647 | } else { 648 | qb = qb.update('d').with_(doc).in(collection); 649 | } 650 | var query = qb.toAQL() + ' LET modified = NEW RETURN modified'; 651 | debug('update() query:', query); 652 | 653 | this.db.query(query, function(err, cursor) { 654 | if (err) return cb(err); 655 | cursor.all(function(err, vals) { 656 | return cb(err, vals); 657 | }); 658 | }); 659 | }; 660 | 661 | /* 662 | * Deletes a document from a collection 663 | */ 664 | DbHelper.prototype.destroy = function(collection, options, cb) { 665 | var qb = this.optionsToQuery(collection, options); 666 | debug('destroy() qb:', qb); 667 | qb = qb.remove('d').in(collection); 668 | this.db.query(qb.toAQL() + ' LET removed = OLD RETURN removed', function(err, cursor) { 669 | if (err) return cb(err); 670 | cursor.all(function(err, vals) { 671 | return cb(err, vals); 672 | }); 673 | }); 674 | }; 675 | 676 | DbHelper.prototype._filterSelected = function(cursor, criteria) { 677 | // filter to selected fields 678 | return cursor.map((v) => { 679 | debug('_filterSelected v:', v); 680 | if (criteria.select && criteria.select.length > 0) { 681 | let nv = {d: {}}; 682 | _.each(criteria.joins, function(join) { 683 | nv[join.alias] = v[join.alias]; 684 | }); 685 | 686 | ['_id', '_key', '_rev'].forEach((k) => { nv.d[k] = v.d[k]; }); 687 | 688 | criteria.select.forEach((sk) => { 689 | nv.d[sk] = v.d[sk]; 690 | }); 691 | debug('nv:', nv); 692 | return nv; 693 | } 694 | return v; 695 | }); 696 | }; 697 | 698 | /* 699 | * Perform simple join 700 | */ 701 | DbHelper.prototype.join = function(collection, criteria, cb) { 702 | debug('join collection:', collection, 'criteria:', criteria); 703 | debug('criteria.joins:', criteria.joins); 704 | const from = criteria.joins[0]; 705 | const aliasAttrs = this.collections[collection]._attributes[from.alias]; 706 | debug(`alias ${from.alias} attrs:`, aliasAttrs); 707 | 708 | var me = this, 709 | join_query; 710 | 711 | if (!aliasAttrs.edge) { // Standard waterline Join? 712 | debug('waterline join'); 713 | var q = aqb.for(collection).in(collection); 714 | var mergeObj = {}; 715 | _.each(criteria.joins, function(join) { 716 | q = q 717 | .for(join.parentKey) 718 | .in(join.child) 719 | .filter(aqb.eq(`${join.parentKey}.${join.childKey}`, `${join.parent}.${join.parentKey}`)); 720 | mergeObj[join.parentKey] = join.parentKey; 721 | }); 722 | q = q.return(aqb.MERGE(collection, mergeObj)); 723 | var q_d = aqb.for('d').in(q); 724 | var q_opts = this.optionsToQuery(collection, criteria, q_d); 725 | join_query = me.applyFunctions(criteria, q_opts); 726 | 727 | } else { // graph 728 | debug('edge join'); 729 | const edgeCollection = aliasAttrs.edge; 730 | let edgeJoin = {}; 731 | 732 | // TODO: Use above AQB approach with edges too 733 | 734 | var qb = this.optionsToQuery(collection, criteria).toAQL(), 735 | ret = ' RETURN { "d" : d'; 736 | 737 | _.each(criteria.joins, function(join, i) { 738 | debug('join each i:', i, 'join:', join); 739 | 740 | // skip waterline implied junction tables 741 | if (i % 2 === 0) { // even? 742 | edgeJoin = join; 743 | return; 744 | 745 | } else { // odd? 746 | edgeJoin.edgeChild = join.child; 747 | } 748 | 749 | var _id; 750 | if (criteria.where) { 751 | _id = criteria.where._id; // always _id for edges 752 | if (!_id) 753 | _id = criteria.where._key; 754 | } 755 | debug('join criteria _id field:', _id); 756 | 757 | ret += ', "' + edgeJoin.alias + '" : (FOR ' + edgeJoin.alias + ' IN ANY ' + aqb.str(_id).toAQL() + ' ' + 758 | edgeCollection + 759 | ' OPTIONS {bfs: true, uniqueVertices: true} FILTER IS_SAME_COLLECTION("' +edgeJoin.edgeChild + '", ' + edgeJoin.alias + ') RETURN ' + edgeJoin.alias + ')'; 760 | }); 761 | ret += ' }'; 762 | join_query = qb + ret; 763 | } 764 | 765 | debug('join query:', join_query); 766 | this.db.query(join_query, function(err, cursor) { 767 | if (err) return cb(err); 768 | 769 | debug('join() criteria.select:', criteria.select); 770 | 771 | me._filterSelected(cursor, criteria) 772 | .then((vals) => { 773 | debug('query join response:', vals.length, 774 | 'documents returned for query:', 775 | (typeof join_query === 'string' ? join_query : join_query.toAQL())); 776 | debug('vals[0]:', vals[0]); 777 | return cb(null, _.map(vals, function(item) { 778 | var bo = item.d; 779 | 780 | if (aliasAttrs.edge) { 781 | _.each(criteria.joins, function(join) { 782 | if (!criteria.select || criteria.select.includes(join.alias)) { 783 | bo[join.alias] = _.map(item[join.alias], function(i) { 784 | return i; 785 | }); 786 | } 787 | }); 788 | } 789 | return bo; 790 | })); 791 | }) 792 | .catch((err) => { 793 | console.error('join() error:', err); 794 | cb(err, null); 795 | }); 796 | 797 | }); 798 | }; 799 | 800 | /* 801 | * Creates edge between two vertices pointed by from and to 802 | */ 803 | var toArangoRef = function(val) { 804 | var ret = val; 805 | if (typeof ret === 'object') { 806 | ret = ret._id.split('/', 2); 807 | } else { 808 | ret = ret.split('/', 2); 809 | } 810 | return ret; 811 | }; 812 | 813 | DbHelper.prototype.createEdge = function(from, to, options, cb) { 814 | var src = toArangoRef(from), 815 | dst = toArangoRef(to), 816 | srcAttr; 817 | 818 | srcAttr = _.find(this.collections[src[0]].attributes, function(i) { 819 | return i.collection == dst[0]; 820 | }); 821 | 822 | // create edge 823 | this.graph.edgeCollection(srcAttr.edge, 824 | function(err, collection) { 825 | if (err) return cb(err); 826 | 827 | collection.save((options.data ? options.data : {}), 828 | src.join('/'), 829 | dst.join('/'), 830 | function(err, edge) { 831 | if (err) return cb(err); 832 | cb(null, edge); 833 | }); 834 | }); 835 | }; 836 | 837 | /* 838 | * Removes edges between two vertices pointed by from and to 839 | */ 840 | DbHelper.prototype.deleteEdges = function(from, to, options, cb) { 841 | var src = toArangoRef(from), 842 | dst = toArangoRef(to), 843 | srcAttr; 844 | 845 | srcAttr = _.find(this.collections[src[0]].attributes, function(i) { 846 | return i.collection == dst[0]; 847 | }); 848 | 849 | // delete edge 850 | this.db.collection(srcAttr.edge, 851 | function(err, collection) { 852 | if (err) return cb(err); 853 | 854 | collection.edges(src.join('/'), function(err, edges) { 855 | var dErr = err; 856 | if (err) return cb(err); 857 | _.each(edges, function(i) { 858 | collection.remove(i._id, function(err, edge) { 859 | dErr = err; 860 | }); 861 | }); 862 | if (dErr !== null) { 863 | return cb(dErr); 864 | } 865 | cb(null, edges); 866 | }); 867 | }); 868 | }; 869 | 870 | /** 871 | * Create a named graph 872 | * 873 | * @function 874 | * @name createGraph 875 | * @param {string} connectionName Connection name 876 | * @param {string} graphName Graph name 877 | * @param {array} edgeDefs Array of edge definitions 878 | * @param {function} cb Optional Callback (err, res) 879 | * @returns {Promise} 880 | * 881 | * example of edgeDefs: 882 | * ``` 883 | * [{ 884 | * collection: 'edges', 885 | * from: ['start-vertices'], 886 | * to: ['end-vertices'] 887 | * }, ...] 888 | * ``` 889 | */ 890 | DbHelper.prototype.createGraph = function(db, graphName, edgeDefs, cb){ 891 | debug('createGraph() graphName:', graphName, 892 | 'edgeDefs:', edgeDefs, 893 | 'cb:', cb); 894 | 895 | var graph = db.graph(graphName); 896 | return graph.create({ 897 | edgeDefinitions: edgeDefs 898 | }) 899 | .then((res) => { 900 | if (cb) { 901 | cb(null, res); 902 | } 903 | return Promise.resolve(res); 904 | }) 905 | .catch((err) => { 906 | if (cb) { 907 | cb(err); 908 | } 909 | return Promise.reject(err); 910 | }); 911 | }; 912 | 913 | var connect = function(connection, collections) { 914 | // if an active connection exists, use 915 | // it instead of tearing the previous 916 | // one down 917 | var d = Q.defer(); 918 | 919 | try { 920 | var db = getDb(connection); 921 | var graph = getGraph(db, connection); 922 | var helper = new DbHelper(db, graph, collections, connection); 923 | 924 | helper.registerCollections().then(function(classes, err) { 925 | d.resolve(helper); 926 | }); 927 | } catch (err) { 928 | console.error('An error has occured when trying to connect to ArangoDB:', err); 929 | d.reject(err); 930 | throw err; 931 | } 932 | return d.promise; 933 | }; 934 | 935 | return { 936 | create: function(connection, collections) { 937 | return connect(connection, collections); 938 | } 939 | }; 940 | })(); 941 | -------------------------------------------------------------------------------- /lib/processor.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, esversion:6 */ 2 | 'use strict'; 3 | 4 | var _ = require('lodash'); 5 | var debug = require('debug')('sails-arangodb:processor'); 6 | 7 | /** 8 | * Processes data returned from a AQL query. 9 | * Taken and modified from https://github.com/balderdashy/sails-postgresql/blob/master/lib/processor.js 10 | * @module 11 | * @name processor 12 | */ 13 | var Processor = module.exports = function Processor(schema) { 14 | this.schema = _.cloneDeep(schema); 15 | return this; 16 | }; 17 | 18 | /** 19 | * Cast special values to proper types. 20 | * 21 | * Ex: Array is stored as "[0,1,2,3]" and should be cast to proper 22 | * array for return values. 23 | */ 24 | 25 | Processor.prototype.cast = function(collectionName, result) { 26 | 27 | var self = this; 28 | var _result = _.cloneDeep(result._result); 29 | 30 | // TODO: go deep in results for value casting 31 | 32 | if (_result !== undefined) { 33 | if (_.isArray(_result)) { 34 | debug('cast() _result:', _result); 35 | // _result is in the following form: 36 | // [ { name: 'Gab', 37 | // createdAt: '2015-11-26T01:09:44.197Z', 38 | // updatedAt: '2015-11-26T01:09:44.197Z', 39 | // username: 'gab-arango', 40 | // _id: 'user/4715390689', 41 | // _rev: '5874132705', 42 | // _key: '4715390689' } ] 43 | _result.forEach(function(r) { 44 | debug('cast() r:', r); 45 | if (r._key) { 46 | r.id = r._key; 47 | } 48 | Object.keys(r).forEach(function(key) { 49 | self.castValue(collectionName, key, r[key], r); 50 | }); 51 | }); 52 | } else { 53 | // cast single document 54 | _result.id = _result._key; 55 | Object.keys(_result).forEach(function(key) { 56 | self.castValue(collectionName, key, _result[key], _result); 57 | }); 58 | } 59 | } 60 | 61 | return _result; 62 | }; 63 | 64 | /** 65 | * Cast a value 66 | * 67 | * @param {String} key 68 | * @param {Object|String|Integer|Array} value 69 | * @param {Object} schema 70 | * @api private 71 | */ 72 | 73 | Processor.prototype.castValue = function(table, key, value, attributes) { 74 | debug('castValue: table:', table, 'key:', key); 75 | 76 | var self = this; 77 | var identity = table; 78 | var attr; 79 | 80 | // Check for a columnName, serialize so we can do any casting 81 | if (this.schema[identity]) { 82 | Object.keys(this.schema[identity]._attributes).forEach(function(attribute) { 83 | if(self.schema[identity]._attributes[attribute].columnName === key) { 84 | attr = attribute; 85 | return; 86 | } 87 | }); 88 | } 89 | 90 | if(!attr) attr = key; 91 | 92 | // Lookup Schema "Type" 93 | if(!this.schema[identity] || !this.schema[identity]._attributes[attr]) return; 94 | var type; 95 | 96 | if(!_.isPlainObject(this.schema[identity]._attributes[attr])) { 97 | type = this.schema[identity]._attributes[attr]; 98 | } else { 99 | type = this.schema[identity]._attributes[attr].type; 100 | } 101 | 102 | debug(`castValue() field: ${key} has type ${type}`); 103 | 104 | if(!type) return; 105 | 106 | switch(type) { 107 | case 'array': 108 | try { 109 | // Attempt to parse Array 110 | attributes[key] = JSON.parse(value); 111 | } catch(e) { 112 | return; 113 | } 114 | break; 115 | case 'date': 116 | case 'datetime': 117 | attributes[key] = new Date(attributes[key]); 118 | break; 119 | } 120 | debug('cast type?:', attributes[key] ? attributes[key].constructor : 'undefined', 'value:', attributes[key]); 121 | }; 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-arangodb", 3 | "version": "0.3.0", 4 | "description": "Arangodb adapter for Sails / Waterline", 5 | "main": "./lib/adapter.js", 6 | "scripts": { 7 | "test": "gulp waterline; gulp test", 8 | "lint": "jshint lib/*.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/gabriel-letarte/sails-arangodb.git" 13 | }, 14 | "keywords": [ 15 | "arangodb", 16 | "adapter", 17 | "sails", 18 | "waterline", 19 | "sails.js", 20 | "plugin" 21 | ], 22 | "author": "Gabriel Letarte ", 23 | "license": "MIT", 24 | "readmeFilename": "README.md", 25 | "dependencies": { 26 | "aqb": ">=1.8.3", 27 | "arangojs": "5.x.x", 28 | "async": "^0.9.0", 29 | "debug": "^2.3.3", 30 | "lodash": ">=3.10.1", 31 | "q": "^1.0.1" 32 | }, 33 | "devDependencies": { 34 | "captains-log": "~0.11.0", 35 | "del": "^2.2.2", 36 | "gulp": "^3.9.1", 37 | "gulp-batch": "^1.0.5", 38 | "gulp-jsdoc3": "^1.0.1", 39 | "gulp-load-plugins": "^1.5.0", 40 | "gulp-mocha": "^4.1.0", 41 | "gulp-watch": "^4.3.11", 42 | "jshint": "^2.6.3", 43 | "mocha": "*", 44 | "should": "^11.2.1", 45 | "waterline": "^0.11.11", 46 | "waterline-adapter-tests": "~0.11.1" 47 | }, 48 | "waterlineAdapter": { 49 | "type": "arangodb", 50 | "interfaces": [ 51 | "semantic", 52 | "queryable" 53 | ], 54 | "waterlineVersion": "~0.11.0" 55 | }, 56 | "jshintConfig": { 57 | "bitwise": true, 58 | "curly": false, 59 | "eqeqeq": false, 60 | "latedef": true, 61 | "shadow": "inner", 62 | "undef": true, 63 | "unused": "vars", 64 | "node": true, 65 | "sub": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | test.json -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Running waterline adapter compliance tests 2 | 3 | copy test-example.json to test.json and edit appropriately for your instance of arangodb, e.g.: 4 | 5 | ``` 6 | { 7 | "host": "localhost", 8 | "port": 8529, 9 | "user": "test", 10 | "password": "test", 11 | "database": "test" 12 | } 13 | ``` 14 | 15 | and run: 16 | ``` 17 | $ npm test 18 | ``` -------------------------------------------------------------------------------- /test/adapter.test.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, esversion:6 */ 2 | 'use strict'; 3 | 4 | /*global describe, before, it, after, beforeEach, afterEach */ 5 | const assert = require('assert'); 6 | const should = require('should'); 7 | 8 | const Database = require('arangojs'); 9 | 10 | const Waterline = require('waterline'); 11 | const orm = new Waterline(); 12 | const adapter = require('../'); 13 | const config = require("./test.json"); 14 | config.adapter = 'arangodb'; 15 | 16 | const Users_1 = require('./models/users_1'); 17 | const Pets_1 = require('./models/pets_1'); 18 | const Profiles_1 = require('./models/profiles_1'); 19 | const Users_Profiles_Graph = require('./models/users_profiles_graph'); 20 | const Users_Users_Graph = require('./models/users_users_graph'); 21 | 22 | const waterline_config = { 23 | adapters: { 24 | 'default': adapter, 25 | arangodb: adapter 26 | }, 27 | connections: { 28 | arangodb: config 29 | }, 30 | defaults: { 31 | } 32 | }; 33 | 34 | let db, 35 | models, 36 | connections, 37 | savePetId, 38 | saveId; 39 | 40 | describe('adapter', function () { 41 | 42 | before(function (done) { 43 | 44 | const dbUrl = `http://${config.user}:${config.password}@${config.host}:${config.port}`; 45 | const sys_db = new Database({ 46 | url: dbUrl, 47 | databaseName: '_system' 48 | }); 49 | 50 | sys_db.dropDatabase(config.database) 51 | .then(() => { 52 | orm.loadCollection(Users_1); 53 | orm.loadCollection(Pets_1); 54 | orm.loadCollection(Profiles_1); 55 | orm.loadCollection(Users_Profiles_Graph); 56 | orm.loadCollection(Users_Users_Graph); 57 | orm.initialize(waterline_config, (err, o) => { 58 | if (err) { 59 | return done(err); 60 | } 61 | models = o.collections; 62 | connections = o.connections; 63 | 64 | connections.arangodb._adapter.getDB('arangodb', '', (n_db) => { 65 | db = n_db; 66 | done(); 67 | }); 68 | }); 69 | }) 70 | .catch((err) => { 71 | done(err); 72 | }); 73 | }); 74 | 75 | after(function () { 76 | orm.teardown(); 77 | }); 78 | 79 | describe('connection', function () { 80 | it('should establish a connection', () => { 81 | connections.should.ownProperty('arangodb'); 82 | }); 83 | }); 84 | 85 | describe('methods', function () { 86 | it('should create a new document in pets', (done) => { 87 | models.pets_1.create({name: 'Woof'}) 88 | .then((pet) => { 89 | should.exist(pet); 90 | pet.should.have.property('id'); 91 | pet.should.have.property('_key'); 92 | pet.should.have.property('_rev'); 93 | pet.should.have.property('name'); 94 | pet.should.have.property('createdAt'); 95 | pet.should.have.property('updatedAt'); 96 | pet.name.should.equal('Woof'); 97 | savePetId = pet.id; 98 | done(); 99 | }) 100 | .catch((err) => { 101 | done(err); 102 | }); 103 | }); 104 | 105 | it('should find previously created pet by id', (done) => { 106 | models.pets_1.find({id: savePetId}) 107 | .then((pets) => { 108 | should.exist(pets); 109 | pets.should.be.an.Array(); 110 | pets.length.should.equal(1); 111 | const pet = pets[0]; 112 | pet.should.have.property('id'); 113 | pet.should.have.property('_key'); 114 | pet.should.have.property('_rev'); 115 | pet.should.have.property('name'); 116 | pet.should.have.property('createdAt'); 117 | pet.should.have.property('updatedAt'); 118 | pet.name.should.equal('Woof'); 119 | done(); 120 | }) 121 | .catch((err) => { 122 | done(err); 123 | }); 124 | }); 125 | 126 | it('should create a new document in users', (done) => { 127 | models.users_1.create({name: 'Fred Blogs', pet: savePetId, second: 'match'}) 128 | .then((user) => { 129 | should.exist(user); 130 | user.should.have.property('id'); 131 | user.should.have.property('_key'); 132 | user.should.have.property('_rev'); 133 | user.should.have.property('name'); 134 | user.should.have.property('createdAt'); 135 | user.should.have.property('updatedAt'); 136 | user.name.should.equal('Fred Blogs'); 137 | saveId = user.id; 138 | done(); 139 | }) 140 | .catch((err) => { 141 | done(err); 142 | }); 143 | }); 144 | 145 | it('should find previously created user by id', (done) => { 146 | models.users_1.find({id: saveId}) 147 | .then((users) => { 148 | should.exist(users); 149 | users.should.be.an.Array(); 150 | users.length.should.equal(1); 151 | const user = users[0]; 152 | user.should.have.property('id'); 153 | user.should.have.property('_key'); 154 | user.should.have.property('_rev'); 155 | user.should.have.property('name'); 156 | user.should.have.property('createdAt'); 157 | user.should.have.property('updatedAt'); 158 | user.name.should.equal('Fred Blogs'); 159 | done(); 160 | }) 161 | .catch((err) => { 162 | done(err); 163 | }); 164 | }); 165 | 166 | it('should find user by name', (done) => { 167 | models.users_1.find({name: 'Fred Blogs'}) 168 | .then((users) => { 169 | should.exist(users); 170 | users.should.be.an.Array(); 171 | users.length.should.equal(1); 172 | const user = users[0]; 173 | user.name.should.equal('Fred Blogs'); 174 | done(); 175 | }) 176 | .catch((err) => { 177 | done(err); 178 | }); 179 | }); 180 | 181 | it('should find user by name (case insensitive)', (done) => { 182 | models.users_1.find({name: {contains: 'fred blogs', caseSensitive: false}}) 183 | .then((users) => { 184 | should.exist(users); 185 | users.should.be.an.Array(); 186 | users.length.should.equal(1); 187 | const user = users[0]; 188 | user.name.should.equal('Fred Blogs'); 189 | done(); 190 | }) 191 | .catch((err) => { 192 | done(err); 193 | }); 194 | }); 195 | 196 | it('should find user by name (case sensitive)', (done) => { 197 | models.users_1.find({name: {contains: 'Fred Blogs', caseSensitive: true}}) 198 | .then((users) => { 199 | should.exist(users); 200 | users.should.be.an.Array(); 201 | users.length.should.equal(1); 202 | const user = users[0]; 203 | user.name.should.equal('Fred Blogs'); 204 | done(); 205 | }) 206 | .catch((err) => { 207 | done(err); 208 | }); 209 | }); 210 | 211 | it('should find user by name (case insensitive by default)', (done) => { 212 | models.users_1.find({name: {contains: 'fred blogs'}}) 213 | .then((users) => { 214 | should.exist(users); 215 | users.should.be.an.Array(); 216 | users.length.should.equal(1); 217 | const user = users[0]; 218 | user.name.should.equal('Fred Blogs'); 219 | done(); 220 | }) 221 | .catch((err) => { 222 | done(err); 223 | }); 224 | }); 225 | 226 | it('should fail to find user by name (case sensitive)', (done) => { 227 | models.users_1.find({name: {contains: 'fred blogs', caseSensitive: true}}) 228 | .then((users) => { 229 | should.exist(users); 230 | users.should.be.an.Array(); 231 | users.length.should.equal(0); 232 | done(); 233 | }) 234 | .catch((err) => { 235 | done(err); 236 | }); 237 | }); 238 | 239 | it('should update the user name by id', (done) => { 240 | models.users_1.update(saveId, {name: 'Joe Blogs'}) 241 | .then((users) => { 242 | should.exist(users); 243 | users.should.be.an.Array(); 244 | users.length.should.equal(1); 245 | const user = users[0]; 246 | user.name.should.equal('Joe Blogs'); 247 | user.should.have.property('id'); 248 | done(); 249 | }) 250 | .catch((err) => { 251 | done(err); 252 | }); 253 | }); 254 | 255 | it('should update the user name by name search', (done) => { 256 | models.users_1.update({name: 'Joe Blogs'}, {name: 'Joseph Blogs'}) 257 | .then((users) => { 258 | should.exist(users); 259 | users.should.be.an.Array(); 260 | users.length.should.equal(1); 261 | const user = users[0]; 262 | user.name.should.equal('Joseph Blogs'); 263 | done(); 264 | }) 265 | .catch((err) => { 266 | done(err); 267 | }); 268 | }); 269 | 270 | it('should update the user name by name search (case insensitive)', (done) => { 271 | models.users_1.update({name: {contains: 'joseph blogs', caseSensitive: false}}, {name: 'joseph blogs'}) 272 | .then((users) => { 273 | should.exist(users); 274 | users.should.be.an.Array(); 275 | users.length.should.equal(1); 276 | const user = users[0]; 277 | user.name.should.equal('joseph blogs'); 278 | done(); 279 | }) 280 | .catch((err) => { 281 | done(err); 282 | }); 283 | }); 284 | 285 | it('should support complex objects on update', (done) => { 286 | var complex = { 287 | age: 100, 288 | name: 'Fred', 289 | profile: { 290 | nested1: { 291 | value: 50, 292 | nested2: { 293 | another_value1: 60, 294 | another_value2: 70 295 | } 296 | } 297 | } 298 | }; 299 | models.users_1.update(saveId, {complex: complex}) 300 | .then((users) => { 301 | should.exist(users); 302 | users.should.be.an.Array(); 303 | users.length.should.equal(1); 304 | const user = users[0]; 305 | user.complex.should.eql(complex); 306 | complex.age.should.equal(100); 307 | complex.profile.nested1.nested2.another_value2.should.equal(70); 308 | done(); 309 | }) 310 | .catch((err) => { 311 | done(err); 312 | }); 313 | }); 314 | 315 | it('should select a top-level field using select', (done) => { 316 | models.users_1.find({select: ['complex']}) 317 | .then((users) => { 318 | should.exist(users); 319 | users.should.be.an.Array(); 320 | users.length.should.equal(1); 321 | const user = users[0]; 322 | should.not.exist(users[0].name); 323 | should.exist(users[0].complex); 324 | done(); 325 | }) 326 | .catch((err) => { 327 | done(err); 328 | }); 329 | }); 330 | 331 | // it('should select a top-level field using fields', (done) => { 332 | // models.users_1.find({}, {fields: {complex: 1}}) 333 | // .then((users) => { 334 | // console.log('users: users:', users); 335 | // should.exist(users); 336 | // users.should.be.an.Array(); 337 | // users.length.should.equal(1); 338 | // const user = users[0]; 339 | // should.not.exist(users[0].name); 340 | // should.exist(users[0].complex); 341 | // done(); 342 | // }); 343 | // }); 344 | 345 | // it('should select a 2nd-level field', (done) => { 346 | // models.users_1.find({select: ['complex.age']}) 347 | // .then((users) => { 348 | // console.log('users: users:', users); 349 | // should.exist(users); 350 | // users.should.be.an.Array(); 351 | // users.length.should.equal(1); 352 | // const user = users[0]; 353 | // should.not.exist(users[0].name); 354 | // should.exist(users[0].complex); 355 | // done(); 356 | // }); 357 | // }); 358 | 359 | it('should find by nested where clause', (done) => { 360 | models.users_1.find({complex: {age: 100}}) 361 | .then((users) => { 362 | should.exist(users); 363 | users.should.be.an.Array(); 364 | users.length.should.equal(1); 365 | const user = users[0]; 366 | user.complex.age.should.equal(100); 367 | done(); 368 | }) 369 | .catch((err) => { 370 | done(err); 371 | }); 372 | }); 373 | 374 | it('should find by nested where clause with contains filter', (done) => { 375 | models.users_1.find({complex: {name: {contains: 'Fr'}}}) 376 | .then((users) => { 377 | should.exist(users); 378 | users.should.be.an.Array(); 379 | users.length.should.equal(1); 380 | const user = users[0]; 381 | user.complex.name.should.equal('Fred'); 382 | done(); 383 | }) 384 | .catch((err) => { 385 | done(err); 386 | }); 387 | }); 388 | 389 | it('should find by nested where clause with contains filter (case insensitive)', (done) => { 390 | models.users_1.find({complex: {name: {caseSensitive: false, contains: 'fr'}}}) 391 | .then((users) => { 392 | should.exist(users); 393 | users.should.be.an.Array(); 394 | users.length.should.equal(1); 395 | const user = users[0]; 396 | user.complex.name.should.equal('Fred'); 397 | done(); 398 | }) 399 | .catch((err) => { 400 | done(err); 401 | }); 402 | }); 403 | 404 | it('should find using 2 fields (AND)', (done) => { 405 | models.users_1.find({name: 'joseph blogs', second: 'match'}) 406 | .then((users) => { 407 | should.exist(users); 408 | users.should.be.an.Array(); 409 | users.length.should.equal(1); 410 | const user = users[0]; 411 | user.complex.name.should.equal('Fred'); 412 | user.second.should.equal('match'); 413 | done(); 414 | }) 415 | .catch((err) => { 416 | done(err); 417 | }); 418 | }); 419 | 420 | it('should find using 2 fields (AND) w/complex second field', (done) => { 421 | models.users_1.find({name: 'joseph blogs', complex: {name: {contains: 'fr'}}}) 422 | .then((users) => { 423 | should.exist(users); 424 | users.should.be.an.Array(); 425 | users.length.should.equal(1); 426 | const user = users[0]; 427 | user.complex.name.should.equal('Fred'); 428 | user.second.should.equal('match'); 429 | done(); 430 | }) 431 | .catch((err) => { 432 | done(err); 433 | }); 434 | }); 435 | 436 | it('should find using 2 contains fields (AND)', (done) => { 437 | models.users_1.find({name: {contains: 'joseph'}, second: {contains: 'mat'}}) 438 | .then((users) => { 439 | should.exist(users); 440 | users.should.be.an.Array(); 441 | users.length.should.equal(1); 442 | const user = users[0]; 443 | user.complex.name.should.equal('Fred'); 444 | user.second.should.equal('match'); 445 | done(); 446 | }) 447 | .catch((err) => { 448 | done(err); 449 | }); 450 | }); 451 | 452 | it('should find using 2 contains fields (AND) w/complex second field', (done) => { 453 | models.users_1.find({name: {contains: 'joseph'}, complex: {name: {contains: 'fr'}}}) 454 | .then((users) => { 455 | should.exist(users); 456 | users.should.be.an.Array(); 457 | users.length.should.equal(1); 458 | const user = users[0]; 459 | user.complex.name.should.equal('Fred'); 460 | user.second.should.equal('match'); 461 | done(); 462 | }) 463 | .catch((err) => { 464 | done(err); 465 | }); 466 | }); 467 | 468 | it('join should populate pet', (done) => { 469 | models.users_1.find({}) 470 | .populate('pet') 471 | .then((users) => { 472 | should.exist(users); 473 | users.should.be.an.Array(); 474 | users.length.should.equal(1); 475 | const user = users[0]; 476 | should.exist(user.pet); 477 | done(); 478 | }) 479 | .catch((err) => { 480 | done(err); 481 | }); 482 | }); 483 | 484 | it('join should populate pet and find by pet name', (done) => { 485 | models.users_1.find({pet: {name: 'Woof'}}) 486 | .populate('pet') 487 | .then((users) => { 488 | should.exist(users); 489 | users.should.be.an.Array(); 490 | users.length.should.equal(1); 491 | const user = users[0]; 492 | should.exist(user.pet); 493 | done(); 494 | }) 495 | .catch((err) => { 496 | done(err); 497 | }); 498 | }); 499 | 500 | it('should not return documents with undefined field with <=', (done) => { 501 | models.users_1.find({notexists: {'lessThanOrEqual': 10}}) 502 | .then((users) => { 503 | should.exist(users); 504 | users.should.be.an.Array(); 505 | users.length.should.equal(0); 506 | done(); 507 | }) 508 | .catch((err) => { 509 | done(err); 510 | }); 511 | }); 512 | 513 | it('should not return documents with undefined field with <', (done) => { 514 | models.users_1.find({notexists: {'lessThan': 10}}) 515 | .then((users) => { 516 | should.exist(users); 517 | users.should.be.an.Array(); 518 | users.length.should.equal(0); 519 | done(); 520 | }) 521 | .catch((err) => { 522 | done(err); 523 | }); 524 | }); 525 | 526 | describe('delete', () => { 527 | beforeEach((done) => { 528 | db.collection('users_1').truncate() 529 | .then(() => { 530 | return models.users_1.create({name: 'Don\'t Delete Me', pet: savePetId, second: 'match'}); 531 | }) 532 | .then(() => { 533 | models.users_1.create({name: 'Delete Me', pet: savePetId, second: 'match'}) 534 | .then((user) => { 535 | done(); 536 | }) 537 | .catch((err) => { 538 | done(err); 539 | }); 540 | }); 541 | }); 542 | 543 | it('should delete entry by name', (done) => { 544 | models.users_1 545 | .destroy({name: 'Delete Me'}) 546 | .then((deleted) => { 547 | should.exist(deleted); 548 | deleted.should.be.an.Array(); 549 | deleted.length.should.equal(1); 550 | deleted[0].name.should.equal('Delete Me'); 551 | done(); 552 | }) 553 | .catch((err) => { 554 | done(err); 555 | }); 556 | }); 557 | 558 | it('should delete entry in array', (done) => { 559 | models.users_1 560 | .destroy({name: ['Me', 'Delete Me']}) 561 | .then((deleted) => { 562 | should.exist(deleted); 563 | deleted.should.be.an.Array(); 564 | deleted.length.should.equal(1); 565 | deleted[0].name.should.equal('Delete Me'); 566 | done(); 567 | }) 568 | .catch((err) => { 569 | done(err); 570 | }); 571 | }); 572 | 573 | it('should delete entry not in (with v0.11 !) array', (done) => { 574 | models.users_1 575 | .destroy({name: {'!': ['Me', 'Don\'t Delete Me']}}) 576 | .then((deleted) => { 577 | should.exist(deleted); 578 | deleted.should.be.an.Array(); 579 | deleted.length.should.equal(1); 580 | deleted[0].name.should.equal('Delete Me'); 581 | done(); 582 | }) 583 | .catch((err) => { 584 | done(err); 585 | }); 586 | }); 587 | 588 | it('should delete entry not in (with v0.12 nin) array', (done) => { 589 | models.users_1 590 | .destroy({name: {nin: ['Me', 'Don\'t Delete Me']}}) 591 | .then((deleted) => { 592 | should.exist(deleted); 593 | deleted.should.be.an.Array(); 594 | deleted.length.should.equal(1); 595 | deleted[0].name.should.equal('Delete Me'); 596 | done(); 597 | }) 598 | .catch((err) => { 599 | done(err); 600 | }); 601 | }); 602 | }); // delete 603 | 604 | // adapted from waterline tests 605 | describe('greaterThanOrEqual (>=)', function() { 606 | describe('dates', function() { 607 | 608 | ///////////////////////////////////////////////////// 609 | // TEST SETUP 610 | //////////////////////////////////////////////////// 611 | 612 | var testName = 'greaterThanOrEqual dates test'; 613 | 614 | before(function(done) { 615 | // Insert 10 Users 616 | var users = [], 617 | date; 618 | 619 | for(var i=0; i<10; i++) { 620 | date = new Date(2013,10,1); 621 | date.setDate(date.getDate() + i); 622 | 623 | users.push({ 624 | name: 'required, but ignored in this test', 625 | first_name: 'greaterThanOrEqual_dates_user' + i, 626 | type: testName, 627 | dob: date 628 | }); 629 | } 630 | 631 | models.users_1.createEach(users, function(err, users) { 632 | if(err) return done(err); 633 | done(); 634 | }); 635 | }); 636 | 637 | ///////////////////////////////////////////////////// 638 | // TEST METHODS 639 | //////////////////////////////////////////////////// 640 | 641 | it('should return records with greaterThanOrEqual key when searching dates', function(done) { 642 | models.users_1.find({ type: testName, dob: { greaterThanOrEqual: new Date(2013, 10, 9) }}).sort('first_name').exec(function(err, users) { 643 | assert.ifError(err); 644 | assert(Array.isArray(users)); 645 | assert.strictEqual(users.length, 2); 646 | assert.equal(users[0].first_name, 'greaterThanOrEqual_dates_user8'); 647 | done(); 648 | }); 649 | }); 650 | 651 | it('should return records with symbolic usage >= usage when searching dates', function(done) { 652 | models.users_1.find({ type: testName, dob: { '>=': new Date(2013, 10, 9) }}).sort('first_name').exec(function(err, users) { 653 | assert.ifError(err); 654 | assert(Array.isArray(users)); 655 | assert.strictEqual(users.length, 2); 656 | assert.equal(users[0].first_name, 'greaterThanOrEqual_dates_user8'); 657 | done(); 658 | }); 659 | }); 660 | 661 | it('should return records with symbolic usage >= usage when searching dates as ISO strings', function(done) { 662 | var dateString = new Date(2013,10,9); 663 | // dateString = dateString.toString(); 664 | dateString = dateString.toISOString(); 665 | models.users_1.find({ type: testName, dob: { '>=': dateString }}).sort('first_name').exec(function(err, users) { 666 | assert.ifError(err); 667 | assert(Array.isArray(users)); 668 | assert.strictEqual(users.length, 2); 669 | assert.equal(users[0].first_name, 'greaterThanOrEqual_dates_user8'); 670 | done(); 671 | }); 672 | }); 673 | 674 | }); 675 | }); 676 | 677 | describe('update', function () { 678 | let id; 679 | beforeEach(function (done) { 680 | var user = { 681 | name: 'update test', 682 | first_name: 'update', 683 | type: 'update test' 684 | }; 685 | models.users_1.create(user) 686 | .then((user) => { 687 | id = user.id; 688 | done(); 689 | }); 690 | }); 691 | 692 | afterEach(function (done) { 693 | models.users_1.destroy(id) 694 | .then(() => { 695 | done(); 696 | }); 697 | }); 698 | 699 | it('should merge on update by default', function (done) { 700 | models.users_1.update({ 701 | id: id 702 | }, {newfield: 'merged'}) 703 | .then((updated) => { 704 | return models.users_1.find(id) 705 | .then((docs) => { 706 | const doc = docs[0]; 707 | should.exist(doc); 708 | should.exist(doc.createdAt); 709 | should.exist(doc.name); 710 | done(); 711 | }); 712 | }) 713 | .catch((err) => { 714 | done(err); 715 | }); 716 | }); 717 | 718 | it('should replace on update', function (done) { 719 | models.users_1.update({ 720 | id: id, 721 | $replace: true 722 | }, {newfield: 'replaced'}) 723 | .then((updated) => { 724 | return models.users_1.find(id) 725 | .then((docs) => { 726 | const doc = docs[0]; 727 | should.exist(doc); 728 | should.exist(doc.createdAt); 729 | should.not.exist(doc.name); 730 | done(); 731 | }); 732 | }) 733 | .catch((err) => { 734 | done(err); 735 | }); 736 | }); 737 | }); 738 | 739 | }); // methods 740 | 741 | 742 | //////////////////// 743 | // Graphs 744 | 745 | describe('Graphs', () => { 746 | let profile_id, 747 | user_id, 748 | edge_id; 749 | 750 | before((done) => { 751 | models.profiles_1.create({url: 'http://gravitar...'}) 752 | .then((profile) => { 753 | profile_id = profile.id; 754 | return models.users_1.create({name: 'Graph User'}) 755 | .then((user) => { 756 | user_id = user.id; 757 | done(); 758 | }); 759 | }) 760 | .catch((err) => { 761 | done(err); 762 | }); 763 | }); 764 | 765 | describe('createEdge as anonymous graph', () => { 766 | it('should create an edge', (done) => { 767 | models.users_1.createEdge('profileOf', user_id, profile_id, { 768 | test_attr1: 'edge attr value 1' 769 | }) 770 | .then((res) => { 771 | res.should.have.property('_id'); 772 | res.should.have.property('_key'); 773 | res.should.have.property('_rev'); 774 | edge_id = res._id; 775 | done(); 776 | }) 777 | .catch((err) => { 778 | done(err); 779 | }); 780 | }); 781 | }); 782 | 783 | describe('neighbors', () => { 784 | it('should return the neighbor (profile) of the user (anonymous graph)', (done) => { 785 | models.users_1.neighbors(user_id, ['profileOf']) 786 | .then((res) => { 787 | should.exist(res); 788 | res.should.be.an.Array(); 789 | res.length.should.equal(1); 790 | 791 | const n = res[0]; 792 | should.exist(n); 793 | n.should.have.property('_id'); 794 | n.should.have.property('_key'); 795 | n.should.have.property('_rev'); 796 | n.should.have.property('url'); 797 | n._id.should.equal(profile_id); 798 | done(); 799 | }) 800 | .catch((err) => { 801 | done(err); 802 | }); 803 | }); 804 | }); 805 | 806 | describe('join (populate) with edge collection profileOf', () => { 807 | it('should populate (join) profile and for the user (via graph)', (done) => { 808 | models.users_1.find({id: user_id}) 809 | .populate('profile') 810 | .then((users) => { 811 | should.exist(users); 812 | users.should.be.an.Array(); 813 | users.length.should.equal(1); 814 | const user = users[0]; 815 | should.exist(user.profile); 816 | // console.log('user:', user); 817 | user.profile.should.be.an.Array(); 818 | user.profile.length.should.equal(1); 819 | done(); 820 | }) 821 | .catch((err) => { 822 | done(err); 823 | }); 824 | }); 825 | }); 826 | 827 | describe('deleteEdge', () => { 828 | it('should delete an edge', (done) => { 829 | models.users_1.deleteEdge('profileOf', edge_id) 830 | .then((res) => { 831 | res.should.have.property('_id'); 832 | res.should.have.property('_key'); 833 | res.should.have.property('_rev'); 834 | done(); 835 | }) 836 | .catch((err) => { 837 | done(err); 838 | }); 839 | }); 840 | }); 841 | 842 | describe('createEdge on named graph', () => { 843 | it('should create an edge on the named graph', (done) => { 844 | models.users_1.createEdge('users_profiles', user_id, profile_id, { 845 | test_attr1: 'attr value 2 named graph' 846 | }) 847 | .then((res) => { 848 | res.should.have.property('_id'); 849 | res.should.have.property('_key'); 850 | res.should.have.property('_rev'); 851 | edge_id = res._id; 852 | done(); 853 | }) 854 | .catch((err) => { 855 | done(err); 856 | }); 857 | }); 858 | }); 859 | 860 | describe('neighbors', () => { 861 | it('should return the neighbor (profile) of the user (edge coll from named graph)', (done) => { 862 | models.users_1.neighbors(user_id, ['users_profiles']) 863 | .then((res) => { 864 | should.exist(res); 865 | res.should.be.an.Array(); 866 | res.length.should.equal(1); 867 | 868 | const n = res[0]; 869 | should.exist(n); 870 | n.should.have.property('_id'); 871 | n.should.have.property('_key'); 872 | n.should.have.property('_rev'); 873 | n.should.have.property('url'); 874 | n._id.should.equal(profile_id); 875 | done(); 876 | }) 877 | .catch((err) => { 878 | done(err); 879 | }); 880 | }); 881 | 882 | it('should return the neighbor (profile) of the user (named graph)', (done) => { 883 | models.users_1.neighbors(user_id, 'users_profiles_graph') 884 | .then((res) => { 885 | should.exist(res); 886 | res.should.be.an.Array(); 887 | res.length.should.equal(1); 888 | 889 | const n = res[0]; 890 | should.exist(n); 891 | n.should.have.property('_id'); 892 | n.should.have.property('_key'); 893 | n.should.have.property('_rev'); 894 | n.should.have.property('url'); 895 | n._id.should.equal(profile_id); 896 | done(); 897 | }) 898 | .catch((err) => { 899 | done(err); 900 | }); 901 | }); 902 | }); 903 | 904 | 905 | }); // graphs 906 | 907 | describe('drop collection(s)', () => { 908 | it('should drop the users_1 collection', (done) => { 909 | models.users_1.drop((err) => { 910 | done(err); 911 | }); 912 | }); 913 | 914 | it('should drop the pets_1 collection', (done) => { 915 | models.pets_1.drop((err) => { 916 | done(err); 917 | }); 918 | }); 919 | 920 | it('should drop the profiles_1 collection', (done) => { 921 | models.profiles_1.drop((err) => { 922 | done(err); 923 | }); 924 | }); 925 | 926 | it('should drop the profiles_1_id__users_1_profile (?) junctionTable collection', (done) => { 927 | models.profiles_1_id__users_1_profile.drop((err) => { 928 | done(err); 929 | }); 930 | }); 931 | 932 | }); 933 | 934 | 935 | }); // adapter 936 | -------------------------------------------------------------------------------- /test/integration/runner.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true */ 2 | 'use strict'; 3 | 4 | /** 5 | * Run integration tests 6 | * 7 | * Uses the `waterline-adapter-tests` module to 8 | * run mocha tests against the appropriate version 9 | * of Waterline. Only the interfaces explicitly 10 | * declared in this adapter's `package.json` file 11 | * are tested. (e.g. `queryable`, `semantic`, etc.) 12 | */ 13 | 14 | 15 | /** 16 | * Module dependencies 17 | */ 18 | 19 | var util = require('util'); 20 | var mocha = require('mocha'); 21 | var log = new (require('captains-log'))(); 22 | var TestRunner = require('waterline-adapter-tests'); 23 | var Adapter = require('../../'); 24 | var config = require("../test.json"); 25 | 26 | 27 | 28 | // Grab targeted interfaces from this adapter's `package.json` file: 29 | var packagejson = {}; 30 | var interfaces = []; 31 | try { 32 | packagejson = require('../../package.json'); 33 | interfaces = packagejson.waterlineAdapter.interfaces; 34 | } 35 | catch (e) { 36 | throw new Error( 37 | '\n'+ 38 | 'Could not read supported interfaces from `waterlineAdapter.interfaces`'+'\n' + 39 | 'in this adapter\'s `package.json` file ::' + '\n' + 40 | util.inspect(e) 41 | ); 42 | } 43 | 44 | 45 | 46 | 47 | 48 | log.info('Testing `' + packagejson.name + '`, a Sails/Waterline adapter.'); 49 | log.info('Running `waterline-adapter-tests` against ' + interfaces.length + ' interfaces...'); 50 | log.info('( ' + interfaces.join(', ') + ' )'); 51 | console.log(); 52 | log('Latest draft of Waterline adapter interface spec:'); 53 | log('http://links.sailsjs.org/docs/plugins/adapters/interfaces'); 54 | console.log(); 55 | 56 | 57 | 58 | 59 | /** 60 | * Integration Test Runner 61 | * 62 | * Uses the `waterline-adapter-tests` module to 63 | * run mocha tests against the specified interfaces 64 | * of the currently-implemented Waterline adapter API. 65 | */ 66 | new TestRunner({ 67 | 68 | // Load the adapter module. 69 | adapter: Adapter, 70 | 71 | // Default adapter config to use. 72 | config: config, 73 | 74 | // The set of adapter interfaces to test against. 75 | // (grabbed these from this adapter's package.json file above) 76 | interfaces: interfaces 77 | 78 | // Most databases implement 'semantic' and 'queryable'. 79 | // 80 | // As of Sails/Waterline v0.10, the 'associations' interface 81 | // is also available. If you don't implement 'associations', 82 | // it will be polyfilled for you by Waterline core. The core 83 | // implementation will always be used for cross-adapter / cross-connection 84 | // joins. 85 | // 86 | // In future versions of Sails/Waterline, 'queryable' may be also 87 | // be polyfilled by core. 88 | // 89 | // These polyfilled implementations can usually be further optimized at the 90 | // adapter level, since most databases provide optimizations for internal 91 | // operations. 92 | // 93 | // Full interface reference: 94 | // https://github.com/balderdashy/sails-docs/blob/master/adapter-specification.md 95 | }); 96 | -------------------------------------------------------------------------------- /test/models/pets_1.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, esversion: 6*/ 2 | 'use strict'; 3 | 4 | const Waterline = require('waterline'); 5 | 6 | const Pets = Waterline.Collection.extend({ 7 | identity: 'pets_1', 8 | schema: true, 9 | connection: 'arangodb', 10 | 11 | attributes: { 12 | 13 | id: { 14 | type: 'string', 15 | primaryKey: true, 16 | columnName: '_id' 17 | }, 18 | 19 | name: { 20 | type: 'string', 21 | required: true 22 | }, 23 | 24 | complex: { type: 'object' } 25 | 26 | } 27 | }); 28 | 29 | module.exports = Pets; 30 | -------------------------------------------------------------------------------- /test/models/profiles_1.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, esversion: 6*/ 2 | 'use strict'; 3 | 4 | const Waterline = require('waterline'); 5 | 6 | const Profiles = Waterline.Collection.extend({ 7 | identity: 'profiles_1', 8 | schema: true, 9 | connection: 'arangodb', 10 | 11 | attributes: { 12 | 13 | id: { 14 | type: 'string', 15 | primaryKey: true, 16 | columnName: '_id' 17 | }, 18 | 19 | url: { 20 | type: 'string' 21 | }, 22 | 23 | } 24 | }); 25 | 26 | module.exports = Profiles; 27 | -------------------------------------------------------------------------------- /test/models/users_1.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, esversion: 6*/ 2 | 'use strict'; 3 | 4 | const Waterline = require('waterline'); 5 | 6 | const Users = Waterline.Collection.extend({ 7 | identity: 'users_1', 8 | schema: true, 9 | connection: 'arangodb', 10 | 11 | attributes: { 12 | 13 | id: { 14 | type: 'string', 15 | primaryKey: true, 16 | columnName: '_id' 17 | }, 18 | 19 | name: { 20 | type: 'string', 21 | required: true 22 | }, 23 | 24 | complex: { type: 'object' }, 25 | 26 | pet: { 27 | model: 'pets_1' 28 | }, 29 | 30 | second: { 31 | type: 'string' 32 | }, 33 | 34 | first_name: 'string', 35 | dob: 'date', 36 | type: 'string', 37 | 38 | newfield: 'string', 39 | 40 | profile: { 41 | collection: 'profiles_1', 42 | via: 'id', 43 | edge: 'profileOf' 44 | } 45 | 46 | } 47 | }); 48 | 49 | module.exports = Users; 50 | -------------------------------------------------------------------------------- /test/models/users_profiles_graph.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, esversion: 6*/ 2 | 'use strict'; 3 | 4 | const Waterline = require('waterline'); 5 | 6 | const UsersProfilesGraph = Waterline.Collection.extend({ 7 | identity: 'users_profiles_graph', 8 | schema: true, 9 | connection: 'arangodb', 10 | 11 | attributes: { 12 | // this is a named graph 13 | $edgeDefinitions: [ 14 | { 15 | collection: 'users_profiles', 16 | from: ['users_1'], 17 | to: ['profiles_1'] 18 | } 19 | ] 20 | } 21 | 22 | }); 23 | 24 | module.exports = UsersProfilesGraph; 25 | -------------------------------------------------------------------------------- /test/models/users_users_graph.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true, esversion: 6*/ 2 | 'use strict'; 3 | 4 | const Waterline = require('waterline'); 5 | 6 | const UsersProfilesGraph = Waterline.Collection.extend({ 7 | identity: 'users_users_graph', 8 | schema: true, 9 | connection: 'arangodb', 10 | 11 | attributes: { 12 | // this is a named graph 13 | $edgeDefinitions: [ 14 | { 15 | collection: 'users_know_users', 16 | from: ['users_1'], 17 | to: ['users_1'] 18 | } 19 | ] 20 | } 21 | 22 | }); 23 | 24 | module.exports = UsersProfilesGraph; 25 | -------------------------------------------------------------------------------- /test/test-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 8529, 4 | "user": "test", 5 | "password": "test", 6 | "database": "test" 7 | } 8 | --------------------------------------------------------------------------------