├── .gitignore ├── .jshintrc ├── .npmignore ├── .npmrc ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── data ├── IMDB │ ├── dump │ │ ├── imdb_edges.data.json │ │ ├── imdb_edges.structure.json │ │ ├── imdb_vertices.data.json │ │ └── imdb_vertices.structure.json │ └── import.sh ├── airports │ ├── data.csv │ └── import.sh ├── json.sh └── users │ ├── data.json │ ├── import.sh │ └── names.json ├── index.js ├── package.json ├── src └── arangodb.coffee └── test ├── core.test.coffee ├── crud.test.coffee ├── crud ├── document.test.coffee └── edge.test.coffee ├── imported.test.coffee ├── init.coffee ├── migration.test.coffee ├── mocha.opts ├── operators.test.coffee └── persistence-hooks.test.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /lib 5 | /dist 6 | /tmp 7 | /out-tsc 8 | /.nyc_output 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # IDEs and editors 14 | /.idea 15 | .project 16 | .classpath 17 | .c9/ 18 | *.launch 19 | .settings/ 20 | *.sublime-workspace 21 | 22 | # IDE - VSCode 23 | .vscode/* 24 | !.vscode/settings.json 25 | !.vscode/tasks.json 26 | !.vscode/launch.json 27 | !.vscode/extensions.json 28 | 29 | # misc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | package-lock.json 36 | testem.log 37 | /typings 38 | .strong-pm 39 | .loopbackrc 40 | /.history 41 | 42 | # e2e 43 | /e2e/*.js 44 | /e2e/*.map 45 | 46 | # System Files 47 | .DS_Store 48 | Thumbs.db -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": false, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": false, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true 20 | } 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | data 3 | node_modules 4 | test 5 | 6 | .loopbackrc 7 | .idea 8 | .travis.yml 9 | .history 10 | .nyc_output 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 6 5 | - 8 6 | - 10 7 | 8 | env: 9 | - ARANGODB_VERSION=2.8 10 | - ARANGODB_VERSION=3.1 11 | - ARANGODB_VERSION=3.2 12 | - ARANGODB_VERSION=3.3 13 | 14 | before_install: 15 | - docker pull arangodb:$ARANGODB_VERSION 16 | - docker run -e ARANGO_NO_AUTH=1 -p 8529:8529 -d arangodb:$ARANGODB_VERSION 17 | 18 | after_success: 19 | - npm run coverage:ci 20 | 21 | branches: 22 | only: 23 | - master 24 | - 2.x -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2018-04-25, Version 2.0.5 2 | ========================= 3 | 4 | * fix between operator (Matteo Padovano) 5 | 6 | 7 | 2018-02-01, Version 2.0.4 8 | ========================= 9 | 10 | * fix wrong aql generator for lte and lt condition (Matteo Padovano) 11 | 12 | * check for error callback (Matteo Padovano) 13 | 14 | * fix wrong index generator with complex query (Matteo Padovano) 15 | 16 | 17 | 2018-01-26, Version 2.0.3 18 | ========================= 19 | 20 | * Improve the performance for count method (Matteo Padovano) 21 | 22 | 23 | 2017-12-12, Version 2.0.2 24 | ========================= 25 | 26 | * fix bound scope (Matteo Padovano) 27 | 28 | 29 | 2017-12-04, Version 2.0.1 30 | ========================= 31 | 32 | * remove default promise settings (Matteo Padovano) 33 | 34 | * fix wrong repository url (Matteo Padovano) 35 | 36 | * update travis file (Matteo Padovano) 37 | 38 | * update gitignore (Matteo Padovano) 39 | 40 | 41 | 2017-06-01, Version 2.0.0 42 | ========================= 43 | 44 | * update readme for new release (Matteo Padovano) 45 | 46 | * add arangodb v3 to travis (Matteo Padovano) 47 | 48 | * add .npmignore file (Matteo Padovano) 49 | 50 | 51 | 2017-04-06, Version 1.1.0 52 | ========================= 53 | 54 | * update dependecies (Matteo Padovano) 55 | 56 | * update badge (Matteo Padovano) 57 | 58 | * add grunt file and coverage (Matteo Padovano) 59 | 60 | * enable CI and fix error imported tests (Matteo Padovano) 61 | 62 | * fix typo (Matteo Padovano) 63 | 64 | * Update api url from relative to absolute. (Matteo Padovano) 65 | 66 | * remove strict conversation for inq operator (Matteo Padovano) 67 | 68 | * Update url project (Matteo Padovano) 69 | 70 | * Update readme (Matteo Padovano) 71 | 72 | * Added travis configuration (Matteo Padovano) 73 | 74 | * bump version; update url repository. (Matteo Padovano) 75 | 76 | 77 | 2015-11-05, Version 1.0.0 78 | ========================= 79 | 80 | * bump version (Matteo Padovano) 81 | 82 | * refactoring upsert with aqb (Matteo Padovano) 83 | 84 | * Complete refactoring driver; Split document and edge test into separate files; Update dependency (Matteo Padovano) 85 | 86 | * Refactoring driver; Style format (Matteo Padovano) 87 | 88 | * Add folder .idea to .gitignore (Matteo Padovano) 89 | 90 | * Remove pre-commit hook (Matteo Padovano) 91 | 92 | * Change timeout of mocha tests only for specific ones instead of all. (Matteo Padovano) 93 | 94 | * Fix wrong model and collection name (Matteo Padovano) 95 | 96 | * Removed obsolete scripts shell. (Matteo Padovano) 97 | 98 | * build library. (Matteo Padovano) 99 | 100 | * Method all returns all data of cursor instead of first 1000 (default batch size of cursor) objects. (Matteo Padovano) 101 | 102 | * Increased timeout for test mocha. (Matteo Padovano) 103 | 104 | * add contributor (Matteo Padovano) 105 | 106 | 107 | 2015-09-07, Version 0.1.0 108 | ========================= 109 | 110 | * First release! 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 mrbatista 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 | # loopback-connector-arangodb 2 | 3 | [![tag][tag-image]][tag-url] 4 | [![build][travis-image]][travis-url] 5 | [![Coverage Status][coverage-image]][coverage-url] 6 | [![license:mit](https://img.shields.io/badge/license-mit-green.svg)](#license) 7 |
8 | [![npm][npm-image]][npm-url] 9 | [![npm downloads][npm-downloads-image]][npm-downloads-url] 10 | [![dependencies][dep-status-image]][dep-status-url] 11 | [![devDependency][dev-dep-status-image]][dev-dep-status-url] 12 | 13 | The ArangoDB connector for the LoopBack framework. 14 | 15 | ## Note 16 | 17 | 1. Version 2.x.x **drop** support for node v0.12. The supported version 18 | are node v4.x.x and v6.x.x 19 | 2. If you want to migrate to 2.x.x and use ArangoDB 2.8.x is it necessary 20 | to configure the connector to use the old version. Example: 21 | ```json 22 | "test": { 23 | "arangodb": { 24 | "host": "127.0.0.1", 25 | "database": "test", 26 | "username": "youruser", 27 | "password": "yourpass", 28 | "port": 8529, 29 | "arangoVersion": 28000 30 | } 31 | } 32 | ``` 33 | 34 | ## Customizing ArangoDB configuration for tests/examples 35 | 36 | By default, examples and tests from this module assume there is a ArangoDB server 37 | instance running on localhost at port 8529. 38 | 39 | To customize the settings, you can drop in a `.loopbackrc` file to the root directory 40 | of the project or the home folder. 41 | 42 | **Note**: Tests and examples in this project configure the data source using the deprecated '.loopbackrc' file method, 43 | which is not suppored in general. 44 | For information on configuring the connector in a LoopBack application, please refer to [LoopBack documentation](http://docs.strongloop.com/display/LB/MongoDB+connector). 45 | 46 | The .loopbackrc file is in JSON format, for example: 47 | ```json 48 | { 49 | "dev": { 50 | "arangodb": { 51 | "host": "127.0.0.1", 52 | "database": "test", 53 | "username": "youruser", 54 | "password": "yourpass", 55 | "port": 8529 56 | } 57 | }, 58 | "test": { 59 | "arangodb": { 60 | "host": "127.0.0.1", 61 | "database": "test", 62 | "username": "youruser", 63 | "password": "yourpass", 64 | "port": 8529 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | **Note**: username/password is only required if the ArangoDB server has 71 | authentication enabled. 72 | 73 | ## Contributing 74 | 75 | **We love contributions!** 76 | 77 | When contributing, follow the simple rules: 78 | 79 | * Don't violate [DRY](http://programmer.97things.oreilly.com/wiki/index.php/Don%27t_Repeat_Yourself) principles. 80 | * [Boy Scout Rule](http://programmer.97things.oreilly.com/wiki/index.php/The_Boy_Scout_Rule) needs to have been applied. 81 | * Your code should look like all the other code – this project should look like it was written by one man, always. 82 | * If you want to propose something – just create an issue and describe your question with as much description as you can. 83 | * If you think you have some general improvement, consider creating a pull request with it. 84 | * If you add new code, it should be covered by tests. No tests – no code. 85 | * If you add a new feature, don't forget to update the documentation for it. 86 | * If you find a bug (or at least you think it is a bug), create an issue with the library version and test case that we can run and see what are you talking about, or at least full steps by which we can reproduce it. 87 | 88 | ## Running tests 89 | 90 | The tests in this repository are mainly integration tests, meaning you will need 91 | to run them using our preconfigured test server. 92 | 93 | 1. Ask a core developer for instructions on how to set up test server 94 | credentials on your machine 95 | 2. `npm test` 96 | 97 | ## Release notes 98 | 99 | ## License 100 | 101 | [MIT](LICENSE) 102 | 103 | [tag-image]: https://img.shields.io/github/tag/mrbatista/loopback-connector-arangodb.svg 104 | [tag-url]: https://github.com/mrbatista/loopback-connector-arangodb/releases 105 | [npm-image]: https://img.shields.io/npm/v/loopback-connector-arangodb.svg 106 | [npm-url]: https://npmjs.org/package/loopback-connector-arangodb 107 | [npm-downloads-image]: https://img.shields.io/npm/dm/loopback-connector-arangodb.svg 108 | [npm-downloads-url]: https://npmjs.org/package/loopback-connector-arangodb 109 | [dep-status-image]: https://img.shields.io/david/mrbatista/loopback-connector-arangodb.svg 110 | [dep-status-url]: https://david-dm.org/mrbatista/loopback-connector-arangodb 111 | [dev-dep-status-image]: https://david-dm.org/mrbatista/loopback-connector-arangodb/dev-status.svg 112 | [dev-dep-status-url]: https://david-dm.org/mrbatista/loopback-connector-arangodb#info=devDependencies 113 | [travis-image]: https://travis-ci.org/mrbatista/loopback-connector-arangodb.svg 114 | [travis-url]: https://travis-ci.org/mrbatista/loopback-connector-arangodb 115 | [coverage-image]: https://coveralls.io/repos/github/mrbatista/loopback-connector-arangodb/badge.svg 116 | [coverage-url]: https://coveralls.io/github/mrbatista/loopback-connector-arangodb -------------------------------------------------------------------------------- /data/IMDB/dump/imdb_edges.structure.json: -------------------------------------------------------------------------------- 1 | { 2 | "parameters": { 3 | "version": 4, 4 | "type": 3, 5 | "cid": "266191978919", 6 | "deleted": false, 7 | "doCompact": true, 8 | "maximalSize": 33554432, 9 | "name": "imdb_edges", 10 | "isVolatile": false, 11 | "waitForSync": false 12 | }, 13 | "indexes": [] 14 | } -------------------------------------------------------------------------------- /data/IMDB/dump/imdb_vertices.structure.json: -------------------------------------------------------------------------------- 1 | { 2 | "parameters": { 3 | "version": 4, 4 | "type": 2, 5 | "cid": "266191323559", 6 | "deleted": false, 7 | "doCompact": true, 8 | "maximalSize": 33554432, 9 | "name": "imdb_vertices", 10 | "isVolatile": false, 11 | "waitForSync": false 12 | }, 13 | "indexes": [{ 14 | "id": "266193551783", 15 | "type": "fulltext", 16 | "unique": false, 17 | "minLength": 3, 18 | "fields": ["description"] 19 | }, { 20 | "id": "266193748391", 21 | "type": "fulltext", 22 | "unique": false, 23 | "minLength": 3, 24 | "fields": ["title"] 25 | }, { 26 | "id": "266193944999", 27 | "type": "fulltext", 28 | "unique": false, 29 | "minLength": 3, 30 | "fields": ["name"] 31 | }, { 32 | "id": "266194141607", 33 | "type": "fulltext", 34 | "unique": false, 35 | "minLength": 3, 36 | "fields": ["birthplace"] 37 | }] 38 | } -------------------------------------------------------------------------------- /data/IMDB/import.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | . ../json.sh 3 | 4 | host="$(json_key 'test' 'arangodb' 'host' < ../../.loopbackrc)" 5 | port="$(json_key 'test' 'arangodb' 'port' < ../../.loopbackrc)" 6 | database="$(json_key 'test' 'arangodb' 'database' < ../../.loopbackrc)" 7 | username="$(json_key 'test' 'arangodb' 'username' < ../../.loopbackrc)" 8 | password="$(json_key 'test' 'arangodb' 'password' < ../../.loopbackrc)" 9 | 10 | cmd_parameters='' 11 | # set url=host:port, connect via tcp 12 | if [ -z "$host" ] | [ -z "$port" ] 13 | then 14 | cmd_parameters+='' 15 | else 16 | cmd_parameters+="--server.endpoint=tcp://$host:$port " 17 | fi 18 | 19 | # set database 20 | if [ -z "$database" ] 21 | then 22 | cmd_parameters+='' 23 | else 24 | cmd_parameters+="--server.database=$database " 25 | fi 26 | 27 | # username 28 | if [ -z "$username" ] 29 | then 30 | cmd_parameters+='' 31 | else 32 | cmd_parameters+="--server.username=$username " 33 | fi 34 | 35 | # password 36 | if [ -z "$password" ] 37 | then 38 | cmd_parameters+='' 39 | else 40 | cmd_parameters+="--server.password=$password " 41 | fi 42 | 43 | 44 | ${ARANGODB_BIN}arangosh $cmd_parameters --quiet <", 46 | "contributors": [ 47 | { 48 | "name": "Matteo Padovano", 49 | "email": "mrba7ista@gmail.com" 50 | } 51 | ], 52 | "license": "MIT", 53 | "nyc": { 54 | "extension": [ 55 | ".coffee" 56 | ], 57 | "exclude": [ 58 | "server/server.js", 59 | "coverage/**", 60 | "test/**" 61 | ], 62 | "reporter": [ 63 | "lcov", 64 | "text-summary" 65 | ], 66 | "check-coverage": true, 67 | "statements": 66, 68 | "branches": 67, 69 | "functions": 90, 70 | "lines": 67 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/arangodb.coffee: -------------------------------------------------------------------------------- 1 | # node modules 2 | 3 | # Module dependencies 4 | arangojs = require 'arangojs' 5 | qb = require 'aqb' 6 | url = require 'url' 7 | merge = require 'extend' 8 | async = require 'async' 9 | _ = require 'underscore' 10 | Connector = require('loopback-connector').Connector 11 | debug = require('debug') 'loopback:connector:arango' 12 | 13 | ### 14 | Generate the arangodb URL from the options 15 | ### 16 | exports.generateArangoDBURL = generateArangoDBURL = (settings) -> 17 | u = {} 18 | u.protocol = settings.protocol or 'http:' 19 | u.hostname = settings.hostname or settings.host or '127.0.0.1' 20 | u.port = settings.port or 8529 21 | u.auth = "#{settings.username}:#{settings.password}" if settings.username and settings.password 22 | settings.databaseName = settings.database or settings.db or '_system' 23 | return url.format u 24 | 25 | ### 26 | Check if field should be included 27 | @param {Object} fields 28 | @param {String} fieldName 29 | @returns {Boolean} 30 | @private 31 | ### 32 | _fieldIncluded = (fields, fieldName) -> 33 | if not fields then return true 34 | 35 | if Array.isArray fields 36 | return fields.indexOf fieldName >= 0 37 | 38 | if fields[fieldName] 39 | # Included 40 | return true 41 | 42 | if fieldName in fields and !fields[fieldName] 43 | # Excluded 44 | return false 45 | 46 | for f in fields 47 | return !fields[f]; # If the fields has exclusion 48 | 49 | return true 50 | 51 | ### 52 | Verify if a field is a reserved arangoDB key 53 | @param {String} key The name of key to verify 54 | @returns {Boolean} 55 | @private 56 | ### 57 | _isReservedKey = (key) -> 58 | key in ['_key', '_id', '_rev', '_from', '_to'] 59 | 60 | class Sequence 61 | constructor: (start) -> 62 | @nextVal = start or 0 63 | 64 | next: () -> 65 | return @nextVal++; 66 | 67 | 68 | ### 69 | Initialize the ArangoDB connector for the given data source 70 | @param {DataSource} dataSource The data source instance 71 | @param {Function} [callback] The callback function 72 | ### 73 | exports.initialize = initializeDataSource = (dataSource, callback) -> 74 | return if not arangojs 75 | 76 | s = dataSource.settings 77 | s.url = s.url or generateArangoDBURL s 78 | dataSource.connector = new ArangoDBConnector s, dataSource 79 | dataSource.connector.connect callback if callback? 80 | 81 | ### 82 | Loopback ArangoDB Connector 83 | @extend Connector 84 | ### 85 | class ArangoDBConnector extends Connector 86 | returnVariable = 'result' 87 | @collection = 'collection' 88 | @edgeCollection = 'edgeCollection' 89 | @returnVariable = 'result' 90 | 91 | ### 92 | The constructor for ArangoDB connector 93 | @param {Object} settings The settings object 94 | @param {DataSource} dataSource The data source instance 95 | @constructor 96 | ### 97 | constructor: (settings, dataSource) -> 98 | super 'arangodb', settings 99 | # debug 100 | @debug = dataSource.settings.debug or debug.enabled 101 | # link to datasource 102 | @dataSource = dataSource 103 | # Arango Query Builder 104 | # TODO MAJOR rename to aqb 105 | @qb = qb 106 | 107 | ### 108 | Connect to ArangoDB 109 | @param {Function} [callback] The callback function 110 | 111 | @callback callback 112 | @param {Error} err The error object 113 | @param {Db} db The arangoDB object 114 | ### 115 | connect: (callback) -> 116 | debug "ArangoDB connection is called with settings: #{JSON.stringify @settings}" if @debug 117 | if not @db 118 | @db = arangojs @settings 119 | @api = @db.route '/_api' 120 | process.nextTick () => 121 | callback null, @db if callback 122 | 123 | ### 124 | Get the types of this connector 125 | @return {Array} The types of connector 126 | ### 127 | getTypes: () -> 128 | return ['db', 'nosql', 'arangodb'] 129 | 130 | ### 131 | The default Id type 132 | @return {String} The type of id value 133 | ### 134 | getDefaultIdType: () -> 135 | return String 136 | 137 | ### 138 | Get the model class for a certain model name 139 | @param {String} model The model name to lookup 140 | @return {Object} The model class of this model 141 | ### 142 | getModelClass: (model) -> 143 | return @_models[model] 144 | 145 | ### 146 | Get the collection name for a certain model name 147 | @param {String} model The model name to lookup 148 | @return {Object} The collection name for this model 149 | ### 150 | getCollectionName: (model) -> 151 | modelClass = @getModelClass model 152 | if modelClass.settings and modelClass.settings.arangodb 153 | model = modelClass.settings.arangodb.collection or model 154 | return model 155 | 156 | ### 157 | Coerce the id value 158 | ### 159 | coerceId: (model, id) -> 160 | return id if not id? 161 | idValue = id; 162 | idName = @idName model 163 | 164 | # Type conversion for id 165 | idProp = @getPropertyDefinition model, idName 166 | if idProp && typeof idProp.type is 'function' 167 | if not (idValue instanceof idProp.type) 168 | idValue = idProp.type id 169 | # Reset to id 170 | if idProp.type is Number and isNaN id then idValue = id 171 | return idValue; 172 | 173 | ### 174 | Set value of specific field into data object 175 | @param data {Object} The data object 176 | @param field {String} The name of field to set 177 | @param value {Any} The value to set 178 | ### 179 | _setFieldValue: (data, field, value) -> 180 | if data then data[field] = value; 181 | 182 | ### 183 | Verify if the collection is an edge collection 184 | @param model [String] The model name to lookup 185 | @return [Boolean] Return true if collection is edge false otherwise 186 | ### 187 | _isEdge: (model) -> 188 | modelClass = @getModelClass model 189 | settings = modelClass.settings 190 | return settings and settings.arangodb and settings.arangodb.edge || false 191 | 192 | ### 193 | ### 194 | _getNameOfProperty: (model, p) -> 195 | props = @getModelClass(model).properties 196 | for key, prop of props 197 | if prop[p] then return key else continue 198 | return false 199 | 200 | ### 201 | Get if the model has _id field 202 | @param {String} model The model name to lookup 203 | @return {String|Boolean} Return name of _id or false if model not has _id field 204 | ### 205 | _fullIdName: (model) -> 206 | @_getNameOfProperty model, '_id' 207 | 208 | ### 209 | Get if the model has _from field 210 | @param {String} model The model name to lookup 211 | @return {String|Boolean} Return name of _from or false if model not has _from field 212 | ### 213 | _fromName: (model) -> 214 | @_getNameOfProperty model, '_from' 215 | 216 | ### 217 | Get if the model has _to field 218 | @param {String} model The model name to lookup 219 | @return {String|Boolean} Return name of _to or false if model not has _to field 220 | ### 221 | _toName: (model) -> 222 | @_getNameOfProperty model, '_to' 223 | 224 | ### 225 | Access a ArangoDB collection by model name 226 | @param {String} model The model name 227 | @return {*} 228 | ### 229 | getCollection: (model) -> 230 | if not @db then throw new Error('ArangoDB connection is not established') 231 | 232 | collection = ArangoDBConnector.collection 233 | if @_isEdge model then collection = ArangoDBConnector.edgeCollection 234 | return @db[collection] @getCollectionName model 235 | 236 | ### 237 | Converts the retrieved data from the database to JSON, based on the properties of a given model 238 | @param {String} model The model name to look up the properties 239 | @param {Object} [data] The data from DB 240 | @return {Object} The converted data as an JSON Object 241 | ### 242 | fromDatabase: (model, data) -> 243 | return null if not data? 244 | 245 | props = @getModelClass(model).properties 246 | for key, val of props 247 | #Buffer type 248 | if data[key]? and val? and val.type is Buffer 249 | data[key] = new Buffer(data[key]) 250 | # Date 251 | if data[key]? and val? and val.type is Date 252 | data[key] = new Date data[key] 253 | # GeoPoint 254 | if data[key]? and val? and val.type and val.type.name is 'GeoPoint' 255 | console.warn('GeoPoint is not supported by connector'); 256 | return data 257 | 258 | ### 259 | Execute a ArangoDB command 260 | ### 261 | execute: (model, command) -> 262 | #Get the parameters for the given command 263 | args = [].slice.call(arguments, 2); 264 | #The last argument must be a callback function 265 | callback = args[args.length - 1]; 266 | context = 267 | req: 268 | command: command 269 | params: args 270 | 271 | @notifyObserversAround 'execute', context, (context, done) => 272 | debug 'ArangoDB: model=%s command=%s', model, command, args if @debug 273 | 274 | args[args.length - 1] = (err, result) -> 275 | if err 276 | debug('Error: ', err); 277 | if err.code 278 | err.statusCode = err.code 279 | err.response? delete err.response 280 | else 281 | context.res = result; 282 | debug('Result: ', result) 283 | done(err, result); 284 | 285 | if command is 'query' 286 | query = context.req.params[0] 287 | bindVars = context.req.params[1] 288 | if @debug 289 | if typeof query.toAQL is 'function' 290 | q = query.toAQL() 291 | else 292 | q = query 293 | debug "query: #{q} bindVars: #{JSON.stringify bindVars}" 294 | 295 | @db.query.apply @db, args 296 | else 297 | collection = @getCollection model 298 | collection[command].apply collection, args 299 | , callback 300 | 301 | ### 302 | Get the version of the ArangoDB 303 | @param callback [Function] The callback function 304 | 305 | @callback callback 306 | @param {Error} err The error object 307 | @param {String} version The arangoDB version 308 | ### 309 | getVersion: (callback) -> 310 | if @version? 311 | callback null, @version 312 | else 313 | @api.get 'version', (err, result) => 314 | callback err if err 315 | @version = result.body 316 | callback null, @version 317 | 318 | ### 319 | Create a new model instance for the given data 320 | @param {String} model The model name 321 | @param {Object} data The data to create 322 | @param {Object} options The data to create 323 | @param callback [Function] The callback function 324 | ### 325 | create: (model, data, options, callback) -> 326 | debug "create model #{model} with data: #{JSON.stringify data}" if @debug 327 | 328 | idValue = @getIdValue model, data 329 | idName = @idName model 330 | if !idValue? or typeof idValue is 'undefined' 331 | delete data[idName] 332 | else 333 | id = @getDefaultIdType() idValue 334 | data._key = id 335 | if idName isnt '_key' then delete data[idName] 336 | 337 | # Check and delete full id name if present 338 | fullIdName = @_fullIdName model 339 | if fullIdName then delete data[fullIdName] 340 | 341 | isEdge = @_isEdge model 342 | fromName = null 343 | toName = null 344 | 345 | if isEdge 346 | fromName = @_fromName model 347 | data._from = data[fromName] 348 | if fromName isnt '_from' 349 | data._from = data[fromName] 350 | delete data[fromName] 351 | toName = @_toName model 352 | if toName isnt '_to' 353 | data._to = data[toName] 354 | delete data[toName] 355 | 356 | @execute model, 'save', data, (err, result) => 357 | if err 358 | # Change message error to pass junit test 359 | if err.errorNum is 1210 then err.message = '/duplicate/i' 360 | return callback(err) 361 | # Save _key and _id value 362 | idValue = @coerceId model, result._key 363 | delete data._key 364 | data[idName] = idValue; 365 | 366 | if isEdge 367 | if fromName isnt '_from' then data[fromName] = data._from 368 | if toName isnt '_to' then data[toName] = data._to 369 | 370 | if fullIdName 371 | data[fullIdName] = result._id 372 | delete result._id 373 | 374 | callback err, idValue 375 | 376 | ### 377 | Update if the model instance exists with the same id or create a new instance 378 | @param model [String] The model name 379 | @param data [Object] The model instance data 380 | @param options [Object] The options 381 | @param callback [Function] The callback function, called with a (possible) error object and updated or created object 382 | ### 383 | updateOrCreate: (model, data, options, callback) -> 384 | debug "updateOrCreate for Model #{model} with data: #{JSON.stringify data}" if @debug 385 | 386 | idValue = @getIdValue(model, data) 387 | idName = @idName(model) 388 | idValue = @getDefaultIdType() idValue if typeof idValue is 'number' 389 | delete data[idName] 390 | 391 | fullIdName = @_fullIdName model 392 | if fullIdName then delete data[fullIdName] 393 | 394 | isEdge = @_isEdge model 395 | fromName = null 396 | toName = null 397 | 398 | if isEdge 399 | fromName = @_fromName model 400 | if fromName isnt '_from' 401 | data._from = data[fromName] 402 | delete data[fromName] 403 | toName = @_toName model 404 | if toName isnt '_to' 405 | data._to = data[toName] 406 | delete data[toName] 407 | 408 | dataI = _.clone(data) 409 | dataI._key = idValue 410 | 411 | aql = qb.upsert({_key: '@id'}).insert('@dataI').update('@data').in('@@collection').let('isNewInstance', 412 | qb.ref('OLD').then(false).else(true)).return({doc: 'NEW', isNewInstance: 'isNewInstance'}); 413 | bindVars = 414 | '@collection': @getCollectionName model 415 | id: idValue 416 | dataI: dataI 417 | data: data 418 | 419 | @execute model, 'query', aql, bindVars, (err, result) => 420 | if result and result._result[0] 421 | newDoc = result._result[0].doc 422 | # Delete revision 423 | delete newDoc._rev 424 | if fullIdName 425 | data[fullIdName] = newDoc._id 426 | if fullIdName isnt '_id' then delete newDoc._id 427 | else 428 | delete newDoc._id 429 | if isEdge 430 | if fromName isnt '_from' then data[fromName] = data._from 431 | if toName isnt '_to' then data[toName] = data._to 432 | 433 | isNewInstance = { isNewInstance: result._result[0].isNewInstance } 434 | @setIdValue(model, data, newDoc._key) 435 | @setIdValue(model, newDoc, newDoc._key) 436 | if idName isnt '_key' then delete newDoc._key 437 | callback err, newDoc, isNewInstance 438 | 439 | ### 440 | Save the model instance for the given data 441 | @param model [String] The model name 442 | @param data [Object] The updated data to save or create 443 | @param options [Object] 444 | @param callback [Function] The callback function, called with a (possible) error object and the number of affected objects 445 | ### 446 | save: @::updateOrCreate 447 | 448 | ### 449 | Check if a model instance exists by id 450 | @param model [String] The model name 451 | @param id [String] The id value 452 | @param options [Object] 453 | @param callback [Function] The callback function, called with a (possible) error object and an boolean value if the specified object existed (true) or not (false) 454 | ### 455 | exists: (model, id, options, callback) -> 456 | debug "exists for #{model} with id: #{id}" if @debug 457 | 458 | @find model, id, options, (err, result) -> 459 | return callback err if err 460 | callback null, result.length > 0 461 | 462 | ### 463 | Find a model instance by id 464 | @param model [String] model The model name 465 | @param id [String] id The id value 466 | @param options [Object] 467 | @param callback [Function] The callback function, called with a (possible) error object and the found object 468 | ### 469 | find: (model, id, options, callback) -> 470 | debug "find for #{model} with id: #{id}" if @debug 471 | 472 | command = 'document' 473 | if @_isEdge model then command = 'edge' 474 | 475 | @execute model, command, id, (err, result) -> 476 | return callback err if err 477 | callback null, result 478 | 479 | ### 480 | Extracts where relevant information from the filter for a certain model 481 | @param [String] model The model name 482 | @param [Object] filter The filter object, also containing the where conditions 483 | @param [Sequence] sequence The sequence instance used to generate random bind vars 484 | @return return [Object] 485 | @option return aqlArray [Array] The issued conditions as an array of AQL query builder objects 486 | @option return bindVars [Object] The variables, bound in the conditions 487 | @option return geoObject [Object] An query builder object containing possible parameters for a geo query 488 | ### 489 | _buildWhere: (model, where, sequence) -> 490 | debug "Evaluating where object #{JSON.stringify where} for Model #{model}" if @debug 491 | 492 | if !where? or typeof where isnt 'object' 493 | return 494 | 495 | # array holding the filter 496 | aqlArray = [] 497 | # the object holding the assignments of conditional values to temporary variables 498 | bindVars = {} 499 | geoExpr = {} 500 | # index for condition parameter binding 501 | sequence = sequence or new Sequence 502 | # helper function to fill bindVars with the upcoming temporary variables that the where sentence will generate 503 | assignNewQueryVariable = (value) -> 504 | partName = 'param_' + sequence.next() 505 | bindVars[partName] = value 506 | return '@' + partName 507 | 508 | idName = @idName model 509 | fullIdName = @_fullIdName model 510 | fromName = @_fromName model 511 | toName = @_toName model 512 | ### 513 | the where object comes in two flavors 514 | 515 | - where[prop] = value: this is short for "prop" equals "value" 516 | - where[prop][op] = value: this is the long version and stands for "prop" "op" "value" 517 | ### 518 | for condProp, condValue of where 519 | do() => 520 | # special treatment for 'and', 'or' and 'nor' operator, since there value is an array of conditions 521 | if condProp in ['and', 'or', 'nor'] 522 | # 'and', 'or' and 'nor' have multiple conditions so we run buildWhere recursively on their array to 523 | if Array.isArray condValue 524 | aql = qb 525 | # condValue is an array of conditions so get the conditions from it via a recursive buildWhere call 526 | for c, a of condValue 527 | cond = @_buildWhere model, a, sequence 528 | aql = aql[condProp] cond.aqlArray[0] 529 | bindVars = merge true, bindVars, cond.bindVars 530 | aqlArray.push aql 531 | aql = null 532 | return 533 | 534 | # correct if the conditionProperty falsely references to 'id' 535 | if condProp is idName 536 | condProp = '_key' 537 | if typeof condValue is 'number' then condValue = String(condValue) 538 | if condProp is fullIdName 539 | condProp = '_id' 540 | if condProp is fromName 541 | condProp = '_from' 542 | if condProp is toName 543 | condProp = '_to' 544 | 545 | # special case: if condValue is a Object (instead of a string or number) we have a conditionOperator 546 | if condValue and condValue.constructor.name is 'Object' 547 | # condition operator is the only keys value, the new condition value is shifted one level deeper and can be a object with keys and values 548 | options = condValue.options 549 | condOp = Object.keys(condValue)[0] 550 | condValue = condValue[condOp] 551 | if condOp 552 | # If the value is not an array, fall back to regular fields 553 | switch 554 | when condOp in ['lte', 'lt'] 555 | tempAql = qb[condOp] "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}" 556 | # https://docs.arangodb.com/2.8/Aql/Basics.html#type-and-value-order 557 | if condValue isnt null 558 | tempAql = tempAql.and qb['neq'] "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(null)}" 559 | aqlArray.push(tempAql) 560 | when condOp in ['gte', 'gt'] 561 | # https://docs.arangodb.com/2.8/Aql/Basics.html#type-and-value-order 562 | if condValue is null 563 | if condOp is 'gte' then condOp = 'lte' 564 | if condOp is 'gt' then condOp = 'lt' 565 | aqlArray.push qb[condOp] "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(null)}" 566 | else 567 | aqlArray.push qb[condOp] "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}" 568 | when condOp in ['eq', 'neq'] 569 | aqlArray.push qb[condOp] "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}" 570 | # range comparison 571 | when condOp is 'between' 572 | tempAql = qb.gte "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue[0])}" 573 | tempAql = tempAql.and qb.lte "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue[1])}" 574 | aqlArray.push(tempAql) 575 | # string comparison 576 | when condOp is 'like' 577 | if options is 'i' then options = true else options = false 578 | aqlArray.push qb.fn('LIKE') "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}", options 579 | when condOp is 'nlike' 580 | if options is 'i' then options = true else options = false 581 | aqlArray.push qb.not qb.fn('LIKE') "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}", options 582 | # array comparison 583 | when condOp is 'nin' 584 | if _isReservedKey condProp 585 | condValue = (value.toString() for value in condValue) 586 | aqlArray.push qb.not qb.in "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}" 587 | when condOp is 'inq' 588 | if _isReservedKey condProp 589 | condValue = (value.toString() for value in condValue) 590 | aqlArray.push qb.in "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}" 591 | # geo comparison (extra object) 592 | when condOp is 'near' 593 | # 'near' does not create a condition in the filter part, it returnes the lat/long pair 594 | # the query will be done from the querying method itself 595 | [lat, long] = condValue.split(',') 596 | collection = @getCollectionName model 597 | if where.limit? 598 | geoExpr = qb.NEAR collection, lat, long, where.limit 599 | else 600 | geoExpr = qb.NEAR collection, lat, long 601 | # if we don't have a matching operator or no operator at all (condOp = false) print warning 602 | else 603 | console.warn 'No matching operator for : ', condOp 604 | else 605 | aqlArray.push qb.eq "#{returnVariable}.#{condProp}", "#{assignNewQueryVariable(condValue)}" 606 | return { 607 | aqlArray: aqlArray 608 | bindVars: bindVars 609 | geoExpr: geoExpr 610 | } 611 | 612 | ### 613 | Find matching model instances by the filter 614 | @param [String] model The model name 615 | @param [Object] filter The filter 616 | @param options [Object] 617 | @param [Function] callback Callback with (possible) error object or list of objects 618 | ### 619 | all: (model, filter, options, callback) -> 620 | debug "all for #{model} with filter #{JSON.stringify filter}" if @debug 621 | 622 | idName = @idName model 623 | fullIdName = @_fullIdName model 624 | fromName = @_fromName model 625 | toName = @_toName model 626 | 627 | bindVars = 628 | '@collection': @getCollectionName model 629 | aql = qb.for(returnVariable).in('@@collection') 630 | 631 | if filter.where 632 | where = @_buildWhere(model, filter.where) 633 | for w in where.aqlArray 634 | aql = aql.filter(w) 635 | merge true, bindVars, where.bindVars 636 | 637 | if filter.order 638 | if typeof filter.order is 'string' then filter.order = filter.order.split(',') 639 | for order in filter.order 640 | m = order.match(/\s+(A|DE)SC$/) 641 | field = order.replace(/\s+(A|DE)SC$/, '').trim() 642 | if field in [idName, fullIdName, fromName, toName] 643 | switch field 644 | when idName then field = '_key' 645 | when fullIdName then field = '_id' 646 | when fromName then field = '_from' 647 | when toName then field = '_to' 648 | if m and m[1] is 'DE' 649 | aql = aql.sort(returnVariable + '.' + field, 'DESC') 650 | else 651 | aql = aql.sort(returnVariable + '.' + field, 'ASC') 652 | else if not @settings.disableDefaultSortByKey 653 | aql = aql.sort(returnVariable + '._key') 654 | 655 | if filter.limit 656 | aql = aql.limit(filter.skip, filter.limit) 657 | 658 | fields = _.clone(filter.fields) 659 | if fields 660 | indexId = fields.indexOf(idName) 661 | if indexId isnt -1 662 | fields[indexId] = '_key' 663 | indexFullId = fields.indexOf(fullIdName) 664 | if indexFullId isnt -1 665 | fields[indexFullId] = '_id' 666 | indexFromName = fields.indexOf(fromName) 667 | if indexFromName isnt -1 668 | fields[indexFromName] = '_from' 669 | indexToName = fields.indexOf(toName) 670 | if indexToName isnt -1 671 | fields[indexToName] = '_to' 672 | fields = ( '"' + field + '"' for field in fields) 673 | aql = aql.return(qb.fn('KEEP') returnVariable, fields) 674 | else 675 | aql = aql.return((qb.fn('UNSET') returnVariable, ['"_rev"'])) 676 | 677 | @execute model, 'query', aql, bindVars, (err, cursor) => 678 | return callback err if err 679 | cursorToArray = (r) => 680 | if _fieldIncluded filter.fields, idName 681 | @setIdValue model, r, r._key 682 | # Don't pass back _key if the fields is set 683 | if idName isnt '_key' then delete r._key; 684 | 685 | if fullIdName 686 | if _fieldIncluded filter.fields, fullIdName 687 | @_setFieldValue r, fullIdName, r._id 688 | if fullIdName isnt '_id' and idName isnt '_id' then delete r._id 689 | else 690 | if idName isnt '_id' then delete r._id 691 | 692 | if @_isEdge model 693 | if _fieldIncluded filter.fields, fromName 694 | @_setFieldValue r, fromName, r._from 695 | if fromName isnt '_from' then delete r._from 696 | if _fieldIncluded filter.fields, toName 697 | @_setFieldValue r, toName, r._to 698 | if toName isnt '_to' then delete r._to 699 | r = @fromDatabase(model, r) 700 | 701 | cursor.map cursorToArray, (err, result) => 702 | return callback err if err 703 | # filter include 704 | if filter.include? 705 | @_models[model].model.include result, filter.include, options, callback 706 | else 707 | callback null, result 708 | 709 | ### 710 | Delete a model instance by id 711 | @param model [String] model The model name 712 | @param id [String] id The id value 713 | @param options [Object] 714 | @param callback [Function] The callback function, called with a (possible) error object and the number of affected objects 715 | ### 716 | destroy: (model, id, options, callback) -> 717 | debug "delete for #{model} with id #{id}" if @debug 718 | 719 | @execute model, 'remove', id, (err, result) -> 720 | # Set error to null if API response is `document not found` 721 | if err and err.errorNum is 1202 then err = null 722 | callback and callback err, {count: if result and !result.error then 1 else 0} 723 | 724 | ### 725 | Delete all instances for the given model 726 | @param [String] model The model name 727 | @param [Object] [where] The filter for where 728 | @param options [Object] 729 | @param [Function] callback Callback with (possible) error object or the number of affected objects 730 | ### 731 | destroyAll: (model, where, options, callback) -> 732 | debug "destroyAll for #{model} with where #{JSON.stringify where}" if @debug 733 | 734 | if !callback && typeof where is 'function' 735 | callback = where 736 | where = undefined 737 | 738 | collection = @getCollectionName model 739 | bindVars = 740 | '@collection': collection 741 | aql = qb.for(returnVariable).in('@@collection') 742 | 743 | if !_.isEmpty(where) 744 | where = @_buildWhere model, where 745 | for w in where.aqlArray 746 | aql = aql.filter(w) 747 | merge true, bindVars, where.bindVars 748 | aql = aql.remove(returnVariable).in('@@collection') 749 | 750 | @execute model, 'query', aql, bindVars, (err, result) -> 751 | if callback 752 | return callback err if err 753 | callback null, {count: result.extra.stats.writesExecuted} 754 | 755 | ### 756 | Count the number of instances for the given model 757 | @param [String] model The model name 758 | @param [Function] callback Callback with (possible) error object or the number of affected objects 759 | @param [Object] where The filter for where 760 | ### 761 | count: (model, where, options, callback) -> 762 | debug "count for #{model} with where #{JSON.stringify where}" if @debug 763 | 764 | collection = @getCollectionName model 765 | bindVars = 766 | '@collection': collection 767 | aql = qb.for(returnVariable).in('@@collection') 768 | 769 | if !_.isEmpty(where) 770 | where = @_buildWhere model, where 771 | for w in where.aqlArray 772 | aql = aql.filter(w) 773 | merge true, bindVars, where.bindVars 774 | 775 | aql = aql.collectWithCountInto(returnVariable).return(returnVariable) 776 | 777 | @execute model, 'query', aql, bindVars, (err, result) -> 778 | return callback err if err 779 | callback null, result._result[0] 780 | 781 | ### 782 | Update properties for the model instance data 783 | @param [String] model The model name 784 | @param [String] id The models id 785 | @param [Object] data The model data 786 | @param [Object] options 787 | @param [Function] callback Callback with (possible) error object or the updated object 788 | ### 789 | updateAttributes: (model, id, data, options, callback) -> 790 | debug "updateAttributes for #{model} with id #{id} and data #{JSON.stringify data}" if @debug 791 | 792 | id = @getDefaultIdType() id 793 | idName = @idName(model) 794 | fullIdName = @_fullIdName model 795 | if fullIdName then delete data[fullIdName] 796 | 797 | isEdge = @_isEdge model 798 | fromName = null 799 | toName = null 800 | 801 | if isEdge 802 | fromName = @_fromName model 803 | delete data[fromName] 804 | toName = @_toName model 805 | delete data[toName] 806 | 807 | @execute model, 'update', id, data, options, (err, result) => 808 | if result 809 | delete result['_rev'] 810 | if idName isnt '_key' then delete result._key; 811 | @setIdValue(model, result, id); 812 | if fullIdName 813 | fullIdValue = result._id 814 | delete result._id 815 | result[fullIdName] = fullIdValue; 816 | if isEdge 817 | result[fromName] = data._from 818 | result[toName] = data._to 819 | callback and callback err, result 820 | 821 | ### 822 | Update matching instance 823 | @param [String] model The model name 824 | @param [Object] where The search criteria 825 | @param [Object] data The property/value pairs to be updated 826 | @param [Object] options 827 | @param [Function] callback Callback with (possible) error object or the number of affected objects 828 | ### 829 | update: (model, where, data, options, callback) -> 830 | debug "updateAll for #{model} with where #{JSON.stringify where} and data #{JSON.stringify data}" if @debug 831 | 832 | collection = @getCollectionName model 833 | bindVars = 834 | '@collection': collection 835 | data: data 836 | 837 | aql = qb.for(returnVariable).in('@@collection') 838 | if where 839 | where = @_buildWhere(model, where) 840 | for w in where.aqlArray 841 | aql = aql.filter(w) 842 | merge true, bindVars, where.bindVars 843 | aql = aql.update(returnVariable).with('@data').in('@@collection') 844 | # _id, _key _from and _to are are immutable once set and cannot be updated 845 | idName = @idName(model) 846 | delete data[idName] 847 | fullIdName = @_fullIdName model 848 | if fullIdName then delete data[fullIdName] 849 | if @_isEdge model 850 | fromName = @_fromName model 851 | delete data[fromName] 852 | toName = @_toName model 853 | delete data[toName] 854 | 855 | @execute model, 'query', aql, bindVars, (err, result) -> 856 | return callback err if err 857 | callback null, {count: result.extra.stats.writesExecuted} 858 | 859 | ### 860 | Update all matching instances 861 | ### 862 | updateAll: @::update 863 | 864 | ### 865 | Perform autoupdate for the given models. It basically calls ensureIndex 866 | @param [String[]] [models] A model name or an array of model names. If not present, apply to all models 867 | @param [Function] [cb] The callback function 868 | ### 869 | autoupdate: (models, cb) -> 870 | if @db 871 | debug 'autoupdate for model %s', models if @debug 872 | if (not cb) and (typeof models is 'function') 873 | cb = models 874 | models = undefined 875 | # First argument is a model name 876 | models = [models] if typeof models is 'string' 877 | models = models or Object.keys @_models 878 | async.each( models, ((model, modelCallback) => 879 | indexes = @_models[model].settings.indexes or [] 880 | indexList = [] 881 | index = {} 882 | options = {} 883 | 884 | if typeof indexes is 'object' 885 | for indexName, index of indexes 886 | if index.keys 887 | # the index object has keys 888 | options = index.options or {} 889 | options.name = options.name or indexName 890 | index.options = options 891 | else 892 | options = 893 | name: indexName 894 | index = 895 | keys: index 896 | options: options 897 | indexList.push index 898 | else if Array.isArray indexes 899 | indexList = indexList.concat indexes 900 | 901 | for propIdx, property of @_models[model].properties 902 | if property.index 903 | index = {} 904 | index[propIdx] = 1 905 | if typeof property.index is 'object' 906 | # If there is a arangodb key for the index, use it 907 | if typeof property.index.arangodb is 'object' 908 | options = property.index.arangodb 909 | index[propIdx] = options.kind or 1 910 | # Backwards compatibility for former type of indexes 911 | options.unique = true if property.index.uniqe is true 912 | else 913 | # If there isn't an properties[p].index.mongodb object, we read the properties from properties[p].index 914 | options = property.index 915 | options.background = true if options.background is undefined 916 | # If properties[p].index isn't an object we hardcode the background option and check for properties[p].unique 917 | else 918 | options = 919 | background: true 920 | options.unique = true if property.unique 921 | indexList.push {keys: index, options: options} 922 | 923 | debug 'create indexes' if @debug 924 | async.each( indexList, ((index, indexCallback) => 925 | debug 'createIndex: %s', index if @debug 926 | collection = @getCollection model 927 | collection.createIndex(index.fields || index.keys, index.options, indexCallback); 928 | ), modelCallback ) 929 | ), cb) 930 | else 931 | @dataSource.once 'connected', () -> @autoupdate models, cb 932 | 933 | ### 934 | Perform automigrate for the given models. It drops the corresponding collections and calls ensureIndex 935 | @param [String[]] [models] A model name or an array of model names. If not present, apply to all models 936 | @param [Function] [cb] The callback function 937 | ### 938 | automigrate: (models, cb) -> 939 | if @db 940 | debug "automigrate for model #{models}" if @debug 941 | if (not cb) and (typeof models is 'function') 942 | cb = models 943 | models = undefined 944 | # First argument is a model name 945 | models = [models] if typeof models is 'string' 946 | models = models || Object.keys @_models 947 | 948 | async.eachSeries(models, ((model, modelCallback) => 949 | collectionName = @getCollectionName model 950 | debug 'drop collection %s for model %s', collectionName, model 951 | collection = @getCollection model 952 | collection.drop (err) => 953 | if err 954 | if err.response.body? 955 | err = err.response.body 956 | # For errors other than 'collection not found' 957 | isCollectionNotFound = err.error is true and err.errorNum is 1203 and 958 | (err.errorMessage is 'unknown collection \'' + model + '\'' or err.errorMessage is 'collection not found') 959 | return modelCallback err if not isCollectionNotFound 960 | # Recreate the collection 961 | debug 'create collection %s for model %s', collectionName, model 962 | collection.create modelCallback 963 | ), ((err) => 964 | return cb and cb err 965 | @autoupdate models, cb 966 | )) 967 | else 968 | @dataSource.once 'connected', () -> @automigrate models cb 969 | 970 | exports.ArangoDBConnector = ArangoDBConnector 971 | -------------------------------------------------------------------------------- /test/core.test.coffee: -------------------------------------------------------------------------------- 1 | # This test written in mocha+should.js 2 | should = require('./init'); 3 | 4 | arangojs = require 'arangojs' 5 | qb = require 'aqb' 6 | chance = require('chance').Chance() 7 | arangodb = require '../src/arangodb' 8 | GeoPoint = require('loopback-datasource-juggler').GeoPoint 9 | 10 | describe 'arangodb core functionality', () -> 11 | ds = null 12 | before () -> 13 | ds = getDataSource() 14 | 15 | describe 'connecting', () -> 16 | before () -> 17 | simple_model = ds.define 'SimpleModel', { 18 | name: 19 | type: String 20 | } 21 | 22 | complex_model = ds.define 'ComplexModel', { 23 | name: 24 | type: String 25 | money: 26 | type: Number 27 | birthday: 28 | type: Date 29 | icon: 30 | type: Buffer 31 | active: 32 | type: Boolean 33 | likes: 34 | type: Array 35 | address: 36 | street: 37 | type: String 38 | house_number: 39 | type: String 40 | city: 41 | type: String 42 | zip: 43 | type: String 44 | country: 45 | type: String 46 | location: 47 | type: GeoPoint 48 | }, { 49 | arangodb: 50 | collection: 'Complex' 51 | } 52 | 53 | describe 'connection generator:', () -> 54 | it 'should create the default connection object when called with an empty settings object', (done) -> 55 | settings = {} 56 | 57 | connObj = arangodb.generateArangoDBURL settings 58 | connObj.should.eql 'http://127.0.0.1:8529' 59 | done() 60 | 61 | it 'should create an connection using the connection settings when url is not set', (done) -> 62 | settings = 63 | host: 'right_host' 64 | port: 32768 65 | database: 'rightDatabase' 66 | username: 'rightUser' 67 | password: 'rightPassword' 68 | promise: true 69 | 70 | connObj = arangodb.generateArangoDBURL settings 71 | connObj.should.eql 'http://rightUser:rightPassword@right_host:32768' 72 | done() 73 | 74 | describe 'authentication:', () -> 75 | wrongAuth = null 76 | it "should throw an error when using wrong credentials", (done) -> 77 | settings = 78 | password: 'wrong' 79 | wrongAuth = getDataSource settings 80 | `(function(){ 81 | wrongAuth.connector.query('FOR year in 2010..2013 RETURN year', function (err, cursor){ 82 | if (err) 83 | throw err; 84 | }); 85 | }).should.throw();` 86 | done() 87 | 88 | describe 'exposed properties:', () -> 89 | it 'should expose a property "db" to access the driver directly', (done) -> 90 | ds.connector.db.should.be.not.null 91 | ds.connector.db.should.be.Object 92 | ds.connector.db.should.be.arangojs 93 | done() 94 | 95 | it 'should expose a property "qb" to access the query builder directly', (done) -> 96 | ds.connector.qb.should.not.be.null 97 | ds.connector.qb.should.be.qb 98 | done() 99 | 100 | it 'should expose a property "api" to access the HTTP API directly', (done) -> 101 | ds.connector.api.should.not.be.null 102 | ds.connector.api.should.be.Object 103 | done() 104 | 105 | it 'should expose a function "version" which callback with the version of the database', (done) -> 106 | ds.connector.getVersion (err, result) -> 107 | return done err if err 108 | result.should.exist 109 | result.should.have.properties 'server', 'version' 110 | result.version.should.match /[0-9]+\.[0-9]+\.[0-9]+/ 111 | done() 112 | 113 | describe 'connector details:', () -> 114 | it 'should provide a function "getTypes" which returns the array ["db", "nosql", "arangodb"]', (done) -> 115 | types = ds.connector.getTypes() 116 | types.should.not.be.null 117 | types.should.be.Array 118 | types.length.should.be.above(2) 119 | types.should.eql ['db', 'nosql', 'arangodb'] 120 | done() 121 | 122 | it 'should provide a function "getDefaultIdType" that returns String', (done) -> 123 | defaultIdType = ds.connector.getDefaultIdType() 124 | defaultIdType.should.not.be.null 125 | defaultIdType.should.be.a.class 126 | done() 127 | 128 | it "should convert ArangoDB Types to the respective Loopback Data Types", (done) -> 129 | firstName = chance.first() 130 | lastName = chance.last() 131 | birthdate = chance.birthday({american: false}) 132 | money = chance.integer {min: 100, max: 1000} 133 | lat = chance.latitude() 134 | lng = chance.longitude() 135 | fromDB = 136 | name: 137 | first: firstName 138 | last: lastName 139 | profession: 'Node Developer' 140 | money: money 141 | birthday: birthdate 142 | icon: new Buffer('a20').toJSON() 143 | active: true 144 | likes: ['nodejs', 'loopback'] 145 | location: 146 | lat: lat 147 | lng: lng 148 | 149 | jsonData = ds.connector.fromDatabase 'ComplexModel', fromDB 150 | expected = 151 | name: 152 | first: firstName 153 | last: lastName 154 | profession: 'Node Developer' 155 | money: money 156 | birthday: birthdate 157 | icon: new Buffer('a20') 158 | active: true 159 | likes: ['nodejs', 'loopback'] 160 | location: new GeoPoint {lat: lat, lng: lng} 161 | 162 | jsonData.should.eql expected 163 | done() 164 | 165 | describe 'connector access', () -> 166 | it "should get the collection name from the name of the model", (done) -> 167 | simpleCollection = ds.connector.getCollectionName 'SimpleModel' 168 | simpleCollection.should.not.be.null 169 | simpleCollection.should.be.a.String 170 | simpleCollection.should.eql 'SimpleModel' 171 | done() 172 | 173 | it "should get the collection name from the 'name' property on the 'arangodb' property", (done) -> 174 | complexCollection = ds.connector.getCollectionName 'ComplexModel' 175 | complexCollection.should.not.be.null 176 | complexCollection.should.be.a.String 177 | complexCollection.should.eql 'Complex' 178 | done() 179 | 180 | describe 'querying', () -> 181 | it "should execute a AQL query with no variables provided as a string", (done) -> 182 | aql_query_string = [ 183 | "/* Returns the sequence of integers between 2010 and 2013 (including) */", 184 | "FOR year IN 2010..2013", 185 | " RETURN year" 186 | ].join("\n") 187 | 188 | ds.connector.db.query aql_query_string, (err, cursor) -> 189 | return done err if err 190 | cursor.should.exist 191 | cursor.all (err, values) -> 192 | return done err if err 193 | values.should.not.be.null 194 | values.should.be.a.Array 195 | values.should.eql [2010, 2011, 2012, 2013] 196 | done() 197 | 198 | it "should execute a AQL query with bound variables provided as a string", (done) -> 199 | aql_query_string = [ 200 | "/* Returns the sequence of integers between 2010 and 2013 (including) */", 201 | "FOR year IN 2010..2013", 202 | " LET following_year = year + @difference", 203 | " RETURN { year: year, following: following_year }" 204 | ].join("\n") 205 | 206 | ds.connector.db.query aql_query_string, {difference: 1}, (err, cursor) -> 207 | return done err if err 208 | cursor.should.exist 209 | cursor.all (err, values) -> 210 | return done err if err 211 | values.should.not.be.null 212 | values.should.be.a.Array 213 | values.should.eql [{year: 2010, following: 2011}, {year: 2011, following: 2012}, 214 | {year: 2012, following: 2013}, {year: 2013, following: 2014}] 215 | done() 216 | 217 | it "should execute a AQL query with no variables provided using the query builder object", (done) -> 218 | aql_query_object = ds.connector.qb.for('year').in('2010..2013').return('year') 219 | 220 | ds.connector.db.query aql_query_object, (err, cursor) -> 221 | return done err if err 222 | cursor.should.exist 223 | cursor.all (err, values) -> 224 | return done err if err 225 | values.should.not.be.null 226 | values.should.be.a.Array 227 | values.should.eql [2010, 2011, 2012, 2013] 228 | done() 229 | 230 | it "should execute a AQL query with bound variables provided using the query builder object", (done) -> 231 | qb = ds.connector.qb 232 | aql = qb.for('year').in('2010..2013') 233 | aql = aql.let 'following', qb.add(qb.ref('year'), qb.ref('@difference')) 234 | aql = aql.return { 235 | year: qb.ref('year'), 236 | following: qb.ref('following') 237 | } 238 | 239 | ds.connector.db.query aql, {difference: 1}, (err, cursor) -> 240 | return done err if err 241 | cursor.should.exist 242 | cursor.all (err, values) -> 243 | return done err if err 244 | values.should.not.be.null 245 | values.should.be.a.Array 246 | values.should.eql [{year: 2010, following: 2011}, {year: 2011, following: 2012}, 247 | {year: 2012, following: 2013}, {year: 2013, following: 2014}] 248 | done() 249 | -------------------------------------------------------------------------------- /test/crud.test.coffee: -------------------------------------------------------------------------------- 1 | # This test written in mocha+should.js 2 | describe 'arangodb connector crud', () -> 3 | 4 | require('./crud/document.test') 5 | require('./crud/edge.test') 6 | 7 | -------------------------------------------------------------------------------- /test/crud/document.test.coffee: -------------------------------------------------------------------------------- 1 | # This test written in mocha+should.js 2 | should = require('./../init'); 3 | 4 | describe 'document', () -> 5 | db = null 6 | User = null 7 | Post = null 8 | Product = null 9 | PostWithNumberId = null 10 | PostWithStringId = null 11 | PostWithStringKey = null 12 | PostWithNumberUnderscoreId = null 13 | Name = null 14 | 15 | before (done) -> 16 | db = getDataSource() 17 | 18 | User = db.define('User', { 19 | name: {type: String, index: true}, 20 | email: {type: String, index: true, unique: true}, 21 | age: Number, 22 | icon: Buffer 23 | }, { 24 | indexes: { 25 | name_age_index: { 26 | keys: {name: 1, age: -1} 27 | }, # The value contains keys and optinally options 28 | age_index: {age: -1} # The value itself is for keys 29 | } 30 | }); 31 | 32 | Post = db.define('Post', { 33 | title: {type: String, length: 255, index: true}, 34 | content: {type: String}, 35 | comments: [String] 36 | }, 37 | {forceId: false}); 38 | 39 | Product = db.define('Product', { 40 | name: {type: String, length: 255, index: true}, 41 | description: {type: String}, 42 | price: {type: Number}, 43 | pricehistory: {type: Object} 44 | }); 45 | 46 | PostWithStringId = db.define('PostWithStringId', { 47 | id: {type: String, id: true}, 48 | title: { type: String, length: 255, index: true }, 49 | content: { type: String } 50 | }); 51 | 52 | PostWithStringKey = db.define('PostWithStringKey', { 53 | _key: {type: String, id: true}, 54 | title: { type: String, length: 255, index: true }, 55 | content: { type: String } 56 | }); 57 | 58 | PostWithNumberUnderscoreId = db.define('PostWithNumberUnderscoreId', { 59 | _id: {type: Number, id: true}, 60 | title: { type: String, length: 255, index: true }, 61 | content: { type: String } 62 | }); 63 | 64 | PostWithNumberId = db.define('PostWithNumberId', { 65 | id: {type: Number, id: true}, 66 | title: { type: String, length: 255, index: true }, 67 | content: { type: String } 68 | }); 69 | 70 | Name = db.define('Name', {}, {}); 71 | 72 | User.hasMany(Post); 73 | Post.belongsTo(User); 74 | 75 | db.automigrate(['User', 'Post', 'Product', 'PostWithStringId', 'PostWithStringKey', 76 | 'PostWithNumberUnderscoreId','PostWithNumberId'], done) 77 | 78 | beforeEach (done) -> 79 | User.settings.arangodb = {}; 80 | User.destroyAll -> 81 | Post.destroyAll -> 82 | PostWithNumberId.destroyAll -> 83 | PostWithNumberUnderscoreId.destroyAll -> 84 | PostWithStringId.destroyAll -> 85 | PostWithStringKey.destroyAll(done) 86 | 87 | it 'should handle correctly type Number for id field _id', (done) -> 88 | PostWithNumberUnderscoreId.create {_id: 3, content: 'test'}, (err, person) -> 89 | should.not.exist(err) 90 | person._id.should.be.equal(3) 91 | PostWithNumberUnderscoreId.findById person._id, (err, p) -> 92 | should.not.exist(err) 93 | p.content.should.be.equal('test') 94 | 95 | done() 96 | 97 | it 'should handle correctly type Number for id field _id using String', (done) -> 98 | PostWithNumberUnderscoreId.create {_id: 4, content: 'test'}, (err, person) -> 99 | should.not.exist(err) 100 | person._id.should.be.equal(4); 101 | PostWithNumberUnderscoreId.findById '4', (err, p) -> 102 | should.not.exist(err) 103 | p.content.should.be.equal('test'); 104 | done() 105 | 106 | it 'should allow to find post by id string if `_id` is defined id', (done) -> 107 | PostWithNumberUnderscoreId.create (err, post) -> 108 | PostWithNumberUnderscoreId.find {where: {_id: post._id.toString()}}, (err, p) -> 109 | should.not.exist(err) 110 | post = p[0] 111 | should.exist(post) 112 | post._id.should.be.an.instanceOf(Number); 113 | done() 114 | 115 | it 'find with `_id` as defined id should return an object with _id instanceof String', (done) -> 116 | PostWithNumberUnderscoreId.create (err, post) -> 117 | PostWithNumberUnderscoreId.findById post._id, (err, post) -> 118 | should.not.exist(err) 119 | post._id.should.be.an.instanceOf(Number) 120 | done() 121 | 122 | it 'should update the instance with `_id` as defined id', (done) -> 123 | PostWithNumberUnderscoreId.create {title: 'a', content: 'AAA'}, (err, post) -> 124 | post.title = 'b' 125 | PostWithNumberUnderscoreId.updateOrCreate post, (err, p) -> 126 | should.not.exist(err) 127 | p._id.should.be.equal(post._id) 128 | PostWithNumberUnderscoreId.findById post._id, (err, p) -> 129 | should.not.exist(err) 130 | p._id.should.be.eql(post._id) 131 | p.content.should.be.equal(post.content) 132 | p.title.should.be.equal('b') 133 | PostWithNumberUnderscoreId.find {where: {title: 'b'}}, (err, posts) -> 134 | should.not.exist(err) 135 | p = posts[0] 136 | p._id.should.be.eql(post._id) 137 | p.content.should.be.equal(post.content) 138 | p.title.should.be.equal('b') 139 | posts.should.have.lengthOf(1) 140 | done() 141 | 142 | it 'all should return object (with `_id` as defined id) with an _id instanceof String', (done) -> 143 | post = new PostWithNumberUnderscoreId({title: 'a', content: 'AAA'}) 144 | post.save (err, post) -> 145 | PostWithNumberUnderscoreId.all {where: {title: 'a'}}, (err, posts) -> 146 | should.not.exist(err) 147 | posts.should.have.lengthOf(1) 148 | post = posts[0] 149 | post.should.have.property('title', 'a') 150 | post.should.have.property('content', 'AAA') 151 | post._id.should.be.an.instanceOf(Number) 152 | done() 153 | 154 | it 'all return should honor filter.fields, with `_id` as defined id', (done) -> 155 | post = new PostWithNumberUnderscoreId {title: 'a', content: 'AAA'} 156 | post.save (err, post) -> 157 | PostWithNumberUnderscoreId.all {fields: ['title'], where: {title: 'a'}}, (err, posts) -> 158 | should.not.exist(err) 159 | posts.should.have.lengthOf(1) 160 | post = posts[0] 161 | post.should.have.property('title', 'a') 162 | post.should.have.property('content', undefined) 163 | should.not.exist(post._id) 164 | done() 165 | 166 | it 'should allow to find post by id string if `_key` is defined id', (done) -> 167 | PostWithStringKey.create (err, post) -> 168 | PostWithStringKey.find {where: {_key: post._key.toString()}}, (err, p) -> 169 | should.not.exist(err) 170 | post = p[0] 171 | should.exist(post) 172 | post._key.should.be.an.instanceOf(String); 173 | done() 174 | 175 | it 'find with `_key` as defined id should return an object with _key instanceof String', (done) -> 176 | PostWithStringKey.create (err, post) -> 177 | PostWithStringKey.findById post._key, (err, post) -> 178 | should.not.exist(err) 179 | post._key.should.be.an.instanceOf(String) 180 | done() 181 | 182 | it 'should update the instance with `_key` as defined id', (done) -> 183 | PostWithStringKey.create {title: 'a', content: 'AAA'}, (err, post) -> 184 | post.title = 'b' 185 | PostWithStringKey.updateOrCreate post, (err, p) -> 186 | should.not.exist(err) 187 | p._key.should.be.equal(post._key) 188 | PostWithStringKey.findById post._key, (err, p) -> 189 | should.not.exist(err) 190 | p._key.should.be.eql(post._key) 191 | p.content.should.be.equal(post.content) 192 | p.title.should.be.equal('b') 193 | PostWithStringKey.find {where: {title: 'b'}}, (err, posts) -> 194 | should.not.exist(err) 195 | p = posts[0] 196 | p._key.should.be.eql(post._key) 197 | p.content.should.be.equal(post.content) 198 | p.title.should.be.equal('b') 199 | posts.should.have.lengthOf(1) 200 | done() 201 | 202 | it 'all should return object (with `_key` as defined id) with an _key instanceof String', (done) -> 203 | post = new PostWithStringKey({title: 'a', content: 'AAA'}) 204 | post.save (err, post) -> 205 | PostWithStringKey.all {where: {title: 'a'}}, (err, posts) -> 206 | should.not.exist(err) 207 | posts.should.have.lengthOf(1) 208 | post = posts[0] 209 | post.should.have.property('title', 'a') 210 | post.should.have.property('content', 'AAA') 211 | post._key.should.be.an.instanceOf(String) 212 | done() 213 | 214 | it 'all return should honor filter.fields, with `_key` as defined id', (done) -> 215 | post = new PostWithStringKey {title: 'a', content: 'AAA'} 216 | post.save (err, post) -> 217 | PostWithStringKey.all {fields: ['title'], where: {title: 'a'}}, (err, posts) -> 218 | should.not.exist(err) 219 | posts.should.have.lengthOf(1) 220 | post = posts[0] 221 | post.should.have.property('title', 'a') 222 | post.should.have.property('content', undefined) 223 | should.not.exist(post._key) 224 | done() 225 | 226 | it 'should have created simple User models', (done) -> 227 | User.create {age: 3, content: 'test'}, (err, user) -> 228 | should.not.exist(err) 229 | user.age.should.be.equal(3) 230 | user.content.should.be.equal('test') 231 | user.id.should.not.be.null 232 | should.not.exists user._key 233 | done() 234 | 235 | it 'should support Buffer type', (done) -> 236 | User.create {name: 'John', icon: new Buffer('1a2')}, (err, u) -> 237 | User.findById u.id, (err, user) -> 238 | should.not.exist(err) 239 | user.icon.should.be.an.instanceOf(Buffer) 240 | done() 241 | 242 | it 'hasMany should support additional conditions', (done) -> 243 | User.create {}, (e, u) -> 244 | u.posts.create (e, p) -> 245 | u.posts {where: {id: p.id}}, (err, posts) -> 246 | should.not.exist(err) 247 | posts.should.have.lengthOf(1) 248 | done() 249 | 250 | it 'create should return id field but not arangodb _key', (done) -> 251 | Post.create {title: 'Post1', content: 'Post content'}, (err, post) -> 252 | should.not.exist(err) 253 | should.exist(post.id) 254 | should.not.exist(post._key) 255 | should.not.exist(post._id) 256 | done() 257 | 258 | it 'should allow to find by id string', (done) -> 259 | Post.create {title: 'Post1', content: 'Post content'}, (err, post) -> 260 | Post.findById post.id.toString(), (err, p) -> 261 | should.not.exist(err) 262 | should.exist(p) 263 | done() 264 | 265 | it 'should allow to find by id using where', (done) -> 266 | Post.create {title: 'Post1', content: 'Post1 content'}, (err, p1) -> 267 | Post.create {title: 'Post2', content: 'Post2 content'}, (err, p2) -> 268 | Post.find {where: {id: p1.id}}, (err, p) -> 269 | should.not.exist(err) 270 | should.exist(p && p[0]) 271 | p.length.should.be.equal(1) 272 | #Not strict equal 273 | p[0].id.should.be.eql(p1.id) 274 | done() 275 | 276 | it 'should allow to find by id using where inq', (done) -> 277 | Post.create {title: 'Post1', content: 'Post1 content'}, (err, p1) -> 278 | Post.create {title: 'Post2', content: 'Post2 content'}, (err, p2) -> 279 | Post.find {where: {id: {inq: [p1.id]}}}, (err, p) -> 280 | should.not.exist(err) 281 | should.exist(p && p[0]) 282 | p.length.should.be.equal(1) 283 | #Not strict equal 284 | p[0].id.should.be.eql(p1.id) 285 | done() 286 | 287 | it 'inq operator respect type of field', (done) -> 288 | User.create [ 289 | {age: 3, name: 'user0'}, 290 | {age: 2, name: 'user1'}, 291 | {age: 4, name: 'user3'}], 292 | (err, users) -> 293 | should.not.exist(err) 294 | users.should.be.instanceof(Array).and.have.lengthOf(3); 295 | User.find {where: {or: 296 | [ 297 | {age: {inq: [3]}}, 298 | {name: {inq: ['user3']}} 299 | ] 300 | }}, (err, founds) -> 301 | should.not.exist(err) 302 | should.exist(founds) 303 | founds.should.be.instanceof(Array).and.have.lengthOf(2); 304 | founds.should.containDeep({id: users[0], id: users[3]}) 305 | done() 306 | 307 | it 'should invoke hooks', (done) -> 308 | events = [] 309 | connector = Post.getDataSource().connector 310 | connector.observe 'before execute', (ctx, next) -> 311 | ctx.req.command.should.be.string; 312 | ctx.req.params.should.be.array; 313 | events.push('before execute ' + ctx.req.command); 314 | next() 315 | 316 | connector.observe 'after execute', (ctx, next) -> 317 | ctx.res.should.be.object; 318 | events.push('after execute ' + ctx.req.command); 319 | next() 320 | 321 | Post.create {title: 'Post1', content: 'Post1 content'}, (err, p1) -> 322 | Post.find (err, results) -> 323 | events.should.eql(['before execute save', 'after execute save', 324 | 'before execute document', 'after execute document']) 325 | connector.clearObservers 'before execute' 326 | connector.clearObservers 'after execute' 327 | done(err, results) 328 | 329 | 330 | it 'should allow to find by number id using where', (done) -> 331 | PostWithNumberId.create {id: 1, title: 'Post1', content: 'Post1 content'}, (err, p1) -> 332 | PostWithNumberId.create {id: 2, title: 'Post2', content: 'Post2 content'}, (err, p2) -> 333 | PostWithNumberId.find {where: {id: p1.id}}, (err, p) -> 334 | should.not.exist(err) 335 | should.exist(p && p[0]) 336 | p.length.should.be.equal(1) 337 | p[0].id.should.be.eql(p1.id) 338 | done() 339 | 340 | it 'should allow to find by number id using where inq', (done) -> 341 | PostWithNumberId.create {id: 1, title: 'Post1', content: 'Post1 content'}, (err, p1) -> 342 | return done err if err 343 | PostWithNumberId.create {id: 2, title: 'Post2', content: 'Post2 content'}, (err, p2) -> 344 | return done err if err 345 | filter = {where: {id: {inq: [1]}}} 346 | PostWithNumberId.find filter, (err, p) -> 347 | return done err if err 348 | p.length.should.be.equal(1) 349 | p[0].id.should.be.eql(p1.id) 350 | PostWithNumberId.find {where: {id: {inq: [1, 2]}}}, (err, p) -> 351 | return done err if err 352 | p.length.should.be.equal(2) 353 | p[0].id.should.be.eql(p1.id) 354 | p[1].id.should.be.eql(p2.id) 355 | PostWithNumberId.find {where: {id: {inq: [0]}}}, (err, p) -> 356 | return done err if err 357 | p.length.should.be.equal(0) 358 | done() 359 | 360 | it 'save should not return arangodb _key and _rev', (done) -> 361 | Post.create {title: 'Post1', content: 'Post content'}, (err, post) -> 362 | post.content = 'AAA' 363 | post.save (err, p) -> 364 | should.not.exist(err) 365 | should.not.exist(p._key) 366 | should.not.exist(p._rev) 367 | p.id.should.be.equal(post.id) 368 | p.content.should.be.equal('AAA') 369 | done() 370 | 371 | it 'find should return an object with an id, which is instanceof String, but not arangodb _key', (done) -> 372 | Post.create {title: 'Post1', content: 'Post content'}, (err, post) -> 373 | Post.findById post.id, (err, post) -> 374 | should.not.exist(err) 375 | post.id.should.be.an.instanceOf(String) 376 | should.not.exist(post._key) 377 | done() 378 | 379 | 380 | it 'should update attribute of the specific instance', (done) -> 381 | User.create {name: 'Al', age: 31, email:'al@'}, (err, createdusers) -> 382 | createdusers.updateAttributes {age: 32, email:'al@strongloop'}, (err, updated) -> 383 | should.not.exist(err) 384 | updated.age.should.be.equal(32) 385 | updated.email.should.be.equal('al@strongloop') 386 | done() 387 | 388 | # MEMO: Import data present into data/users/names_100000.json before running this test. 389 | it.skip 'cursor should returns all documents more then max single default size (1000) ', (done) -> 390 | # Increase timeout only for this test 391 | this.timeout(20000); 392 | Name.find (err, names) -> 393 | should.not.exist(err) 394 | names.length.should.be.equal(100000) 395 | done() 396 | 397 | it.skip 'cursor should returns all documents more then max single default cursor size (1000) and respect limit filter ', (done) -> 398 | # Increase timeout only for this test 399 | this.timeout(20000); 400 | Name.find {limit: 1002}, (err, names) -> 401 | should.not.exist(err) 402 | names.length.should.be.equal(1002) 403 | done() 404 | 405 | describe 'updateAll', () -> 406 | 407 | it 'should update the instance matching criteria', (done) -> 408 | User.create {name: 'Al', age: 31, email:'al@strongloop'}, (err, createdusers) -> 409 | User.create {name: 'Simon', age: 32, email:'simon@strongloop'}, (err, createdusers) -> 410 | User.create {name: 'Ray', age: 31, email:'ray@strongloop'}, (err, createdusers) -> 411 | User.updateAll {age:31},{company:'strongloop.com'}, (err, updatedusers) -> 412 | should.not.exist(err) 413 | updatedusers.should.have.property('count', 2); 414 | User.find {where:{age:31}}, (err2, foundusers) -> 415 | should.not.exist(err2) 416 | foundusers[0].company.should.be.equal('strongloop.com') 417 | foundusers[1].company.should.be.equal('strongloop.com') 418 | done() 419 | 420 | 421 | it 'updateOrCreate should update the instance', (done) -> 422 | Post.create {title: 'a', content: 'AAA'}, (err, post) -> 423 | post.title = 'b' 424 | Post.updateOrCreate post, (err, p) -> 425 | should.not.exist(err) 426 | p.id.should.be.equal(post.id) 427 | p.content.should.be.equal(post.content) 428 | should.not.exist(p._key) 429 | Post.findById post.id, (err, p) -> 430 | p.id.should.be.eql(post.id) 431 | should.not.exist(p._key) 432 | p.content.should.be.equal(post.content) 433 | p.title.should.be.equal('b') 434 | done() 435 | 436 | it 'updateOrCreate should update the instance without removing existing properties', (done) -> 437 | Post.create {title: 'a', content: 'AAA', comments: ['Comment1']}, (err, post) -> 438 | post = post.toObject() 439 | delete post.title 440 | delete post.comments; 441 | Post.updateOrCreate post, (err, p) -> 442 | should.not.exist(err) 443 | p.id.should.be.equal(post.id) 444 | p.content.should.be.equal(post.content) 445 | should.not.exist(p._key) 446 | Post.findById post.id, (err, p) -> 447 | p.id.should.be.eql(post.id) 448 | should.not.exist(p._key) 449 | p.content.should.be.equal(post.content) 450 | p.title.should.be.equal('a') 451 | p.comments[0].should.be.equal('Comment1') 452 | done() 453 | 454 | it 'updateOrCreate should create a new instance if it does not exist', (done) -> 455 | post = {id: '123', title: 'a', content: 'AAA'}; 456 | Post.updateOrCreate post, (err, p) -> 457 | should.not.exist(err) 458 | p.title.should.be.equal(post.title) 459 | p.content.should.be.equal(post.content) 460 | p.id.should.be.eql(post.id) 461 | Post.findById p.id, (err, p) -> 462 | p.id.should.be.equal(post.id) 463 | should.not.exist(p._key) 464 | p.content.should.be.equal(post.content) 465 | p.title.should.be.equal(post.title) 466 | p.id.should.be.equal(post.id) 467 | done() 468 | 469 | it 'save should update the instance with the same id', (done) -> 470 | Post.create {title: 'a', content: 'AAA'}, (err, post) -> 471 | post.title = 'b'; 472 | post.save (err, p) -> 473 | should.not.exist(err) 474 | p.id.should.be.equal(post.id) 475 | p.content.should.be.equal(post.content) 476 | should.not.exist(p._key) 477 | Post.findById post.id, (err, p) -> 478 | p.id.should.be.eql(post.id) 479 | should.not.exist(p._key) 480 | p.content.should.be.equal(post.content) 481 | p.title.should.be.equal('b') 482 | done() 483 | 484 | it 'save should update the instance without removing existing properties', (done) -> 485 | Post.create {title: 'a', content: 'AAA'}, (err, post) -> 486 | delete post.title 487 | post.save (err, p) -> 488 | should.not.exist(err) 489 | p.id.should.be.equal(post.id) 490 | p.content.should.be.equal(post.content) 491 | should.not.exist(p._key) 492 | Post.findById post.id, (err, p) -> 493 | p.id.should.be.eql(post.id) 494 | should.not.exist(p._key) 495 | p.content.should.be.equal(post.content) 496 | p.title.should.be.equal('a') 497 | done() 498 | 499 | it 'save should create a new instance if it does not exist', (done) -> 500 | post = new Post {title: 'a', content: 'AAA'} 501 | post.save post, (err, p) -> 502 | should.not.exist(err) 503 | p.title.should.be.equal(post.title); 504 | p.content.should.be.equal(post.content); 505 | p.id.should.be.equal(post.id) 506 | Post.findById p.id, (err, p) -> 507 | p.id.should.be.equal(post.id) 508 | should.not.exist(p._key) 509 | p.content.should.be.equal(post.content) 510 | p.title.should.be.equal(post.title) 511 | p.id.should.be.equal(post.id) 512 | done() 513 | 514 | it 'all should return object with an id, which is instanceof String, but not arangodb _key', (done) -> 515 | post = new Post {title: 'a', content: 'AAA'} 516 | post.save (err, post) -> 517 | Post.all {where: {title: 'a'}}, (err, posts) -> 518 | should.not.exist(err) 519 | posts.should.have.lengthOf(1) 520 | post = posts[0] 521 | post.should.have.property('title', 'a') 522 | post.should.have.property('content', 'AAA') 523 | post.id.should.be.an.instanceOf(String) 524 | should.not.exist(post._key) 525 | done() 526 | 527 | it 'all return should honor filter.fields', (done) -> 528 | post = new Post {title: 'b', content: 'BBB'} 529 | post.save (err, post) -> 530 | Post.all {fields: ['title'], where: {content: 'BBB'}}, (err, posts) -> 531 | should.not.exist(err) 532 | posts.should.have.lengthOf(1) 533 | post = posts[0] 534 | post.should.have.property('title', 'b') 535 | post.should.have.property('content', undefined) 536 | should.not.exist(post._key) 537 | should.not.exist(post.id) 538 | done() 539 | 540 | it 'find should order by id if the order is not set for the query filter', (done) -> 541 | PostWithStringId.create {id: '2', title: 'c', content: 'CCC'}, (err, post) -> 542 | PostWithStringId.create {id: '1', title: 'd', content: 'DDD'}, (err, post) -> 543 | PostWithStringId.find (err, posts) -> 544 | should.not.exist(err) 545 | posts.length.should.be.equal(2) 546 | posts[0].id.should.be.equal('1') 547 | PostWithStringId.find {limit: 1, offset: 0}, (err, posts) -> 548 | should.not.exist(err) 549 | posts.length.should.be.equal(1) 550 | posts[0].id.should.be.equal('1') 551 | PostWithStringId.find {limit: 1, offset: 1}, (err, posts) -> 552 | should.not.exist(err) 553 | posts.length.should.be.equal(1) 554 | posts[0].id.should.be.equal('2') 555 | done() 556 | 557 | it 'order by specific query filter', (done) -> 558 | PostWithStringId.create {id: '2', title: 'c', content: 'CCC'}, (err, post) -> 559 | PostWithStringId.create {id: '1', title: 'd', content: 'DDD'}, (err, post) -> 560 | PostWithStringId.create {id: '3', title: 'd', content: 'AAA'}, (err, post) -> 561 | PostWithStringId.find {order: ['title DESC', 'content ASC']}, (err, posts) -> 562 | posts.length.should.be.equal(3) 563 | posts[0].id.should.be.equal('3') 564 | PostWithStringId.find {order: ['title DESC', 'content ASC'], limit: 1, offset: 0}, (err, posts) -> 565 | should.not.exist(err) 566 | posts.length.should.be.equal(1) 567 | posts[0].id.should.be.equal('3') 568 | PostWithStringId.find {order: ['title DESC', 'content ASC'], limit: 1, offset: 1}, (err, posts) -> 569 | should.not.exist(err) 570 | posts.length.should.be.equal(1) 571 | posts[0].id.should.be.equal('2') 572 | PostWithStringId.find {order: ['title DESC', 'content ASC'], limit: 1, offset: 2}, (err, posts) -> 573 | should.not.exist(err) 574 | posts.length.should.be.equal(1) 575 | posts[0].id.should.be.equal('1') 576 | done() 577 | 578 | it 'should report error on duplicate keys', (done) -> 579 | Post.create {title: 'd', content: 'DDD'}, (err, post) -> 580 | Post.create {id: post.id, title: 'd', content: 'DDD'}, (err, post) -> 581 | should.exist(err) 582 | done() 583 | 584 | it 'should allow to find using like', (done) -> 585 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 586 | Post.find {where: {title: {like: 'M%st'}}}, (err, posts) -> 587 | should.not.exist(err) 588 | posts.should.have.property('length', 1) 589 | done() 590 | 591 | it 'should allow to find using case insensitive like', (done) -> 592 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 593 | Post.find {where: {title: {like: 'm%st', options: 'i'}}}, (err, posts) -> 594 | should.not.exist(err) 595 | posts.should.have.property('length', 1) 596 | done() 597 | 598 | it 'should allow to find using case insensitive like', (done) -> 599 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 600 | Post.find {where: {content: {like: 'HELLO', options: 'i'}}}, (err, posts) -> 601 | should.not.exist(err) 602 | posts.should.have.property('length', 1) 603 | done() 604 | 605 | it 'should support like for no match', (done) -> 606 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 607 | Post.find {where: {title: {like: 'M%XY'}}}, (err, posts) -> 608 | should.not.exist(err) 609 | posts.should.have.property('length', 0) 610 | done() 611 | 612 | it 'should allow to find using nlike', (done) -> 613 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 614 | Post.find {where: {title: {nlike: 'M%st'}}}, (err, posts) -> 615 | should.not.exist(err) 616 | posts.should.have.property('length', 0) 617 | done() 618 | 619 | it 'should allow to find using case insensitive nlike', (done) -> 620 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 621 | Post.find {where: {title: {nlike: 'm%st', options: 'i'}}}, (err, posts) -> 622 | should.not.exist(err) 623 | posts.should.have.property('length', 0) 624 | done() 625 | 626 | it 'should support nlike for no match', (done) -> 627 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 628 | Post.find {where: {title: {nlike: 'M%XY'}}}, (err, posts) -> 629 | should.not.exist(err) 630 | posts.should.have.property('length', 1) 631 | done() 632 | 633 | it 'should support "and" operator that is satisfied', (done) -> 634 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 635 | Post.find {where: {and: [{title: 'My Post'}, {content: 'Hello'}]}}, (err, posts) -> 636 | should.not.exist(err) 637 | posts.should.have.property('length', 1) 638 | done() 639 | 640 | it 'should support "and" operator that is not satisfied', (done) -> 641 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 642 | Post.find {where: {and: [{title: 'My Post'}, {content: 'Hello1'}]}}, (err, posts) -> 643 | should.not.exist(err) 644 | posts.should.have.property('length', 0) 645 | done() 646 | 647 | it 'should support "or" that is satisfied', (done) -> 648 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 649 | Post.find {where: {or: [{title: 'My Post'}, {content: 'Hello1'}]}}, (err, posts) -> 650 | should.not.exist(err) 651 | posts.should.have.property('length', 1) 652 | done() 653 | 654 | it 'should support "or" operator that is not satisfied', (done) -> 655 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 656 | Post.find {where: {or: [{title: 'My Post1'}, {content: 'Hello1'}]}}, (err, posts) -> 657 | should.not.exist(err) 658 | posts.should.have.property('length', 0) 659 | done() 660 | 661 | # TODO: Add support to "nor" 662 | # it 'should support "nor" operator that is satisfied', (done) -> 663 | # 664 | # Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 665 | # Post.find {where: {nor: [{title: 'My Post1'}, {content: 'Hello1'}]}}, (err, posts) -> 666 | # should.not.exist(err) 667 | # posts.should.have.property('length', 1) 668 | # 669 | # done() 670 | # 671 | # it 'should support "nor" operator that is not satisfied', (done) -> 672 | # 673 | # Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 674 | # Post.find {where: {nor: [{title: 'My Post'}, {content: 'Hello1'}]}}, (err, posts) -> 675 | # should.not.exist(err) 676 | # posts.should.have.property('length', 0) 677 | # 678 | # done() 679 | 680 | it 'should support neq for match', (done) -> 681 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 682 | Post.find {where: {title: {neq: 'XY'}}}, (err, posts) -> 683 | should.not.exist(err) 684 | posts.should.have.property('length', 1) 685 | done() 686 | 687 | it 'should support neq for no match', (done) -> 688 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 689 | Post.find {where: {title: {neq: 'My Post'}}}, (err, posts) -> 690 | should.not.exist(err) 691 | posts.should.have.property('length', 0) 692 | done() 693 | 694 | # The where object should be parsed by the connector 695 | it 'should support where for count', (done) -> 696 | Post.create {title: 'My Post', content: 'Hello'}, (err, post) -> 697 | Post.count {and: [{title: 'My Post'}, {content: 'Hello'}]}, (err, count) -> 698 | should.not.exist(err) 699 | count.should.be.equal(1) 700 | Post.count {and: [{title: 'My Post1'}, {content: 'Hello'}]}, (err, count) -> 701 | should.not.exist(err) 702 | count.should.be.equal(0) 703 | done() 704 | 705 | # The where object should be parsed by the connector 706 | it 'should support where for destroyAll', (done) -> 707 | Post.create {title: 'My Post1', content: 'Hello'}, (err, post) -> 708 | Post.create {title: 'My Post2', content: 'Hello'}, (err, post) -> 709 | Post.destroyAll {and: [ 710 | {title: 'My Post1'}, 711 | {content: 'Hello'} 712 | ]}, (err) -> 713 | should.not.exist(err) 714 | Post.count (err, count) -> 715 | should.not.exist(err) 716 | count.should.be.equal(1) 717 | done() 718 | 719 | # context 'regexp operator', () -> 720 | # before () -> 721 | # deleteExistingTestFixtures (done) -> 722 | # Post.destroyAll(done) 723 | # 724 | # beforeEach () -> 725 | # createTestFixtures (done) -> 726 | # Post.create [ 727 | # {title: 'a', content: 'AAA'}, 728 | # {title: 'b', content: 'BBB'} 729 | # ], done 730 | # 731 | # after () -> 732 | # deleteTestFixtures (done) -> 733 | # Post.destroyAll(done); 734 | # 735 | # context 'with regex strings', () -> 736 | # context 'using no flags', () -> 737 | # it 'should work', (done) -> 738 | # Post.find {where: {content: {regexp: '^A'}}}, (err, posts) -> 739 | # should.not.exist(err) 740 | # posts.length.should.equal(1) 741 | # posts[0].content.should.equal('AAA') 742 | # done() 743 | # 744 | # context 'using flags', () -> 745 | # beforeEach () -> 746 | # addSpy () -> 747 | # sinon.stub(console, 'warn'); 748 | # 749 | # afterEach () -> 750 | # removeSpy -> 751 | # console.warn.restore(); 752 | # 753 | # it 'should work', (done) -> 754 | # Post.find {where: {content: {regexp: '^a/i'}}}, (err, posts) -> 755 | # should.not.exist(err) 756 | # posts.length.should.equal(1) 757 | # posts[0].content.should.equal('AAA') 758 | # done() 759 | # 760 | # it 'should print a warning when the global flag is set', (done) -> 761 | # Post.find {where: {content: {regexp: '^a/g'}}}, (err, posts) -> 762 | # console.warn.calledOnce.should.be.ok 763 | # done() 764 | # 765 | # context 'with regex literals', () -> 766 | # context 'using no flags', () -> 767 | # it 'should work', (done) -> 768 | # Post.find {where: {content: {regexp: /^A/}}}, (err, posts) -> 769 | # should.not.exist(err) 770 | # posts.length.should.equal(1) 771 | # posts[0].content.should.equal('AAA') 772 | # done() 773 | # 774 | # 775 | # context 'using flags', () -> 776 | # beforeEach () -> 777 | # addSpy () -> 778 | # sinon.stub(console, 'warn') 779 | # 780 | # afterEach () -> 781 | # removeSpy () -> 782 | # console.warn.restore() 783 | # 784 | # 785 | # it 'should work', (done) -> 786 | # Post.find {where: {content: {regexp: /^a/i}}}, (err, posts) -> 787 | # should.not.exist(err) 788 | # posts.length.should.equal(1) 789 | # posts[0].content.should.equal('AAA') 790 | # done() 791 | # 792 | # it 'should print a warning when the global flag is set', (done) -> 793 | # Post.find {where: {content: {regexp: /^a/g}}}, (err, posts) -> 794 | # console.warn.calledOnce.should.be.ok 795 | # done() 796 | # 797 | # context 'with regex object', () -> 798 | # context 'using no flags', () -> 799 | # it 'should work', (done) -> 800 | # Post.find {where: {content: {regexp: new RegExp(/^A/)}}}, (err, posts) -> 801 | # should.not.exist(err) 802 | # posts.length.should.equal(1) 803 | # posts[0].content.should.equal('AAA') 804 | # done() 805 | # 806 | # 807 | # context 'using flags', () -> 808 | # beforeEach () -> 809 | # addSpy () -> 810 | # sinon.stub(console, 'warn') 811 | # 812 | # afterEach () -> 813 | # removeSpy () -> 814 | # console.warn.restore() 815 | # 816 | # 817 | # it 'should work', (done) -> 818 | # Post.find {where: {content: {regexp: new RegExp(/^a/i)}}}, (err, posts) -> 819 | # should.not.exist(err) 820 | # posts.length.should.equal(1) 821 | # posts[0].content.should.equal('AAA') 822 | # done() 823 | # 824 | # it 'should print a warning when the global flag is set', (done) -> 825 | # Post.find {where: {content: {regexp: new RegExp(/^a/g)}}}, (err, posts) -> 826 | # should.not.exist(err) 827 | # console.warn.calledOnce.should.be.ok; 828 | # done() 829 | 830 | after (done) -> 831 | User.destroyAll -> 832 | Post.destroyAll -> 833 | PostWithNumberId.destroyAll -> 834 | PostWithStringId.destroyAll -> 835 | PostWithStringKey.destroyAll -> 836 | PostWithNumberUnderscoreId.destroyAll(done) 837 | -------------------------------------------------------------------------------- /test/crud/edge.test.coffee: -------------------------------------------------------------------------------- 1 | ## This test written in mocha+should.js 2 | should = require('./../init'); 3 | 4 | describe 'edge', () -> 5 | db = null 6 | User = null 7 | Friend = null 8 | FriendCustom = null 9 | 10 | before (done) -> 11 | db = getDataSource() 12 | 13 | User = db.define('User', { 14 | fullId: {type: String, _id: true}, 15 | name: {type: String} 16 | email: {type: String}, 17 | age: Number, 18 | }, updateOnLoad: true); 19 | 20 | Friend = db.define('Friend', { 21 | _id: {type: String, _id: true}, 22 | _from: {type: String, _from: true}, 23 | _to: {type: String, _to: true}, 24 | label: {type: String} 25 | }, {updateOnLoad: true, arangodb: {edge: true}}); 26 | 27 | FriendCustom = db.define('FriendCustom', { 28 | fullId: {type: String, _id: true}, 29 | from: {type: String, _from: true, required: true}, 30 | to: {type: String, _to: true, required: true}, 31 | label: {type: String} 32 | }, { 33 | updateOnLoad: true, 34 | arangodb: { 35 | collection: 'Friend', 36 | edge: true 37 | } 38 | }); 39 | 40 | db.automigrate done; 41 | 42 | beforeEach (done) -> 43 | User.destroyAll -> 44 | Friend.destroyAll done 45 | 46 | after (done) -> 47 | User.destroyAll -> 48 | Friend.destroyAll done 49 | 50 | it 'should report error create edge without field `_to`', (done) -> 51 | User.create [{name: 'Matteo'}, {name: 'Antonio'}], (err, users) -> 52 | return done err if err 53 | users.should.have.length(2) 54 | Friend.create {_from: users[0].fullId, label: 'friend'}, (err) -> 55 | should.exist(err) 56 | err.name.should.equal('ArangoError') 57 | err.code.should.equal(400) 58 | err.message.should.match(/^\'to\' is missing, expecting|invalid edge attribute|edge attribute missing or invalid/) 59 | done() 60 | 61 | it 'should report error create edge without field `_from`', (done) -> 62 | User.create [{name: 'Matteo'}, {name: 'Antonio'}], (err, users) -> 63 | return done err if err 64 | users.should.have.length(2) 65 | Friend.create {_to: users[0].fullId, label: 'friend'}, (err) -> 66 | should.exist(err) 67 | err.name.should.equal('ArangoError') 68 | err.code.should.equal(400) 69 | err.message.should.match(/^\'from\' is missing, expecting|invalid edge attribute|edge attribute missing or invalid/) 70 | done() 71 | 72 | it 'create edge should return default fields _to and _from', (done) -> 73 | User.create [{name: 'Matteo'}, {name: 'Antonio'}], (err, users) -> 74 | return done err if err 75 | users.should.have.length(2) 76 | Friend.create {_from: users[0].fullId, _to: users[1].fullId, label: 'friend'}, (err, friend) -> 77 | return done err if err 78 | should.exist(friend) 79 | should.exist(friend.id) 80 | should.exist(friend._id) 81 | friend._from.should.equal(users[0].fullId) 82 | friend._to.should.equal(users[1].fullId) 83 | friend.label.should.equal('friend') 84 | done() 85 | 86 | it 'create edge should return custom fields `to` and `from` defined as `_to` and `_from`', (done) -> 87 | User.create [{name: 'Matteo'}, {name: 'Antonio'}], (err, users) -> 88 | return done err if err 89 | users.should.have.length(2) 90 | FriendCustom.create {from: users[1].fullId, to: users[0].fullId, label: 'friend'}, (err, friend) -> 91 | return done err if err 92 | should.exist(friend) 93 | should.exist(friend.id) 94 | should.exist(friend.fullId) 95 | friend.from.should.equal(users[1].fullId) 96 | friend.to.should.equal(users[0].fullId) 97 | friend.label.should.equal('friend') 98 | done() 99 | -------------------------------------------------------------------------------- /test/imported.test.coffee: -------------------------------------------------------------------------------- 1 | describe 'arangodb imported features', () -> 2 | 3 | before () -> 4 | require('./init') 5 | 6 | require('loopback-datasource-juggler/test/common.batch') 7 | require('loopback-datasource-juggler/test/default-scope.test') 8 | require('loopback-datasource-juggler/test/include.test') 9 | -------------------------------------------------------------------------------- /test/init.coffee: -------------------------------------------------------------------------------- 1 | module.exports = require('should'); 2 | 3 | DataSource = require('loopback-datasource-juggler').DataSource 4 | 5 | TEST_ENV = process.env.TEST_ENV or 'test' 6 | config = require('rc')('loopback', { test: { arangodb: {}}})[TEST_ENV].arangodb; 7 | 8 | calculateArangoDBVersion = (version) -> 9 | if !version then return 30000 10 | 11 | version = version.split '.' 12 | major = Number version[0] 13 | minor = Number version[1] 14 | patch = Number version[1] or 0 15 | 16 | return major * 10000 + minor * 100 + patch 17 | 18 | if process.env.CI 19 | ARANGODB_VERSION = calculateArangoDBVersion process.env.ARANGODB_VERSION 20 | config = 21 | host: process.env.ARANGODB_HOST or 'localhost' 22 | port: process.env.ARANGODB_PORT or 8529 23 | database: process.env.ARANGODB_DATABASE or '_system' 24 | arangoVersion: ARANGODB_VERSION 25 | 26 | global.config = config; 27 | 28 | global.getDataSource = global.getSchema = (customConfig) -> 29 | db = new DataSource(require('../src/arangodb'), customConfig or config); 30 | db.log = (msg) -> console.log msg 31 | return db 32 | 33 | global.connectorCapabilities = 34 | ilike: false 35 | nilike: false 36 | nestedProperty: true 37 | replaceOrCreateReportsNewInstance: true 38 | supportInclude: true -------------------------------------------------------------------------------- /test/migration.test.coffee: -------------------------------------------------------------------------------- 1 | # This test written in mocha+should.js 2 | should = require('./init'); 3 | 4 | GeoPoint = require('loopback-datasource-juggler').GeoPoint 5 | 6 | describe 'arangodb migration functionality', () -> 7 | 8 | before () -> 9 | ds = getDataSource() 10 | 11 | inline_model = ds.define 'InlineModel',{ 12 | hashIndex1: 13 | type: String 14 | index: true 15 | 16 | hashIndex2: 17 | type: String 18 | index: 19 | hash: true 20 | 21 | hashIndexSparsed: 22 | type: String 23 | index: 24 | hash: 25 | sparse: true 26 | 27 | hashIndexUnique: 28 | type: String 29 | index: 30 | hash: 31 | unique: true 32 | 33 | skiplist: 34 | type: String 35 | index: 36 | skiplist: true 37 | 38 | skiplistSparsed: 39 | type: String 40 | index: 41 | skiplist: 42 | sparse: true 43 | 44 | skiplistUnique: 45 | type: String 46 | index: 47 | skiplist: 48 | unique : true 49 | 50 | fulltext: 51 | type: String 52 | index: 53 | fulltext: true 54 | 55 | fulltextMinWordLength: 56 | type: String 57 | index: 58 | fulltext: 59 | minWordLength: 4 60 | capSizeOnly: 61 | type: String 62 | index: 63 | size: 10 64 | 65 | capByteSize: 66 | type: String 67 | index: 68 | size: 10 69 | byteSize: 100 70 | geo: 71 | type: GeoPoint 72 | 73 | 74 | } 75 | 76 | explicit_model = ds.define 'ExplicitModel',{ 77 | hashIndex1: 78 | type: String 79 | hashIndex2: 80 | type: String 81 | hashIndexSparsed: 82 | type: String 83 | hashIndexUnique: 84 | type: String 85 | skiplist: 86 | type: String 87 | skiplistSparsed: 88 | type: String 89 | skiplistUnique: 90 | type: String 91 | fulltext: 92 | type: String 93 | fulltext: 94 | type: String 95 | fulltextMinWordLength: 96 | type: String 97 | capSizeOnly: 98 | type: String 99 | capByteSize: 100 | type: String 101 | } 102 | 103 | 104 | describe 'inline defined indexes', () -> 105 | describe 'hash index', () -> 106 | it 'should define a hash index when defined as boolean "index":true' 107 | 108 | it 'should define a hash index when defined as object with key "hash": true' 109 | 110 | 111 | describe 'skiplist index', () -> 112 | describe 'fulltext index', () -> 113 | describe 'geo index', () -> 114 | describe 'cap constraint index', () -> 115 | describe 'explicit defined indexes', () -> 116 | describe 'hash index', () -> 117 | describe 'skiplist index', () -> 118 | describe 'fulltext index', () -> 119 | describe 'cap constraint index', () -> 120 | 121 | 122 | describe 'hash indexes:', () -> 123 | describe 'defined explicit:', () -> 124 | it 'should define a hash index from model settings' 125 | 126 | it 'should define a sparsed hash index from model settings' 127 | 128 | describe 'defined inline:', () -> 129 | it 'should define a hash index from property settings' 130 | 131 | it 'should define a sparsed hash index from property settings' 132 | 133 | 134 | describe 'skiplist indexes:', () -> 135 | describe 'defined inline:', () -> 136 | it 'should define a skiplist index from model settings' 137 | 138 | it 'should define a sparsed skiplist index from model settings' 139 | 140 | 141 | describe 'defined explicit:', () -> 142 | it 'should define a skiplist indexes from property settings' 143 | 144 | it 'should define a sparsed skiplist indexes from property settings' 145 | 146 | 147 | 148 | describe 'fulltext indexes:', () -> 149 | describe 'defined inline:', () -> 150 | it 'should define a fulltext index from model settings' 151 | 152 | 153 | it 'should define a fulltext index from model settings' 154 | 155 | 156 | describe 'defined explicit:', () -> 157 | 158 | describe 'geo indexes:', () -> 159 | describe 'defined inline:', () -> 160 | 161 | describe 'defined explicit:', () -> 162 | 163 | describe 'cap indexes:', () -> 164 | describe 'defined inline:', () -> 165 | 166 | describe 'defined explicit:', () -> 167 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffeescript/register -------------------------------------------------------------------------------- /test/operators.test.coffee: -------------------------------------------------------------------------------- 1 | moment = require('moment') 2 | 3 | should = require('./init'); 4 | 5 | describe 'operators', () -> 6 | db = null 7 | User = null 8 | 9 | before (done) -> 10 | db = getDataSource() 11 | 12 | User = db.define 'User', { 13 | name: String, 14 | email: String, 15 | age: Number, 16 | created: Date, 17 | } 18 | User.destroyAll(done) 19 | 20 | describe 'between', () -> 21 | 22 | beforeEach () -> User.destroyAll() 23 | 24 | it 'found data that match operator criteria - date type', (done) -> 25 | now = moment().toDate(); 26 | beforeTenHours = moment(now).subtract({hours: 10}).toDate() 27 | afterTenHours = moment(now).add({hours: 10}).toDate() 28 | 29 | usersData = [ 30 | {name: 'Matteo', created: now}, 31 | {name: 'Antonio', created: beforeTenHours}, 32 | {name: 'Daniele', created: afterTenHours}, 33 | {name: 'Mariangela'}, 34 | ] 35 | 36 | User.create usersData, (err, users) -> 37 | return done err if err 38 | users.should.have.lengthOf(4) 39 | filter = {where: {created: {between: [beforeTenHours, afterTenHours]}}} 40 | User.find filter, (err, users) -> 41 | return done err if err 42 | users.should.have.lengthOf(3) 43 | filter = {where: {created: {between: [now, afterTenHours]}}} 44 | User.find filter, (err, users) -> 45 | return done err if err 46 | users.should.have.lengthOf(2) 47 | done() 48 | -------------------------------------------------------------------------------- /test/persistence-hooks.test.coffee: -------------------------------------------------------------------------------- 1 | should = require('./init') 2 | suite = require('loopback-datasource-juggler/test/persistence-hooks.suite') 3 | 4 | suite(global.getDataSource(), should, global.connectorCapabilities) 5 | --------------------------------------------------------------------------------