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