├── .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 | 
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 |
--------------------------------------------------------------------------------