diff --git a/Makefile b/Makefile index c61b81f..99ef849 100644 --- a/Makefile +++ b/Makefile @@ -3,24 +3,29 @@ MOCHA_OPTS= --check-leaks --timeout 6000 REPORTER = spec DB?=waterline-test-integration -test: test-unit test-integration-all - -test-clean: test-unit test-integration-all clean +test: clean-all test-unit test-integration-all +test-all: test clean test-integration-documentdb test-integration-all: test-integration-orientdb test-integration -test-integration: + +test-integration-generic: @echo "\n\nNOTICE: If tests fail, please ensure you've set the correct credentials in test/test-connection.json\n" @echo "Running 'waterline-adapter-tests' integration tests..." + +test-integration: test-integration-generic @NODE_ENV=test node test/integration/runner.js + +test-integration-documentdb: test-integration-generic + @NODE_ENV=test DATABASE_TYPE=document node test/integration/runner.js test-integration-orientdb: @echo "\n\nNOTICE: If tests fail, please ensure you've set the correct credentials in test/test-connection.json\n" @echo "Running waterline-orientdb integration tests..." @NODE_ENV=test ./node_modules/.bin/mocha \ --reporter $(REPORTER) \ - --timeout 15000 --globals Associations,CREATE_TEST_WATERLINE,DELETE_TEST_WATERLINE \ + --timeout 6000 --globals Associations,CREATE_TEST_WATERLINE,DELETE_TEST_WATERLINE \ test/integration-orientdb/*.js test/integration-orientdb/tests/**/*.js \ test/integration-orientdb/bugs/*.js test/integration-orientdb/bugs/**/*.js @@ -31,7 +36,7 @@ test-unit: $(MOCHA_OPTS) \ test/unit/*.js test/unit/**/*.js -coverage: +coverage: clean-all @echo "\n\nRunning coverage report..." rm -rf coverage ./node_modules/istanbul/lib/cli.js cover --report none --dir coverage/unit \ @@ -41,11 +46,20 @@ coverage: ./node_modules/.bin/_mocha test/integration-orientdb/*.js test/integration-orientdb/tests/**/*.js \ -- --timeout 15000 --globals Associations ./node_modules/istanbul/lib/cli.js cover --report none --dir coverage/integration test/integration/runner.js + ./node_modules/istanbul/lib/cli.js cover --report none --dir coverage/integration-document test/integration/runner.js document ./node_modules/istanbul/lib/cli.js report clean: @echo "\n\nDROPPING ALL COLLECTIONS from db: $(DB)" + @echo "NOTICE: If operation fails, please ensure you've set the correct credentials in oriento.opts file" + @echo "Note: you can choose which db to drop by appending 'DB=', e.g. 'make clean DB=waterline-test-orientdb'\n" + ./node_modules/.bin/oriento db drop $(DB) || true + +clean-all: + @echo "\n\nDROPPING DATABASES: waterline-test-integration, waterline-test-orientdb" @echo "NOTICE: If operation fails, please ensure you've set the correct credentials in oriento.opts file\n" - ./node_modules/.bin/oriento db drop $(DB) + ./node_modules/.bin/oriento db drop waterline-test-integration > /dev/null 2>&1 || true + ./node_modules/.bin/oriento db drop waterline-test-orientdb > /dev/null 2>&1 || true + @echo "Done" .PHONY: coverage diff --git a/README.md b/README.md index 699b3b8..e718b08 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Build Status](https://travis-ci.org/appscot/waterline-orientdb.svg?branch=master)](https://travis-ci.org/appscot/waterline-orientdb) [![Test Coverage](https://codeclimate.com/github/appscot/waterline-orientdb/badges/coverage.svg)](https://codeclimate.com/github/appscot/waterline-orientdb) [![dependencies](https://david-dm.org/appscot/waterline-orientdb.svg)](https://david-dm.org/appscot/waterline-orientdb) -[![devDependencies](https://david-dm.org/appscot/waterline-orientdb/dev-status.svg)](https://david-dm.org/appscot/waterline-orientdb#info=devDependencies) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/appscot/waterline-orientdb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) # waterline-orientdb @@ -12,14 +11,20 @@ Waterline adapter for OrientDB. [Waterline](https://github.com/balderdashy/water > **Warning** > > `waterline-orientdb` maps the logical `id` attribute to the required `@rid` physical-layer OrientDB Record ID. +> +> +> Migrations +> +> We don't recommend using `migrate: 'alter'` as it has the nasty effect of deleting the data of all edges on a graphDB, leaving only data on the vertexes. +> Either use `'safe'` and migrate manually or use `'drop'` to completely reset the data and collections. In production +> always use `'safe'`. We are currently pushing for a new kind of migration strategy named `'create'`, check [waterline issue #846](https://github.com/balderdashy/waterline/issues/846). + -#### Development Status -* Waterline-orientdb aims to work with Waterline v0.10.x and OrientDB v1.7.10 and later. While it may work with earlier versions, they are not currently supported, [pull requests are welcome](./CONTRIBUTING.md)! +Waterline-orientdb aims to work with Waterline v0.10.x and OrientDB v1.7.10 and later. While it may work with earlier versions, they are not currently supported, [pull requests are welcome](./CONTRIBUTING.md)! -* From the waterline [adapter interfaces](https://github.com/balderdashy/sails-docs/blob/master/contributing/adapter-specification.md) waterline-orientdb fully supports `Semantic`, `Queryable` and `Associations` interfaces. -Waterline-orientdb passes all integration tests from [waterline-adapter-tests](https://github.com/balderdashy/waterline-adapter-tests). +From the waterline [adapter interfaces](https://github.com/balderdashy/sails-docs/blob/master/contributing/adapter-specification.md) waterline-orientdb supports `Semantic`, `Queryable`, `Associations` and `Migratable` interfaces. -* Many-to-many associations currently use a junction table instead of an edge and this will change at some point ([#29](https://github.com/appscot/waterline-orientdb/issues/29)). +Waterline-orientb connects to OrientDB using [Oriento](codemix/oriento) (OrientDB's official driver). ## Table of Contents @@ -27,9 +32,10 @@ Waterline-orientdb passes all integration tests from [waterline-adapter-tests]( 2. [Waterline Configuration](#waterline-configuration) 3. [Overview](#overview) 4. [Usage](#usage) -5. [Waterline](#waterline) +5. [Testing](#testing) 6. [Contributions](#contributions) -7. [License](#license) +7. [About Waterline](#about-waterline) +8. [License](#license) ## Installation @@ -43,7 +49,9 @@ npm install waterline-orientdb --save ## Waterline Configuration -#### Using with Waterline v0.10.x +### Using with Waterline v0.10.x + +#### Basic Example ```javascript var orientAdapter = require('waterline-orientdb'); @@ -70,62 +78,149 @@ var config = { } ``` +#### Connection advanced config example +```javascript + myLocalOrient: { + adapter: 'orient', + host: 'localhost', + port: 2424, + user: 'root', + password: 'root', + database: 'waterline', + + // Additional options + options: { + + // DB Options + // + // database type: graph | document + databaseType : 'graph', + // + // storage type: memory | plocal + storage : 'plocal', + + // Useful in REST APIs + // + // If `id` is URI encoded, decode it with `decodeURIComponent()` (useful when `id` comes from an URL) + decodeURIComponent : true, + // + // Replaces circular references with `id` after populate operations (useful when results will be JSONfied) + removeCircularReferences : false, + + // other + // + // Turn parameterized queries on + parameterized : true + } + } +``` +The values stated above represent the default values. For an up to date comprehensive list check [adapter.js](https://github.com/appscot/waterline-orientdb/blob/master/lib/adapter.js#L87). ## Overview -#### Models -Waterline-orientdb will represent most models in OrientDB as Vertices. The exception being Many-to-Many through join tables which are represented by Edges. +### Models +In a graph db Waterline-orientdb will represent most models in OrientDB as vertexes, the exception being Many-to-Many join tables which are represented by Edges. If using a document db, all models will be represented by documents. -#### Associations -To learn how to create associations with Waterline/Sails.js check the Waterline Docs [Associations Page](https://github.com/balderdashy/waterline-docs/blob/master/associations.md). Below we go through how waterline-orientdb approaches each kind of associations. +### Associations +To learn how to create associations with Waterline/Sails.js check the Waterline Docs [Associations Page](https://github.com/balderdashy/waterline-docs/blob/master/associations.md). Below we go through how waterline-orientdb approaches each kind of association. -###### One-to-One Associations +#### One-to-One Associations For One-to-One Associations waterline-orientdb creates a LINK ([OrientDB Types](http://www.orientechnologies.com/docs/last/orientdb.wiki/Types.html)) to associate records. -###### One-to-Many Associations -One-to-Many Associations are represented in OrientDB by a LINKSET. +#### One-to-Many Associations +One-to-Many Associations are also represented by LINKs in OrientDB. -###### Many-to-Many Associations -Many-to-Many Associations are handled by Waterline core, creating a join table holding foreign keys to the associated records. Waterline-orientdb does not change this behaviour for now but we will replace the join table by Edges in a future release ([#29](https://github.com/appscot/waterline-orientdb/issues/29)). Currently it's not deemed a priority. +#### Many-to-Many Associations +In many-to-many associations waterline-orientdb will connect vertexes using edges, hence edges act as join tables. Usually Waterline will create rather long names for join tables (e.g. driver_taxis__taxi_drivers) which are little meaningful from the perspective of a graphDB. Waterline-orientdb allows you to change the name of the edge by adding a property `joinTableNames` to the dominant collection. Example: +```javascript +{ + identity: 'driver', + joinTableNames: { + taxis: 'drives' + }, + + attributes: { + name: 'string', + taxis: { + collection: 'taxi', + via: 'drivers', + dominant: true + } + } +} +``` +In this example the join table name **driver_taxis__taxi_drivers** get converted to **drives**. Complete example of the fixture can be found [here](https://github.com/appscot/waterline-orientdb/tree/master/test/integration-orientdb/fixtures/manyToMany.driverHack.fixture.js). -###### Many-to-Many Through Associations -In Many-to-Many Through Association the join table is represented in OrientDB by Edges. Waterline-orientdb automatically creates the edges whenever an association is created. The Edge is named after the property tableName or identity in case tableName is missing. +#### Many-to-Many Through Associations +In a [Many-to-Many Through Association](https://github.com/balderdashy/waterline-docs/blob/master/associations.md#many-to-many-through-associations) ([more info](https://github.com/balderdashy/waterline/issues/705#issuecomment-60945411)) the join table is represented in OrientDB by Edges. Waterline-orientdb automatically creates the edges whenever an association is created. The Edge is named after the property tableName (or identity in case tableName is missing). -#### sails-orientdb differences +#### Populate queries (joins) +Waterline-orientdb implements its own custom join function so when the user runs `.populate(some_collection)` it will send a single `SELECT` query with a [fetchplan](http://www.orientechnologies.com/docs/last/orientdb.wiki/Fetching-Strategies.html) to OrientDB. This way join operations remain fast and performant by leveraging OrientDB's graphDB features. -###### Edge creation -The main difference between waterline-orientdb and [sails-orientdb](https://github.com/vjsrinath/sails-orientdb) is the way associations/edges are created. In `sails-orientdb` a special attribute named 'edge' is required while waterline-orientdb tries to adhere to waterline specficiation. +### sails-orientdb differences -###### ID -Waterline-orientdb mimics sails-mongo adapter behaviour and maps the logical `id` attribute to the required `@rid` physical-layer OrientDB Record ID. +#### Edge creation +The main difference between waterline-orientdb and [sails-orientdb](https://github.com/vjsrinath/sails-orientdb) is the way associations/edges are created. In `sails-orientdb` a special attribute named 'edge' is required while waterline-orientdb tries to adhere to waterline specification. + +#### ID +Waterline-orientdb mimics sails-mongo adapter behaviour and maps the logical `id` attribute to the required `@rid` physical-layer OrientDB Record ID. Because of this it's not necessary to declare an `id` attribute on your model definitions. ## Usage +### Models + +`waterline-orientdb` uses the standard [waterline model definition](https://github.com/balderdashy/waterline-docs/blob/master/models.md) and extends it in order to accommodate OrientDB features. + +#### orientdbClass + +It's possible to force the class of a model by adding the property `orientdbClass` to the definition. Generally this is not required as `waterline-orientdb` can determine which is the best class to use, so it should only be used in special cases. Possible values: +* `undefined` - the default and recommended option. The appropriate class will be determined for the model; +* `""` or `"document"` - class will be the default OrientDB document class; +* `"V"` - class will be Vertex; +* `"E"` - class will be Edge. + +Example: +```javascript +{ + identity : 'post', + orientdbClass : 'V' + + attributes : { + name : 'string' + } +} +``` + +Note, when using a document database (through `config.options.databaseType`), `orientdbClass` class will be ignored and all classes will be documents. + + +### Methods + This adapter adds the following methods: -###### `createEdge(from, to, options, callback)` +#### .createEdge (from, to, options, callback) Creates edge between specified two model instances by ID in the form parameters `from` and `to` usage: ```javascript //Assume a model named "Post" Post.createEdge('#12:1', '#13:1', { '@class':'Comments' }, function(err, result){ - + console.log('Edges deleted', result); }); ``` -###### `deleteEdges(from, to, options, callback)` +#### .deleteEdges (from, to, options, callback) Deletes edges between specified two model instances by ID in the form parameters `from` and `to` usage: ```javascript //Assume a model named "Post" Post.deleteEdges('#12:1', '#13:1', null, function(err, result){ - + console.log('Edge created', result); }); ``` -###### `query(connection, collection, query, [options], cb)` +#### .query (query, [options], cb) Runs a SQL query against the database using Oriento's query method. Will attempt to convert @rid's into ids. usage: @@ -145,39 +240,62 @@ usage: }); ``` -###### `getDB(connection, collection, cb)` -Returns a native Oriento object +#### .native (cb) +Returns a native Oriento class + +usage: + ```javascript + //Assume a model named "Post" + Post.native(function(myClass){ + myClass.property.list() + .then(function (properties) { + console.log('The class has the following properties:', properties); + } + }); + ``` + +#### .getDB (cb) +Returns a native Oriento database object usage: ```javascript //Assume a model named "Post" Post.getDB(function(db){ - // db.query(... + db.select('foo() as testresult').from('OUser').limit(1).one() + .then(function(res) { + // res contains the result of foo + console.log(res); + }); }); ``` -###### `getServer(connection, collection, cb)` +#### .getServer (cb) Returns a native Oriento connection usage: ```javascript Post.getServer(function(server){ - // server.list() + server.list() + .then(function (dbs) { + console.log('There are ' + dbs.length + ' databases on the server.'); + }); }); ``` -###### `removeCircularReferences(connection, collection, object, cb)` +#### .removeCircularReferences (object, cb) Convenience method that replaces circular references with `id` when one is available, otherwise it replaces the object with string '[Circular]' usage: ```javascript //Assume a model named "Post" Post.removeCircularReferences(posts, function(result){ - // JSON.stringify(result); // it's safe to stringify result + console.log(JSON.stringify(result)); // it's safe to stringify result }); ``` -#### Example Model definitions +### Example Model definitions + +For a comprehensive set of examples take a look at [waterline-adapter-tests fixtures](https://github.com/balderdashy/waterline-adapter-tests/tree/master/interfaces/associations/support/fixtures), all of those are working examples and frequently tested. Below is an example of a Many-to-many through association. ```javascript /** @@ -277,18 +395,27 @@ An edge named **venueTable** will be created from Team to Stadium model instance See [`FAQ.md`](./FAQ.md). -## Waterline +## Testing +Test are written with mocha. Integration tests are handled by the [waterline-adapter-tests](https://github.com/balderdashy/waterline-adapter-tests) project, which tests adapter methods against the latest Waterline API. -[Waterline](https://github.com/balderdashy/waterline) is a new kind of storage and retrieval engine. - -It provides a uniform API for accessing stuff from different kinds of databases, protocols, and 3rd party APIs. That means you write the same code to get users, whether they live in OrientDB, MySQL, LDAP, MongoDB, or Facebook. +To run tests: +```shell +npm test +``` ## Contributions We are always looking for the quality contributions! Please check the [CONTRIBUTING.md](./CONTRIBUTING.md) for the contribution guidelines. -Thanks so much to Srinath Janakiraman ([vjsrinath](http://github.com/vjsrinath)) who built the original `sails-orient` adapter. +Thanks so much to Srinath Janakiraman ([vjsrinath](http://github.com/vjsrinath)) who built the `sails-orientdb` adapter, from which `waterline-orientdb` was forked. + + +## About Waterline + +[Waterline](https://github.com/balderdashy/waterline) is a new kind of storage and retrieval engine. + +It provides a uniform API for accessing stuff from different kinds of databases, protocols, and 3rd party APIs. That means you write the same code to get users, whether they live in OrientDB, MySQL, LDAP, MongoDB, or Facebook. ## License diff --git a/ci/initialize-ci.sh b/ci/initialize-ci.sh index 9c48a66..395f0cf 100755 --- a/ci/initialize-ci.sh +++ b/ci/initialize-ci.sh @@ -37,5 +37,10 @@ echo "--- Starting an instance of OrientDB ---" sh -c $ODB_LAUNCHER /dev/null & # Wait a bit for OrientDB to finish the initialization phase. -sleep 15 + +if [[ $ODB_VERSION == *"1.7"* ]]; then + sleep 15 +else + sleep 30 +fi printf "\n=== The CI environment has been initialized ===\n" diff --git a/lib/adapter.js b/lib/adapter.js index feba09b..05b142b 100644 --- a/lib/adapter.js +++ b/lib/adapter.js @@ -2,10 +2,12 @@ /** * Module Dependencies */ -var orient = require('./connection'), - Associations = require('./associations'), - utils = require('./utils'); +var ensureNewline = process.env.NODE_ENV !== 'production'; +var log = require('debug-logger').config({ ensureNewline: ensureNewline })('waterline-orientdb:adapter'), + Connection = require('./connection'), + utils = require('./utils'); + /** * waterline-orientdb * @@ -25,6 +27,8 @@ module.exports = (function() { // You'll want to maintain a reference to each connection // that gets registered with this adapter. + // + // Keep track of all the connections used by the app var connections = {}; // You may also want to store additional, private data @@ -43,11 +47,19 @@ module.exports = (function() { // You don't have to support this feature right off the bat in your // adapter, but it ought to get done eventually. // - var getConn = function(config, collections) { - return orient.create(config, collections); - }; - + // Sounds annoying to deal with... + // ...but it's not bad. In each method, acquire a connection using the config + // for the current model (looking it up from `_modelReferences`), establish + // a connection, then tear it down before calling your method's callback. + // Finally, as an optimization, you might use a db pool for each distinct + // connection configuration, partioning pools for each separate configuration + // for your adapter (i.e. worst case scenario is a pool for each model, best case + // scenario is one single single pool.) For many databases, any change to + // host OR database OR user OR password = separate pool. + // var _dbPools = {}; + var adapter = { + identity: 'waterline-orientdb', // Set to true if this adapter supports (or requires) things like data // types, validations, keys, etc. @@ -68,23 +80,56 @@ module.exports = (function() { // alter => Drop/add columns as necessary. // safe => Don't change anything (good for production DBs) // - syncable : false, + syncable : true, // Which type of primary key is used by default pkFormat : 'string', // Default configuration for connections defaults : { + + // Connection Configuration database : 'waterline', host : 'localhost', port : 2424, + // schema : false, // to be consistent with OrientDB we should default to false but breaks 4 tests + + // Additional options options: { + + // DB/Oriento Options + // + // database type: graph | document + databaseType : 'graph', + // + // storage type: memory | plocal + storage : 'plocal', + // + // transport: binary | rest. Currently only binary is supported: https://github.com/codemix/oriento/issues/44 + transport : 'binary', + // + // database username, by default uses connection username set on config + // databaseUser : null, + // + // database password, by default uses connection password set on config + // databasePassword : null, + + // Useful in REST APIs + // + // If `id` is URI encoded, decode it with `decodeURIComponent()` (useful when `id` comes from an URL) + decodeURIComponent : true, + // + // Replaces circular references with `id` after populate operations (useful when results will be JSONfied) + removeCircularReferences : false, + + // other + // + // Turn parameterized queries on + parameterized : true, + // // Waterline only allows populating 1 level below. fetchPlanLevel allows to - // to populate further levels below - fetchPlanLevel : 1, - // Turns on parameterized queries and it should be enabled, but breaks 2 tests against 1.7.*. - // https://github.com/appscot/waterline-orientdb/issues/20 - parameterized : true + // to populate further levels below (experimental) + fetchPlanLevel : 1 } }, @@ -100,6 +145,7 @@ module.exports = (function() { * @return {[type]} [description] */ registerConnection : function(connection, collections, cb) { + log.debug('registerConnection:', connection.database); if (!connection.identity) return cb(new Error('Connection is missing an identity.')); @@ -108,12 +154,8 @@ module.exports = (function() { // Add in logic here to initialize connection // e.g. connections[connection.identity] = new Database(connection, // collections); - - getConn(connection, collections) - .then(function(helper) { - connections[connection.identity] = helper; - cb(); - }); + + connections[connection.identity] = new Connection(connection, collections, cb); }, @@ -127,15 +169,18 @@ module.exports = (function() { * @return {[type]} [description] */ teardown : function(conn, cb) { - + log.debug('teardown:', conn); + /* istanbul ignore if: standard waterline-adapter code */ if ( typeof conn == 'function') { cb = conn; conn = null; } + /* istanbul ignore if: standard waterline-adapter code */ if (!conn) { connections = {}; return cb(); } + /* istanbul ignore if: standard waterline-adapter code */ if (!connections[conn]) return cb(); delete connections[conn]; @@ -149,20 +194,18 @@ module.exports = (function() { * Return the Schema of a collection after first creating the collection * and indexes if they don't exist. * - * @param {String} connectionName - * @param {String} collectionName + * @param {String} connection + * @param {String} collection * @param {Function} callback */ describe : function(connection, collection, cb) { + log.debug('describe:', collection); // Add in logic here to describe a collection (e.g. DESCRIBE TABLE // logic) - - connections[connection].collection.getProperties(collection, function(res, err) { - cb(err, res); - }); + connections[connection].describe(collection, cb); }, - - + + /** * Define * @@ -176,15 +219,10 @@ module.exports = (function() { * @param {Function} cb */ define : function(connection, collection, definition, cb) { - return connections[connection] - .create(collection, { - waitForSync : true - }) - .then(function(res) { - cb(null, res); - }, function(err) { - cb(err); - }); + log.debug('define:', collection); + + // Create the collection and indexes + connections[connection].createCollection(collection, definition, cb); }, @@ -199,12 +237,31 @@ module.exports = (function() { * @param {Function} callback */ drop : function(connection, collection, relations, cb) { + log.debug('drop:', collection); // Add in logic here to delete a collection (e.g. DROP TABLE logic) return connections[connection].drop(collection, relations, cb); }, + /** + * AddAttribute + * + * Add a property to a class + * + * @param {String} connection + * @param {String} collection + * @param {String} attrName + * @param {Object} attrDef + * @param {Function} cb + */ + addAttribute: function(connection, collection, attrName, attrDef, cb) { + log.debug('addAttribute: ' + collection + ', attrName:', attrName); + + return connections[connection].addAttribute(collection, attrName, attrDef, cb); + }, + + /** * Find * @@ -252,10 +309,7 @@ module.exports = (function() { * @return {[type]} [description] */ join : function(connection, collection, options, cb) { - //console.log('\n !!! JOIN, options: ' + require('util').inspect(options)); - - var associations = new Associations(connections[connection]); - return associations.join(collection, options, cb); + return connections[connection].join(collection, options, cb); }, @@ -330,6 +384,20 @@ module.exports = (function() { return connections[connection].query(query, options, cb); }, + /** + * Native + * + * Give access to a native orientd collection object for running custom + * queries. + * + * @param {String} connection + * @param {String} collection + * @param {Function} callback + */ + native: function(connection, collection, cb) { + return connections[connection].native(collection, cb); + }, + /** * Get DB * diff --git a/lib/associations.js b/lib/associations.js index d8ffee0..e49fb35 100644 --- a/lib/associations.js +++ b/lib/associations.js @@ -5,7 +5,9 @@ var _ = require('lodash'), _runJoins = require('waterline-cursor'), utils = require('./utils'), - log = require('debug-logger')('waterline-orientdb:associations'); + Collection = require('./collection'), + log = require('debug-logger')('waterline-orientdb:associations'), + wlFilter = require('waterline-criteria'); /** * Associations @@ -35,8 +37,7 @@ var Associations = module.exports = function Associations(connection) { * @api public */ Associations.prototype.join = function join(collectionName, criteria, cb) { - //TODO: for now we only use fetch plan for many-to-many through associations. Use it for all associations - if(this.isThroughJoin(criteria)) + if(this.isEdgeJoin(criteria)) return this.fetchPlanJoin(collectionName, criteria, cb); return this.genericJoin(collectionName, criteria, cb); @@ -76,21 +77,22 @@ Associations.prototype.getFetchPlan = function getFetchPlan(collectionName, crit return; } - // Many-to-many through associations (edges) + // Edges associations.push(join.parent); var edgeSides = self.getEdgeSides(join.parent); + var joinParentTableName = self.connection.collections[join.parent].tableName; var parentColumnName; if(edgeSides.out.referencedCollectionTableName === join.child && edgeSides.in.referencedAttributeColumnName === join.alias) { - parentColumnName = 'in_' + join.parent; - fetchPlan += parentColumnName + ':1 in_' + join.parent + '.out:' + fetchPlanLevel + ' '; + parentColumnName = 'in_' + joinParentTableName; + fetchPlan += parentColumnName + ':1 in_' + joinParentTableName + '.out:' + fetchPlanLevel + ' '; select.push(parentColumnName); } else if(edgeSides.in.referencedCollectionTableName === join.child && edgeSides.out.referencedAttributeColumnName === join.alias) { - parentColumnName = 'out_' + join.parent; - fetchPlan += parentColumnName + ':1 out_' + join.parent + '.in:' + fetchPlanLevel + ' '; + parentColumnName = 'out_' + joinParentTableName; + fetchPlan += parentColumnName + ':1 out_' + joinParentTableName + '.in:' + fetchPlanLevel + ' '; select.push(parentColumnName); } }); @@ -154,10 +156,11 @@ Associations.prototype.fetchPlanJoin = function fetchPlanJoin(collectionName, cr var parentSchema = self.connection.collections[collectionName].attributes; self.connection.find(collectionName, options, function(err, results){ - if(err) + if(err) { return cb(err); - else if(!results || results.length === 0) + } else if(!results || results.length === 0) { return cb(null, results); + } var normalisedResults = []; var keysToDelete = []; @@ -195,8 +198,6 @@ Associations.prototype.fetchPlanJoin = function fetchPlanJoin(collectionName, cr } //Process record - //TODO: record may be array - if(!parentSide || !record[parentSide.referencedAttributeEdge]){ return; //it probably has been processed already } @@ -206,10 +207,14 @@ Associations.prototype.fetchPlanJoin = function fetchPlanJoin(collectionName, cr delete record[parentSide.referencedAttributeEdge]; record[join.alias] = utils.rewriteIds(record[join.alias], childTableSchema); - + record[join.alias].forEach(function(associatedRecord){ utils.cleanOrientAttributes(associatedRecord, childTableSchema); }); + + if (join.criteria){ + record[join.alias] = wlFilter(record[join.alias], join.criteria).results; + } }); utils.cleanOrientAttributes(record, parentSchema); @@ -357,107 +362,11 @@ Associations.prototype.genericJoin = function genericJoin(collectionName, criter * @api private */ Associations.prototype.getEdgeSides = function getEdgeSides(collectionName) { - var self = this, - collection = this.connection.collections[collectionName], - schema = collection.attributes, - identity = collection.identity || collection.tableName, - vertexA, - vertexB; - - Object.keys(schema).forEach(function(key) { - var reference = schema[key].model || schema[key].references; - if(!reference) - return; - - var referencedCollection = self.connection.collectionsByIdentity[reference]; - var referencedSchema = referencedCollection.attributes; - - var referencedAttributeKey; - Object.keys(referencedSchema).forEach(function(referencedSchemaKey) { - var attribute = referencedSchema[referencedSchemaKey]; - if(attribute.through === identity && attribute.via === key) - referencedAttributeKey = referencedSchemaKey; - }); - if(!referencedAttributeKey){ - Object.keys(referencedSchema).forEach(function(referencedSchemaKey) { - var attribute = referencedSchema[referencedSchemaKey]; - // Optimistic attribute assignment... - if(attribute.through === identity) - referencedAttributeKey = referencedSchemaKey; - }); - } - if(!referencedAttributeKey) - return; - - var referencedAttribute = referencedSchema[referencedAttributeKey]; - - var vertex = { - referencedCollectionName: reference, - referencedCollectionTableName: referencedCollection.tableName || reference, - referencedAttributeKey: referencedAttributeKey, - referencedAttributeColumnName: referencedAttribute.columnName || referencedAttributeKey, - dominant: referencedAttribute.dominant, - junctionTableKey: key, - junctionTableColumnName: schema[key].columnName || key - }; - - if(!vertexA) - vertexA = vertex; - else if(!vertexB) - vertexB = vertex; - else - log.error('Too many associations! Unable to process model [' + identity + '] attribute [' + key + '].'); - }); - - if(!vertexA.dominant && !vertexB.dominant){ - var dominantVertex = (vertexA.junctionTableKey < vertexB.junctionTableKey) ? vertexA : vertexB; - dominantVertex.dominant = true; - - log.warn(collectionName + ' junction table associations [' + vertexA.referencedCollectionName + - ', ' + vertexB.referencedCollectionName + '] have no dominant through association. ' + - dominantVertex.junctionTableKey + - ' was chosen as dominant.'); - } - - if(vertexA.dominant){ - vertexA.referencedAttributeEdge = 'out_' + collectionName; - vertexA.edgeOppositeEnd = 'in'; - vertexB.referencedAttributeEdge = 'in_' + collectionName; - vertexB.edgeOppositeEnd = 'out'; - return { out: vertexA, in: vertexB }; - } - - if(vertexB.dominant){ - vertexA.referencedAttributeEdge = 'in_' + collectionName; - vertexA.edgeOppositeEnd = 'out'; - vertexB.referencedAttributeEdge = 'out_' + collectionName; - vertexB.edgeOppositeEnd = 'in'; - return { out: vertexB, in: vertexA }; - } + var edge = this.connection.collections[collectionName]; + return edge.edgeSides; }; - -/** - * Get edge - * - * Normalizes data for edge creation - * - * @param {Object} values - * @return {Object} - * @api private - */ -Associations.prototype.getEdge = function getEdge(collectionName, values) { - var edgeSides = this.getEdgeSides(collectionName); - return { - from : values && values[edgeSides.out.junctionTableColumnName], - to : values && values[edgeSides.in.junctionTableColumnName], - keys: [edgeSides.out.junctionTableColumnName, edgeSides.in.junctionTableColumnName] - }; -}; - - - /** * Is many-to-many through join * @@ -465,17 +374,24 @@ Associations.prototype.getEdge = function getEdge(collectionName, values) { * @return {Boolean} * @api public */ -Associations.prototype.isThroughJoin = function isThroughJoin(criteria) { +Associations.prototype.isEdgeJoin = function isThroughJoin(criteria) { var self = this; if(!criteria.joins) return false; + var result = false; + for(var i=0; i < criteria.joins.length; i++){ var join = criteria.joins[i]; var collectionInstance = self.connection.collections[join.parent]; - if(utils.isJunctionTableThrough(collectionInstance)) - return true; + if(collectionInstance instanceof Collection.Edge){ + result = true; + } else if(collectionInstance instanceof Collection.Vertex){ + continue; + } else if(collectionInstance instanceof Collection.Document){ + return false; + } } - return false; + return result; }; \ No newline at end of file diff --git a/lib/collection/document.js b/lib/collection/document.js new file mode 100644 index 0000000..ced638b --- /dev/null +++ b/lib/collection/document.js @@ -0,0 +1,439 @@ +"use strict"; + +var _ = require('lodash'), + utils = require('../utils'), + Query = require('../query'), + Record = require('../record'), + log = require('debug-logger')('waterline-orientdb:document'); + +/** + * Manage A Document + * + * @param {Object} definition + * @param {Connection} connection + * @api public + */ +var Document = module.exports = function Document(definition, connection, collectionsByIdentity) { + + // Set a tableName for this document + this.tableName = ''; + + // If tableName is changed, this holds the original name + this.tableNameOriginal = ''; + + // Set an identity for this document + this.identity = ''; + + // Set the orientdb super class ("document" / V / E) for this document + this.superClass = ''; + + // Oriento class object, set by Connection constructor + this.databaseClass = undefined; + + // Set a class command that will be passed to Oriento ("undefined" / VERTEX / EDGE) + this.classCommand = undefined; + + // Hold collection original attributes + this.attributes = null; + + // Hold Schema Information + this.schema = null; + + // Hold schema in OrientDB friendly format + this.orientdbSchema = null; + + // Schemaless mode + this.schemaless = false; + + // Hold the primary key from definition + this.primaryKey = ''; + + // Hold a reference to an active connection + this.connection = connection; + + // Hold Indexes + this.indexes = []; + + // Holds links between properties (associations) + this.links = []; + + // Parse the definition into document attributes + this._parseDefinition(definition, collectionsByIdentity); + + // Build an indexes dictionary + this._buildIndexes(); + + return this; +}; + +///////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS +///////////////////////////////////////////////////////////////////////////////// + +/** + * Find Documents + * + * @param {Object} criteria + * @param {Function} callback + * @api public + */ +Document.prototype.find = function find(criteria, cb) { + var self = this; + var _query, query; + + try { + if(self.schemaless) { criteria.schemaless = true; } + _query = new Query(criteria, self.connection); + + query = _query.getSelectQuery(self.tableNameOriginal, self.schema); + if(self.tableNameOriginal !== self.tableName){ + query.query[0] = query.query[0].replace(self.tableNameOriginal, self.tableName); + } + } catch(e) { + log.error('Failed to compose find SQL query.', e); + return cb(e); + } + + log.debug('Find query:', query.query[0]); + + var opts = { params: query.params || {} }; + if(query.params){ + log.debug('Find params:', opts); + } + if(criteria.fetchPlan){ + opts.fetchPlan = criteria.fetchPlan.where; + log.debug('Find fetchPlan:', opts.fetchPlan); + } + + self.connection.db + .query(query.query[0], opts) + .all() + .then(function (res) { + log.debug('Find results:', res && res.length); + if (res && criteria.fetchPlan) { + //log.debug('res', res); + cb(null, utils.rewriteIdsRecursive(res, self.schema)); + } else { + cb(null, utils.rewriteIds(res, self.schema)); + } + }) + .error(function (e) { + log.error('Failed to query the DB.', e); + cb(e); + }); +}; + +/** + * Insert A New Document + * + * @param {Object|Array} values + * @param {Function} callback + * @api public + */ +Document.prototype.insert = function insert(values, cb) { + var self = this, + record; + + record = new Record(values, self.schema, self.connection, 'insert'); + + log.debug('Insert into [' + self.tableName + '] values:', record.values); + + self.connection.db.insert() + .into(self.tableName) + .set(record.values) + .one() + .then(function(res) { + log.debug('Insert result id:', res['@rid']); + cb(null, utils.rewriteIds(res, self.schema)); + }) + .error(function(err) { + log.error('Failed to create object in [' + self.tableName + ']. DB error:', err); + cb(err); + }); +}; + +/** + * Update Documents + * + * @param {Object} criteria + * @param {Object} values + * @param {Function} callback + * @api public + */ +Document.prototype.update = function update(criteria, values, cb) { + var _query, + record, + where, + self = this; + + // Catch errors from building query and return to the callback + try { + _query = new Query(criteria, self.connection); + record = new Record(values, self.schema, self.connection); + log.debug('Update [' + self.tableName + '] with values:', record.values); + where = _query.getWhereQuery(self.tableNameOriginal); + if(self.tableNameOriginal !== self.tableName){ + where.query[0] = where.query[0].replace(self.tableNameOriginal, self.tableName); + } + } catch(e) { + log.error('Failed to compose update SQL query:', e); + return cb(e); + } + + var query = self.connection.db.update(self.tableName) + .set(record.values) + .return('AFTER'); + + if(where.query[0]){ + log.debug('Update where query:', where.query[0]); + query = query.where(where.query[0]); + if(where.params){ + log.debug('Update params:', where.params); + query = query.addParams(where.params); + } + } + + query + .all() + .then(function(res) { + log.debug('Update results:', res && res.length); + cb(null, utils.rewriteIds(res, self.schema)); + }) + .error(function(err) { + log.error('Failed to update, error:', err); + cb(err); + }); +}; + +/** + * Destroy Documents + * + * @param {Object} criteria + * @param {Function} callback + * @api public + */ +Document.prototype.destroy = function destroy(criteria, cb) { + var _query, + where, + self = this; + + // Catch errors from building query and return to the callback + try { + _query = new Query(criteria, self.connection); + where = _query.getWhereQuery(self.tableNameOriginal); + if(self.tableNameOriginal !== self.tableName){ + where.query[0] = where.query[0].replace(self.tableNameOriginal, self.tableName); + } + } catch(e) { + log.error('Destroy [' + self.tableName + ']: failed to compose destroy SQL query.', e); + return cb(e); + } + + var query = self.connection.db.delete() + .from(self.tableName) + .return('BEFORE'); + + if(where.query[0]){ + log.debug('Destroy [' + self.tableName + '] where:', where.query[0]); + query.where(where.query[0]); + if(where.params){ + log.debug('Destroy [' + self.tableName + '] params:', where.params); + query.addParams(where.params); + } + } + + query + .all() + .then(function(res) { + log.debug('Destroy [' + self.tableName + '] deleted records:', res && res.length); + cb(null, utils.rewriteIds(res, self.schema)); + }) + .error(function(err) { + log.error('Destroy [' + self.tableName + ']: failed to update, error:', err); + cb(err); + }); +}; + + +//Deletes a collection from database +Document.prototype.drop = function (relations, cb) { + var self = this; + self.databaseClass = null; + + self.connection.db.class.drop(self.tableName) + .then(function (res) { + log.debug('Dropped [' + self.tableName + ']'); + cb(null, res); + }) + .error(cb); +}; + + +///////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS +///////////////////////////////////////////////////////////////////////////////// + + +/** + * Parse Document Definition + * + * @param {Object} definition + * @api private + */ +Document.prototype._parseDefinition = function _parseDefinition(definition, collectionsByIdentity) { + collectionsByIdentity = collectionsByIdentity || {}; + var collectionDef = _.cloneDeep(definition); + + var self = this; + + // Hold the Schema + this.schema = collectionDef.definition; + + // Hold the original attributes + this.attributes = definition.attributes; + + this.primaryKey = _.clone(definition.primaryKey); + + // Set the tableName + var tableName = definition.tableName ? definition.tableName : definition.identity.toLowerCase(); + this.tableName = _.clone(tableName); + this.tableNameOriginal = _.clone(tableName); + + // Set the identity + var identity = definition.identity ? definition.identity : definition.tableName; + this.identity = _.clone(identity); + + if(definition.schema !== undefined){ + this.schemaless = !definition.schema; + } + + // Create orientdbSchema + this.orientdbSchema = {}; + + Object.keys(self.schema).forEach(function(attributeName) { + if (attributeName === 'id') { + // @rid is the equivalent of id, no need to add id. + return; + } + + var linkedClass = null, + attributeType = null, + columnName = attributeName, + linkedIdentity, + linkedDefinition; + + var propertyDefinition = self.schema[attributeName]; + + + if (propertyDefinition && propertyDefinition.columnName === '@rid') { + log.warn('Ignoring attribute "' + attributeName + '", waterline-orientdb already maps @rid column to "id".'); + return; + } + + // definition.definition doesn't seem to contain everything... using definition.attributes ocasionally + var propAttributes = utils.getAttributeAsObject(self.attributes, columnName); + + //log.debug('table: ' + self.tableName + ', processing attribute ' + attributeName + ':', propertyDefinition); + + if ( typeof propertyDefinition === 'string') + attributeType = propertyDefinition; + else if ( typeof propertyDefinition === 'function') + return; + else if (propertyDefinition.model || propertyDefinition.references) { + linkedIdentity = propertyDefinition.model || propertyDefinition.references; + linkedDefinition = collectionsByIdentity[linkedIdentity]; + var useLink = linkedDefinition.primaryKey === 'id'; + linkedClass = linkedDefinition.tableName ? linkedDefinition.tableName : linkedDefinition.identity.toLowerCase(); + attributeType = useLink ? 'Link' : definition.pkFormat; + } else if (propertyDefinition.foreignKey) { + attributeType = 'Link'; + } else if (propertyDefinition.collection) { + attributeType = 'linkset'; + linkedIdentity = propertyDefinition.collection; + linkedDefinition = collectionsByIdentity[linkedIdentity]; + linkedClass = linkedDefinition.tableName ? linkedDefinition.tableName : linkedDefinition.identity.toLowerCase(); + } else + attributeType = propertyDefinition.type; + + if (attributeType === 'array') + attributeType = 'embeddedlist'; + else if (attributeType === 'json') + attributeType = 'embedded'; + else if (attributeType && + ['text', 'email', 'alphanumeric', 'alphanumericdashed'].indexOf(attributeType.toLowerCase()) >= 0) + attributeType = 'string'; + + if (propertyDefinition.columnName) + columnName = propertyDefinition.columnName; + + if (attributeType) { + var prop = { + name : columnName, + type : attributeType + }; + + // Check for required flag (not super elegant) + if (propAttributes && !!propAttributes.required) { + prop.mandatory = true; + } + + self.orientdbSchema[columnName] = prop; + + //log.debug('attributeType for ' + attributeName + ':', self.orientdbSchema[columnName].type); + + // process links + if (attributeType.toLowerCase().indexOf('link') === 0 && linkedClass){ + self.links.push({ + klass : tableName, + attributeName : columnName, + linkedClass : linkedClass + }); + } + } + }); +}; + + +/** + * Build Internal Indexes Dictionary based on the current schema. + * + * @api private + */ +Document.prototype._buildIndexes = function _buildIndexes() { + var self = this; + + Object.keys(this.schema).forEach(function(key) { + var columnName = self.schema[key].columnName ? self.schema[key].columnName : key; + var index = { + name: self.tableName + '.' + columnName + }; + + // If index key is `id` or columnName is `@rid` ignore it because OrientDB will automatically handle this + if(key === 'id' || columnName === '@rid') { + return; + } + + // Handle Unique Indexes + if(self.schema[key].unique) { + + // set the index type + index.type = 'unique'; + + // Store the index in the collection + self.indexes.push(index); + return; + } + + // Handle non-unique indexes + if(self.schema[key].index) { + + // set the index type + index.type = 'notunique'; + + // Store the index in the collection + self.indexes.push(index); + return; + } + }); +}; + diff --git a/lib/collection/edge.js b/lib/collection/edge.js new file mode 100644 index 0000000..c660baa --- /dev/null +++ b/lib/collection/edge.js @@ -0,0 +1,299 @@ +"use strict"; + +var _ = require('lodash'), + utils = require('../utils'), + Query = require('../query'), + Document = require('./document'), + Record = require('../record'), + log = require('debug-logger')('waterline-orientdb:edge'); + +/** + * Manage An Edge + * + * @param {Object} definition + * @param {Connection} connection + * @api public + */ +var Edge = module.exports = utils.extend(Document, function() { + Document.apply(this, arguments); + + // Set the orientdb super class ('document' / V / E) for this document + this.superClass = 'E'; + + // Set a class command that will be passed to Oriento ( 'undefined' / VERTEX / EDGE) + this.classCommand = 'EDGE'; + + // Information about edge's in and out properties + this.edgeSides = null; + + this._getEdgeSides(arguments[0], arguments[2]); +}); + + +///////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS +///////////////////////////////////////////////////////////////////////////////// + +/** + * Find Edges + * + * @param {Object} criteria + * @param {Function} callback + * @api public + */ +Edge.prototype.find = function find(criteria, cb) { + var self = this; + var _query, query; + + try { + _query = new Query(criteria, self.connection); + + // Replace junction foreign key columns for in and out + query = _query.getSelectQuery(self.tableNameOriginal, self.schema); + Object.keys(self.edgeSides).forEach(function(key){ + var junctionColumnName = self.edgeSides[key].junctionTableColumnName; + query.query[0] = query.query[0].replace(junctionColumnName, key + ' AS ' + junctionColumnName); + }); + query.query[0] = query.query[0].replace('SELECT ', ''); + query.query[0] = query.query[0].split('FROM')[0]; + criteria.select = [query.query[0]]; + } catch(e) { + log.error('Failed to compose find SQL query.', e); + return cb(e); + } + + var edge = self._getEdge(self.tableName, criteria.where); + if (edge) { + // Replace foreign keys with from and to + if(edge.from) { criteria.where.out = edge.from; } + if(edge.to) { criteria.where.in = edge.to; } + if(criteria.where){ + edge.keys.forEach(function(refKey) { delete criteria.where[refKey]; }); + } + } + + self.$super.prototype.find.call(self, criteria, cb); +}; + + + +/** + * Insert A New Edge + * + * @param {Object|Array} values + * @param {Function} callback + * @api public + */ +Edge.prototype.insert = function insert(values, cb) { + var self = this; + + var edge = self._getEdge(self.tableName, values); + + if (edge) { + // Create edge + values['@class'] = self.tableName; + var record = new Record(values, self.schema, self.connection, 'insert'); + edge.keys.forEach(function(refKey) { delete values[refKey]; }); + return self.connection.createEdge(edge.from, edge.to, record.values, cb); + } + + // creating an edge without connecting it, probably an edge created with orientdbClass = 'E', + // or some manual operation + self.$super.prototype.insert.call(self, values, cb); +}; + + +/** + * Destroy Documents + * + * @param {Object} criteria + * @param {Function} callback + * @api public + */ +Edge.prototype.destroy = function destroy(criteria, cb) { + var self = this; + cb = cb || _.noop; + + // TODO: should be in a transaction + self.find(criteria, function(err, results){ + if(err){ return cb(err); } + + if(results.length === 0){ return cb(null, results); } + + var rids = _.pluck(results, 'id'); + log.debug('Destroy rids: ' + rids); + + self.connection.db.delete(self.classCommand) + .where('@rid in [' + rids.join(',') + ']') + .one() + .then(function (count) { + if(parseInt(count) !== rids.length){ + return cb(new Error('Deleted count [' + count + '] does not match rids length [' + rids.length + + '], some vertices may not have been deleted')); + } + cb(null, results); + }) + .error(cb); + }); +}; + + +//Deletes a collection from database +Edge.prototype.drop = function (relations, cb) { + var self = this; + + // If class doesn't exist don't delete records + if(!self.databaseClass){ + return self.$super.prototype.drop.call(self, relations, cb); + } + + self.connection.db.delete(self.classCommand, self.tableName).one() + .then(function (count) { + log.debug('Drop [' + self.tableName + '], deleted records: ' + count); + self.$super.prototype.drop.call(self, relations, cb); + }) + .error(cb); +}; + + +///////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS +///////////////////////////////////////////////////////////////////////////////// + +/** + * Get edge sides + * + * Returns and object describing the out and the in sides of the edge + * + * @param {Object} collectionName + * @return {Object} + * @api private + */ +Edge.prototype._getEdgeSides = function _getEdgeSides(definition, collectionsById) { + collectionsById = collectionsById || {}; + var self = this, + vertexA, + vertexB; + + log.debug('_getEdgeSides: finding vertexes for: ' + self.tableName); + + Object.keys(self.schema).forEach(function(key) { + var reference = self.schema[key].references; + if(!reference) + return; + + var referencedCollection = _.find(_.values(self.connection.waterlineSchema), { identity: reference }); + var referencedSchema = referencedCollection.attributes; + var referencedDefinition = collectionsById[reference]; + var referencedAttributes = referencedDefinition.attributes; + + var referencedAttributeKey; + Object.keys(referencedSchema).forEach(function(referencedSchemaKey) { + var attribute = referencedSchema[referencedSchemaKey]; + if(self.identity === attribute.collection && (key === attribute.on || key === attribute.via)){ + if(!referencedAttributeKey){ + log.debug('_getEdgeSides: match found for ' + key + ': ' + referencedSchemaKey); + referencedAttributeKey = referencedSchemaKey; + } + else { + // More than one match, let's use via + // Logic for collections with associations to themselves + if(key === attribute.via){ + log.debug('_getEdgeSides: match found for ' + key + ': ' + referencedSchemaKey); + referencedAttributeKey = referencedSchemaKey; + } + } + } + }); + if(!referencedAttributeKey){ + return; + } + + var referencedAttribute = referencedSchema[referencedAttributeKey]; + + // we need referencedOriginalAttribute because referencedAttribute not always has dominant attribute + var referencedOriginalAttribute = referencedAttributes[referencedAttributeKey]; + + var vertex = { + referencedCollectionName: reference, + referencedCollectionTableName: referencedCollection.tableName || reference, + referencedAttributeKey: referencedAttributeKey, + referencedAttributeColumnName: referencedAttribute.columnName || referencedAttributeKey, + dominant: referencedAttribute.dominant || referencedOriginalAttribute.dominant, + junctionTableKey: key, + junctionTableColumnName: self.schema[key].columnName || key + }; + + if(vertex.dominant && (referencedOriginalAttribute.joinTableName || referencedOriginalAttribute.edge)){ + self.tableName = referencedOriginalAttribute.joinTableName || referencedOriginalAttribute.edge; + log.info('Edge [' + self.identity + '] has changed its tableName to [' + self.tableName + ']'); + } else if(vertex.dominant && referencedDefinition.joinTableNames && + referencedDefinition.joinTableNames[referencedAttributeKey]){ + self.tableName = referencedDefinition.joinTableNames[referencedAttributeKey]; + log.info('Edge [' + self.identity + '] has changed its tableName to [' + self.tableName + ']'); + } + + if(!vertexA) + vertexA = vertex; + else if(!vertexB) + vertexB = vertex; + else + log.error('Too many associations! Unable to process model [' + self.identity + '] attribute [' + key + '].'); + }); + + if(!vertexA || !vertexB){ + if(definition.junctionTable){ + log.warn('Vertex(es) missing for edge [' + self.tableName + '] and this edge is marked as waterline junction ' + + 'table. Association operations referenced by this edge will probably fail. Please check your schema.'); + } else { + log.info('Vertex(es) missing for edge [' + self.tableName + '].'); + } + return; + } + + if(!vertexA.dominant && !vertexB.dominant){ + var dominantVertex = (vertexA.junctionTableKey < vertexB.junctionTableKey) ? vertexA : vertexB; + dominantVertex.dominant = true; + + log.warn(self.identity + ' junction table associations [' + vertexA.referencedCollectionName + + ', ' + vertexB.referencedCollectionName + '] have no dominant through association. ' + + dominantVertex.junctionTableKey + + ' was chosen as dominant.'); + } + + if(vertexA.dominant){ + vertexA.referencedAttributeEdge = 'out_' + self.tableName; + vertexA.edgeOppositeEnd = 'in'; + vertexB.referencedAttributeEdge = 'in_' + self.tableName; + vertexB.edgeOppositeEnd = 'out'; + self.edgeSides = { out: vertexA, in: vertexB }; + return; + } + + if(vertexB.dominant){ + vertexA.referencedAttributeEdge = 'in_' + self.tableName; + vertexA.edgeOppositeEnd = 'out'; + vertexB.referencedAttributeEdge = 'out_' + self.tableName; + vertexB.edgeOppositeEnd = 'in'; + self.edgeSides = { out: vertexB, in: vertexA }; + } +}; + + +/** + * Get edge + * + * Normalizes data for edge creation + * + * @param {Object} values + * @return {Object} + * @api private + */ +Edge.prototype._getEdge = function _getEdge(collectionName, values) { + var self = this; + return { + from : values && values[self.edgeSides.out.junctionTableColumnName], + to : values && values[self.edgeSides.in.junctionTableColumnName], + keys: [self.edgeSides.out.junctionTableColumnName, self.edgeSides.in.junctionTableColumnName] + }; +}; diff --git a/lib/collection/index.js b/lib/collection/index.js new file mode 100644 index 0000000..af72078 --- /dev/null +++ b/lib/collection/index.js @@ -0,0 +1,20 @@ +"use strict"; + +var Collection = module.exports = function Collection (definition, connection, databaseClass, collectionsByIdentity) { + if(connection.config.options.databaseType === 'document' || + definition.orientdbClass === '' || + definition.orientdbClass === 'document'){ + return new Collection.Document(definition, connection, databaseClass, collectionsByIdentity); + } + + if(definition.orientdbClass === 'E' || + (definition.junctionTable && definition.orientdbClass !== 'V')){ + return new Collection.Edge(definition, connection, databaseClass, collectionsByIdentity); + } + + return new Collection.Vertex(definition, connection, databaseClass, collectionsByIdentity); +}; + +Collection.Document = require('./document'); +Collection.Vertex = require('./vertex'); +Collection.Edge = require('./edge'); diff --git a/lib/collection/vertex.js b/lib/collection/vertex.js new file mode 100644 index 0000000..eff6a39 --- /dev/null +++ b/lib/collection/vertex.js @@ -0,0 +1,84 @@ +"use strict"; + +var utils = require('../utils'), + _ = require('lodash'), + Document = require('./document'), + log = require('debug-logger')('waterline-orientdb:vertex'); + +/** + * Manage A Vertex + * + * @param {Object} definition + * @param {Connection} connection + * @api public + */ +var Vertex = module.exports = utils.extend(Document, function() { + Document.apply(this, arguments); + + // Set the orientdb super class ('document' / V / E) for this document + this.superClass = 'V'; + + // Set a class command that will be passed to Oriento ( 'undefined' / VERTEX / EDGE) + this.classCommand = 'VERTEX'; +}); + + + +///////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS +///////////////////////////////////////////////////////////////////////////////// + + +/** + * Destroy Documents + * + * @param {Object} criteria + * @param {Function} callback + * @api public + */ +Vertex.prototype.destroy = function destroy(criteria, cb) { + var self = this; + cb = cb || _.noop; + + // TODO: should be in a transaction + self.find(criteria, function(err, results){ + if(err){ return cb(err); } + + if(results.length === 0){ return cb(null, results); } + + var rids = _.pluck(results, 'id'); + log.debug('Destroy rids: ' + rids); + + self.connection.db.delete(self.classCommand) + .where('@rid in [' + rids.join(',') + ']') + .one() + .then(function (count) { + if(parseInt(count) !== rids.length){ + return cb(new Error('Deleted count [' + count + '] does not match rids length [' + rids.length + + '], some vertices may not have been deleted')); + } + cb(null, results); + }) + .error(cb); + }); +}; + + + +//Deletes a collection from database +Vertex.prototype.drop = function (relations, cb) { + var self = this; + + // If class doesn't exist don't delete records + if(!self.databaseClass){ + return self.$super.prototype.drop.call(self, relations, cb); + } + + self.connection.db.delete(self.classCommand, self.tableName).one() + .then(function (count) { + log.debug('Drop [' + self.tableName + '], deleted records: ' + count); + self.$super.prototype.drop.call(self, relations, cb); + }) + .error(cb); +}; + diff --git a/lib/connection.js b/lib/connection.js index d76177b..38659b6 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,711 +1,546 @@ "use strict"; -/*jshint maxlen: 200 */ + var Oriento = require('oriento'), - Q = require('q'), async = require('async'), _ = require('lodash'), utils = require('./utils'), - Query = require('./query'), - Document = require('./document'), Associations = require('./associations'), - log = require('debug-logger')('waterline-orientdb:connection'); - - -module.exports = (function () { - - var defaults = { - createCustomIndex: false, - idProperty: 'id', - options: { - fetchPlanLevel: 1, - parameterized: false - } - }, - dbDefaults = { - type: 'graph', - storage: 'plocal' - }, - server, - transformers = { - '@rid': function (rid) { - return '#' + rid.cluster + ':' + rid.position; - } - }, - DbHelper = function (db, collections, config) { - this.db = db; - this.collections = collections; - this.collectionsByIdentity = _.reduce(collections, function(accumulator, collection){ - accumulator[collection.identity] = collection; - return accumulator; - }, {}); - var auxDefaults = _.merge({}, defaults); - this.config = _.merge(auxDefaults, config); - this.associations = new Associations(this); - this.server = server; - }, - ensureDB = function (connectionProps) { - var dbProps = (typeof connectionProps.database === 'object') ? connectionProps.database : { name: connectionProps.database }; - dbProps.username = connectionProps.user; - dbProps.password = connectionProps.password; - if(connectionProps.options.storage){ - dbProps.storage = connectionProps.options.storage; - } - dbProps = _.extend({}, dbDefaults, dbProps); - var deferred = Q.defer(); - log.debug('Looking for database', connectionProps.database); - server.list().then(function (dbs) { - var dbExists = _.find(dbs, function (db) { - return db.name === dbProps.name; - }); - if (dbExists) { - log.debug('database found.'); - deferred.resolve(server.use(dbProps)); - } else { - log.debug('database not found, will create it.'); - server.create(dbProps).then(function (db) { - deferred.resolve(db); - }); - } - - - }); - - return deferred.promise; - }, - getDb = function (connection) { - - var orientoConnection = { - host: connection.host, - port: connection.host, - username: connection.user, - password: connection.password, - transport: connection.transport || 'binary', - enableRIDBags: false, - useToken: false - }; - - if (!server){ - log.info('Connecting to database...'); - server = new Oriento(orientoConnection); - } - - return ensureDB(connection); - - }; - - DbHelper.prototype.db = null; - DbHelper.prototype.collections = null; - DbHelper.prototype.config = null; - DbHelper.prototype._classes = null; - - - DbHelper.prototype.ensureIndex = function () { - log.debug('Creating indices...', Object.keys(this), this.config); - var deferred = Q.defer(), - db = this.db, - idProp = this.config.idProperty, - indexName = 'V.' + idProp; - - async.auto({ - getVClass: function (next) { - - db.class.get('V') - .then(function (klass, err) { - next(err, klass); - }); - }, - getProps: ['getVClass', - function (next, results) { - var klass = results.getVClass; - klass.property.list() - .then(function (properties, err) { - next(err, properties); - }); - }], - getIdProp: ['getProps', - function (next, results) { - var klass = results.getVClass, - properties = results.getProps, - prop = _.findWhere(properties, { - name: idProp - }); - - if (!prop) { - klass.property.create({ - name: idProp, - type: 'String' - }).then(function (property, err) { - next(err, property); - }); - return; - } - next(null, prop); - }], - ensureIndex: ['getIdProp', - function (next) { - - var createIndex = function (err) { - if (err) { - db.index.create({ - name: indexName, - type: 'unique' - }).then(function (index, err) { - next(err, true); - }); - return; - } - }; - - db.index.get(indexName) - .error(createIndex) - .done(function (index, err) { - //if index not found then create it - return index && next(err, true); - }); - }] - }, - function (err, results) { - if (err) { - log.error('error while creating indices', err); - deferred.reject(err); - return; - } - log.debug('indices created.'); - deferred.resolve(results); - }); - return deferred.promise; + log = require('debug-logger')('waterline-orientdb:connection'), + Collection = require('./collection'), + Sequel = require('waterline-sequel-orientdb'); + +// waterline-sequel-orientdb options +var sqlOptions = { + parameterized : true, + caseSensitive : false, + escapeCharacter : '', + casting : false, + canReturnValues : true, + escapeInserts : true +}; + +/** + * Manage a connection to an OrientDB Server + * + * @param {Object} config + * @param {Object} collections + * @param {Function} callback + * @return {Object} + * @api private + */ +var Connection = module.exports = function Connection(config, collections, cb) { + var self = this; + + // holds the adapter config + this.config = config; + + // holds an associations object used for joins + this.associations = new Associations(self); + + // Hold the waterline schema, used by query namely waterline-sequel-orientdb + this.waterlineSchema = _.values(collections)[0].waterline.schema; + + // update sqlOptions config and instantiate a sequel helper + sqlOptions.parameterized = self.config.options.parameterized; + this.sequel = new Sequel(self.waterlineSchema, sqlOptions); + + // holds existing classes from OrientDB + this.dbClasses = {}; + + // Holds a registry of collections (indexed by tableName) + this.collections = {}; + + // Holds a registry of collections (indexed by identity) + this.collectionsByIdentity = {}; + + // hold an instance of oriento + this.server = null; + + // aux variables used to figure out when all collections have been synced + this._collectionSync = { + modifiedCollections: [], + postProcessed: false, + itemsToProcess: _.clone(collections) + }; + + self._init(config, collections, cb); +}; + - }; - +///////////////////////////////////////////////////////////////////////////////// +// PUBLIC METHODS +///////////////////////////////////////////////////////////////////////////////// - /*Makes sure that all the collections are synced to database classes*/ - DbHelper.prototype.registerCollections = function () { - var deferred = Q.defer(), - me = this, - db = me.db, - collections = this.collections, - linksToBeCreated = []; - - async.auto({ - - ensureIndex: function (next) { - if (me.config.createCustomIndex) { - me.ensureIndex().then(function (indexEnsured, err) { - next(err, indexEnsured); - }); - return; - } - next(null, true); - }, - - getClasses: ['ensureIndex', - function (next) { - db.class.list().then(function (classes, err) { - next(err, classes); - }); - }], - - registerClasses: ['getClasses', - function (complete, results) { - var classes = results.getClasses, - klassesToBeAdded = _.filter(collections, function (v, k) { - return _.isUndefined(_.find(classes, function (klass) { - return k == klass.name; - })); - }); - - if (klassesToBeAdded.length > 0) { - - async.mapSeries(klassesToBeAdded, function (collection, next) { - var tableName = collection.tableName || collection.identity; - var collectionClass = collection.edge || utils.isJunctionTableThrough(collection) ? 'E' : 'V'; - - db.class - .create(tableName, collectionClass) - .then(function (klass, err) { - //TODO: refactor: move this to own method!!! - // Create OrientDB schema - if (collection.attributes){ - log.debug('Creating DB class [' + tableName + '] for collection [' + collection.identity + ']'); - Object.keys(collection.attributes).forEach(function(attributeName){ - if(attributeName === 'id'){ - // @rid is the equivalent of id, no need to add id. - return; - } - var linkedClass = null, - attributeType = null, - columnName = attributeName; - if(typeof collection.attributes[attributeName] === 'string') - attributeType = collection.attributes[attributeName]; - else if (typeof collection.attributes[attributeName] === 'function') - return; - else if (collection.attributes[attributeName].model || collection.attributes[attributeName].references){ - linkedClass = collection.attributes[attributeName].model || collection.attributes[attributeName].references; - var useLink = me.collectionsByIdentity[linkedClass].primaryKey === 'id'; - attributeType = useLink ? 'Link' : collection.pkFormat; - } - else if (collection.attributes[attributeName].foreignKey){ - attributeType = 'Link'; - } - else if (collection.attributes[attributeName].collection){ - attributeType = 'linkset'; - linkedClass = collection.attributes[attributeName].collection; - } - else - attributeType = collection.attributes[attributeName].type; - - if (attributeType === 'array') - attributeType = 'embeddedlist'; - else if (attributeType === 'json') - attributeType = 'embedded'; - else if (['text', 'email', 'alphanumeric', 'alphanumericdashed'].indexOf(attributeType) >= 0) - attributeType = 'string'; - - if(collection.attributes[attributeName].columnName) - columnName = collection.attributes[attributeName].columnName; - - //log.debug('attributeType for ' + attributeName + ':', attributeType); - - if(attributeType){ - var prop = { - name: columnName, - type: attributeType - }; - if(!!collection.attributes[attributeName].required) { - prop.mandatory = true; - } - klass.property.create(prop).then(function(){ - if(!!collection.attributes[attributeName].unique){ - db.index.create({ - name: tableName + '.' + columnName, - type: 'unique' - }); - } else if(!!collection.attributes[attributeName].index){ - db.index.create({ - name: tableName + '.' + columnName, - type: 'notunique' - }); - } - }); - if(attributeType.toLowerCase().indexOf('link') === 0 && linkedClass) - linksToBeCreated.push({ - attributeName: columnName, - klass: klass, - linkedClass: linkedClass - }); - } - }); - } - next(err, klass); - }); - }, - function (err, created) { - complete(err, created); - }); - return; - } - complete(null, classes); - }], - - setClasses: ['registerClasses', - function(complete, results){ - var allClasses = (results.getClasses || []).concat(results.registerClasses); - - //flatten the array of classes to key value pairs to ease the retrieval of classes - me._classes = _.reduce(allClasses, function (initial, klass) { - - var collection = _.find(me.collections, function (v) { - return (v.tableName || v.identity) === klass.name; - }); - //If a matching collection is found then store the class reference using the collection name else use class name itself - initial[(collection && collection.identity) || klass.name] = klass; - return initial; - }, {}); - - complete(null, me._classes); - } - ], - - registerLinks: ['setClasses', - function (complete, results) { - - async.map(linksToBeCreated, function (link, next) { - var linkedClass = results.setClasses[link.linkedClass]; - link.klass.property.update({ - name: link.attributeName, - linkedClass: linkedClass.name - }) - .then(function(){ - next(null, link.klass); - }) - .error(next); - }, - function (err, created) { - complete(err, created); - }); - }], - - rgisterTransformers: ['setClasses', function (complete, results) { - // TODO: this should not be done in such a generic fashion, but truth is - // we currently need this for fetch plans to work. We probably should use these transformers - // inside a collection class such as, e.g. https://github.com/balderdashy/sails-mongo/blob/master/lib/collection.js - // For an example of a transformer, look at: https://github.com/codemix/oriento/blob/aa6257d31d1b873cc19ee07df84e162ea86ff998/test/db/db-test.js#L79 - - function transformer (data) { - var newData = {}; - var keys = Object.keys(data), - length = keys.length, - key, i; - for (i = 0; i < length; i++) { - key = keys[i]; - newData[key] = data[key]; - } - return newData; - } - - var klasses = results.setClasses; - Object.keys(klasses).forEach(function(klassKey){ - db.registerTransformer(klasses[klassKey].name, transformer); - }); - complete(); - - }] - }, - function (err, results) { - if (err) { - deferred.reject(err); - return; - } - - deferred.resolve(results.registerClasses); - }); - return deferred.promise; - }; + +/** + * Describe + * + * @param {String} collectionName + * @param {Function} callback + */ +Connection.prototype.describe = function describe(collectionName, cb) { + var self = this; + + if(self._collectionSync.itemsToProcess[collectionName]){ + delete self._collectionSync.itemsToProcess[collectionName]; + } + + var collection = self.collections[collectionName]; + if(!collection.databaseClass) { return cb(); } + + var schema = {}; + + collection.databaseClass.property.list() + .then(function(properties){ + + // TODO: don't copy collection.schema blindly, check mandatory and indices! + _.forEach(properties, function(property){ + if(collection.schema[property.name]){ + schema[property.name] = collection.schema[property.name]; + } + // else { + // // TODO: include properties found in database which are not in collection.schema + // } + }); + + if(collection.schema.id){ + schema.id = collection.schema.id; + } + + // describting last collection and it exists, calling postProcessing now as there won't + // be a subsequent call to define + if(Object.keys(self._collectionSync.itemsToProcess).length === 0){ + self.postProcessing(function(err){ + if(err){ return cb(err); } + cb(null, schema); + }); + } else { + cb(null, schema); + } + }); + // TODO: fetch indexes +}; - /** - * query - * - * exposes Oriento's query - */ - DbHelper.prototype.query = function(query, options, cb) { - if (options && !cb) { - cb = options; - options = undefined; +/** + * Create Collection + * + * @param {String} collectionName + * @param {Object} definition + * @param {Function} cb + */ +Connection.prototype.createCollection = function createCollection(collectionName, definition, cb) { + var self = this; + + var collection = self.collections[collectionName]; + + // Create the Collection + if (collection.databaseClass) { + // TODO: properties may need updating ? + if(Object.keys(self._collectionSync.itemsToProcess).length === 0){ + return self.postProcessing(function(err){ + if(err){ return cb(err); } + cb(null, collection.schema); + }); + } else { + return cb(null, collection.schema); } + } - this.db.query(query, options) - .all() - .then(function(res) { - cb(null, utils.rewriteIdsRecursive(res)); - }) - .error(cb); - }; - - /** - * getDB - * - * returns the oriento db object - */ - DbHelper.prototype.getDB = function(cb) { - return cb(this.db); - }; - - DbHelper.prototype.getServer = function(cb) { - return cb(this.server); - }; + self.db.class.create(collection.tableName, collection.superClass) + .then(function(klass, err) { + if (err) { log.error('db.class.create: ' + err); } - - /** - * Retrieves records of class collection that fulfill the criteria in options - */ - DbHelper.prototype.find = function(collection, options, cb) { - var collectionInstance = this.collections[collection]; - var schema = collectionInstance.waterline.schema; - var attributes = collectionInstance.attributes; - var _query, query; - - try { - _query = new Query(options, schema, this.config); - } catch(err) { return cb(err); } - - var edge; - if (utils.isJunctionTableThrough(collectionInstance)) { - edge = this.associations.getEdge(collection, options.where); - } - if (edge) { - // Replace foreign keys with from and to - if(edge.from) { _query.criteria.where.out = edge.from; } - if(edge.to) { _query.criteria.where.in = edge.to; } - if(_query.criteria.where){ - edge.keys.forEach(function(refKey) { delete _query.criteria.where[refKey]; }); + collection.databaseClass = klass; + + self._collectionSync.modifiedCollections.push(collection); + + // Create properties + _.values(collection.orientdbSchema).forEach(function(prop) { + klass.property.create(prop).then(); + }); + + // Create transformations + function transformer(data) { + var newData = {}; + var keys = Object.keys(data), length = keys.length, key, i; + for ( i = 0; i < length; i++) { + key = keys[i]; + newData[key] = data[key]; + } + return newData; } - } - - try { - query = _query.getSelectQuery(collection); - } catch(e) { - log.error('Failed to compose find SQL query.', e); - return cb(e); - } - - log.debug('OrientDB query:', query.query[0]); - - var opts = { params: query.params || {} }; - if(query.params){ - log.debug('params:', opts); - } - if(options.fetchPlan){ - opts.fetchPlan = options.fetchPlan.where; - log.debug('opts.fetchPlan:', opts.fetchPlan); - } - - this.db - .query(query.query[0], opts) - .all() - .then(function (res) { - if (res && options.fetchPlan) { - //log.debug('res', res); - cb(null, utils.rewriteIdsRecursive(res, attributes)); + self.db.registerTransformer(collectionName, transformer); + + // Create Indexes + self._ensureIndexes(klass, collection.indexes, function(err/*, result*/){ + if(err) { return cb(err); } + + // Post process if all collections have been processed + if(Object.keys(self._collectionSync.itemsToProcess).length === 0){ + self.postProcessing(function(err){ + if(err){ return cb(err); } + cb(null, collection.schema); + }); } else { - cb(null, utils.rewriteIds(res, attributes)); + cb(null, collection.schema); } - }) - .error(function (e) { - log.error('Failed to query the DB.', e); - cb(e); }); - }; - + }); +}; - //Deletes a collection from database - DbHelper.prototype.drop = function (collection, relations, cb) { - //return this.db.class.drop(collection) - return this.db.query('DROP CLASS ' + collection + ' UNSAFE') - .then(function (res) { - cb(null, res); - }) - .error(cb); +/** + * Add a property to a class + */ +Connection.prototype.addAttribute = function(collectionName, attrName, attrDef, cb) { + var self = this; + + var collection = self.collections[collectionName]; + + var prop; + + if(collection.orientdbSchema[attrName]){ + prop = collection.orientdbSchema[attrName]; + } else { + prop = { + name : attrName, + type : attrDef.type }; - - - - /** - * Creates a new document from a collection - */ - DbHelper.prototype.create = function(collection, options, cb) { - var attributes, _document, collectionInstance, - self = this; - - collectionInstance = self.collections[collection]; - attributes = collectionInstance.definition; - - _document = new Document(options, attributes, self, 'insert'); - - return self.dbCreate(collection, _document.values, cb); - }; + } + collection.databaseClass.property.create(prop).then(function(err, property){ + cb(null, property); + }) + .error(cb); +}; + + +/** + * Post Processing + * + * called after all collections have been created + */ +Connection.prototype.postProcessing = function postProcessing(cb){ + var self = this; - /** - * Calls Oriento to save a new document - */ - DbHelper.prototype.dbCreate = function(collection, options, cb) { - var collectionInstance = this.collections[collection]; - var attributes = collectionInstance.attributes; - - var edge; - if (utils.isJunctionTableThrough(collectionInstance)){ - edge = this.associations.getEdge(collection, options); - } - - if (edge) { - // Create edge - options['@class'] = collection; - edge.keys.forEach(function(refKey) { delete options[refKey]; }); - return this.createEdge(edge.from, edge.to, options, cb); - } - - this.db.insert() - .into(collection) - .set(options) - .transform(transformers) - .one() - .then(function(res) { - cb(null, utils.rewriteIds(res, attributes)); + if(self._collectionSync.postProcessed) { + log.debug('Attempted to postprocess twice. This shouln\'t happen, try to improve the logic behind this.'); + return cb(); + } + self._collectionSync.postProcessed = true; + + log.info('All classes created, post processing'); + + function createLink(collection, complete){ + async.each(collection.links, function(link, next){ + var linkClass = collection.databaseClass; + var linkedClass = self.collections[link.linkedClass]; + + linkClass.property.update({ + name : link.attributeName, + linkedClass : linkedClass.tableName }) - .error(function(err) { - log.error('Failed to create object. DB error.', err); - cb(err); - }); - }; - + .then(function(dbLink){ + next(null, dbLink); + }) + .error(next); + }, complete); + } + + async.each(self._collectionSync.modifiedCollections, createLink, cb); +}; + + +/** + * query + * + * exposes Oriento's query + */ +Connection.prototype.query = function(query, options, cb) { + if (options && !cb) { + cb = options; + options = undefined; + } + + this.db.query(query, options) + .all() + .then(function(res) { + cb(null, utils.rewriteIdsRecursive(res)); + }) + .error(cb); +}; + + +/** + * returns the oriento collection object + */ +Connection.prototype.native = function(collection, cb) { + return cb(this.collections[collection].databaseClass); +}; + + +/** + * returns the oriento db object + */ +Connection.prototype.getDB = function(cb) { + return cb(this.db); +}; + +/** + * returns the oriento object + */ +Connection.prototype.getServer = function(cb) { + return cb(this.server); +}; - /** - * Updates a document from a collection - */ - DbHelper.prototype.update = function(collection, options, values, cb) { - var _query, - _document, - where, - self = this; - var collectionInstance = this.collections[collection]; - var schema = collectionInstance.waterline.schema; - var attributes = collectionInstance.attributes; - - // Catch errors from building query and return to the callback - try { - _query = new Query(options, schema, self.config); - _document = new Document(values, collectionInstance.definition, self); - where = _query.getWhereQuery(collection); - } catch(e) { - log.error('Failed to compose update SQL query.', e); - return cb(e); - } + +/** + * Retrieves records of class collection that fulfill the criteria in options + */ +Connection.prototype.find = function(collection, options, cb) { + this.collections[collection].find(options, cb); +}; + + +/** + * Deletes a collection from database + */ +Connection.prototype.drop = function (collectionName, relations, cb) { + this.collections[collectionName].drop(relations, cb); +}; + + +/** + * Creates a new document from a collection + */ +Connection.prototype.create = function(collection, options, cb) { + this.collections[collection].insert(options, cb); +}; + - var query = this.db.update(collection) - .set(_document.values) - .transform(transformers) - .return('AFTER'); - - if(where.query[0]){ - query.where(where.query[0]); - if(where.params){ - query.addParams(where.params); - } +/** + * Updates a document from a collection + */ +Connection.prototype.update = function(collection, options, values, cb) { + this.collections[collection].update(options, values, cb); +}; + + +/* + * Deletes a document from a collection + */ +Connection.prototype.destroy = function(collection, options, cb) { + this.collections[collection].destroy(options, cb); +}; + +/* + * Peforms a join between 2-3 orientdb collections + */ +Connection.prototype.join = function(collection, options, cb) { + var self = this; + + self.associations.join(collection, options, function(err, results){ + if(err) { return cb(err); } + if(self.config.options.removeCircularReferences){ + utils.removeCircularReferences(results); } - - query - .all() - .then(function(res) { - cb(null, utils.rewriteIds(res, attributes)); - }) - .error(function(err) { - log.error('Failed to update, error:', err); - cb(err); + cb(null, results); + }); +}; + +/* + * Creates edge between two vertices pointed by from and to + * Keeps the same interface as described in: + * https://github.com/codemix/oriento/blob/6b8c40e7f1f195b591b510884a8e05c11b53f724/README.md#creating-an-edge-with-properties + * + */ +Connection.prototype.createEdge = function(from, to, options, cb) { + var schema, + klass = 'E'; + cb = cb || _.noop; + options = options || {}; + + if(options['@class']){ + klass = options['@class']; + schema = this.collections[klass] && this.collections[klass].schema; + } + + this.db.create('EDGE', klass).from(from).to(to) + .set(options) + .one() + .then(function(res) { + cb(null, utils.rewriteIds(res, schema)); + }) + .error(cb); +}; + + +/* + * Removes edges between two vertices pointed by from and to + */ +Connection.prototype.deleteEdges = function(from, to, options, cb) { + cb = cb || _.noop; + + if(!options){ + return this.db.delete('EDGE').from(from).to(to).scalar() + .then(function(count) { + cb(null, count); }); - }; - + } + + // temporary workaround for issue: https://github.com/orientechnologies/orientdb/issues/3114 + var className = _.isString(options) ? options : options['@class']; + var command = 'DELETE EDGE FROM ' + from + ' TO ' + to + " where @class = '" + className + "'"; + this.db.query(command) + .then(function(count) { + cb(null, count); + }); +}; - /* - * Deletes a document from a collection - */ - DbHelper.prototype.destroy = function(collection, options, cb) { - var collectionInstance = this.collections[collection]; - var klassToDelete = utils.isJunctionTableThrough(collectionInstance) ? 'EDGE' : 'VERTEX'; - var self = this; - cb = cb || _.noop; + + +///////////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS +///////////////////////////////////////////////////////////////////////////////// + +/** + * Ensure Indexes + * + * @param {Object} oriento class + * @param {Array} indexes + * @param {Function} callback + * @api private + */ +Connection.prototype._ensureIndexes = function _ensureIndexes(collection, indexes, cb) { + var self = this; + + function createIndex(item, next) { + self.db.index.create(item) + .then(function(index){ next(null, index); }) + .error(next); + } + + async.each(indexes, createIndex, cb); +}; + + +/** + * Initialize a connection + * + * @param {Object} config + * @param {Object} collections + * @param {Object} cb + */ +Connection.prototype._init = function _init(config, collections, cb) { + var self = this; + + this.server = self._getOriento(config); + + function ensureDbAndListClasses(done){ + self._ensureDB(config) + .then(function(database){ + self.db = database; + return database.class.list(); + }) + .then(function(classes){ + done(null, classes); + }) + .catch(done); + } + + function initializeCollections(done){ + self._initializeCollections(collections); + done(); + } + + async.parallel({ + classes : ensureDbAndListClasses, + collections : initializeCollections + }, function(err, results){ + if(err) { return cb(err); } + + results.classes.forEach(function(klass){ + self.dbClasses[klass.name] = klass; + }); - // TODO: should be in a transaction - self.find(collection, options, function(err, results){ - if(err){ return cb(err); } - - if(results.length === 0){ return cb(null, results); } - - var rids = _.pluck(results, 'id'); - log.info('deleting rids: ' + rids); - - self.db.delete(klassToDelete) - .where('@rid in [' + rids.join(',') + ']') - .one() - .then(function (count) { - if(parseInt(count) !== rids.length){ - return cb(new Error('Deleted count [' + count + '] does not match rids length [' + rids.length + - '], some vertices may not have been deleted')); - } - cb(null, results); - }) - .error(cb); + _.values(self.collections).forEach(function(collection) { + // has to run after collection instatiation due to tableName redefinition on edges + collection.databaseClass = self.dbClasses[collection.tableName]; }); - }; - - - /* - * Creates edge between two vertices pointed by from and to - * Keeps the same interface as described in: - * https://github.com/codemix/oriento/blob/6b8c40e7f1f195b591b510884a8e05c11b53f724/README.md#creating-an-edge-with-properties - * - */ - DbHelper.prototype.createEdge = function(from, to, options, cb) { - var attributes, - klass = 'E'; - cb = cb || _.noop; - options = options || {}; - if(options['@class']){ - klass = options['@class']; - attributes = this.collections[klass] && this.collections[klass].attributes; - } - - this.db.create('EDGE', klass).from(from).to(to) - .set(options) - .one() - .then(function(res) { - cb(null, utils.rewriteIds(res, attributes)); - }) - .error(cb); - }; + cb(); + + }); +}; + +/** + * Prepares and oriento config and creates a new instance + * + * @param {Object} config + */ +Connection.prototype._getOriento = function _getOriento(config) { + var orientoOptions = { + host : config.host, + port : config.host, + username : config.user, + password : config.password, + transport : config.options.transport, + enableRIDBags : false, + useToken : false + }; + return new Oriento(orientoOptions); +}; - /* - * Removes edges between two vertices pointed by from and to - */ - DbHelper.prototype.deleteEdges = function(from, to, options, cb) { - cb = cb || _.noop; - - if(!options){ - return this.db.delete('EDGE').from(from).to(to).scalar() - .then(function(count) { - cb(null, count); - }); - } +/** + * Check if a database exists and if not, creates one + * + * @param {Object} config + */ +Connection.prototype._ensureDB = function _ensureDB (config) { + var self = this; + + log.info('Connecting to database...'); + + var dbOptions = typeof config.database === 'object' ? config.database : { name: config.database }; + dbOptions.username = config.options.databaseUser || config.user; + dbOptions.password = config.options.databasePassword || config.password; + dbOptions.storage = config.options.storage; + dbOptions.type = config.options.databaseType; + + return self.server.list() + .then(function(dbs) { + var dbExists = _.find(dbs, function(db) { + return db.name === dbOptions.name; + }); + if (dbExists) { + log.info('Database ' + dbOptions.name + ' found.'); + return self.server.use(dbOptions); + } else { + log.info('Database ' + dbOptions.name + ' not found, will create it.'); + return self.server.create(dbOptions); + } + }); +}; - // temporary workaround for issue: https://github.com/orientechnologies/orientdb/issues/3114 - var className = _.isString(options) ? options : options['@class']; - var command = 'DELETE EDGE FROM ' + from + ' TO ' + to + " where @class = '" + className + "'"; - this.db.query(command) - .then(function(count) { - cb(null, count); - }); - }; - +/** + * Initializes the database collections + * + * @param {Object} collections + */ +Connection.prototype._initializeCollections = function _initializeCollections(collections) { + var self = this; - var connect = function (connection, collections) { - // if an active connection exists, use - // it instead of tearing the previous - // one down - var d = Q.defer(); - - try { - - getDb(connection, collections).then(function (db) { - var helper = new DbHelper(db, collections, connection); - - helper.registerCollections() - .then(function () { - d.resolve(helper); - }); - - }); - - } catch (err) { - log.error('An error has occured while trying to connect to OrientDB.', err); - d.reject(err); - throw err; - } - - - - return d.promise; - - }; - - - return { - create: function (connection, collections) { - return connect(connection, collections); - } - }; + var collectionsByIdentity = _.reduce(collections, function(accumulator, collection){ + accumulator[collection.identity] = collection; + return accumulator; + }, {}); + + Object.keys(collections).forEach(function(key) { + self.collections[key] = new Collection(collections[key], self, collectionsByIdentity); + self.collectionsByIdentity[self.collections[key].identity] = self.collections[key]; + }); +}; -})(); diff --git a/lib/query.js b/lib/query.js index 97b4f8a..db0475f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -5,19 +5,7 @@ var _ = require('lodash'), utils = require('./utils'), hop = utils.object.hop, - Sequel = require('waterline-sequel-orientdb'), RID = require('oriento').RID; - - -// waterline-sequel-orientdb options -var sqlOptions = { - parameterized : true, - caseSensitive : false, - escapeCharacter : '', - casting : false, - canReturnValues : true, - escapeInserts : true -}; /** @@ -28,19 +16,16 @@ var sqlOptions = { * @param {Object} options * @api private */ -var Query = module.exports = function Query(options, schema, adapterConfig) { - // Apply configs - sqlOptions.parameterized = _.isUndefined(adapterConfig.options.parameterized) ? - sqlOptions.parameterized : adapterConfig.options.parameterized; - - // Cache the schema for use in parseTypes - this.schema = schema; - +var Query = module.exports = function Query(options, connection) { + + // Sequel builder + this.sequel = connection.sequel; + + // decode + this.decodeURIComponent = connection.config.options.decodeURIComponent; + // Normalize Criteria this.criteria = this.normalizeCriteria(options); - - // Instantiate a sequel helper - this.sequel = new Sequel(schema, sqlOptions); return this; }; @@ -150,7 +135,9 @@ Query.prototype.fixId = function fixId(original) { // Normalize `id` key into orientdb `@rid` if (key === 'id' && !hop(this, '@rid')) { key = '@rid'; - obj[key] = self.processIdValue(val); + obj[key] = self.decode(val); + } else if(key === '@rid') { + obj[key] = self.decode(val); } else { obj[key] = val; } @@ -162,25 +149,31 @@ Query.prototype.fixId = function fixId(original) { /** - * Convert ID value to string + * Decodes ID from encoded URI component * * @api private * * @param {Array|Object|String} idValue - * @returns {String} + * @returns {Array|String} */ -Query.prototype.processIdValue = function processIdValue(idValue) { - var newVal = idValue; - if(!_.isArray(idValue)){ - newVal = _.isObject(idValue) ? '#' + idValue.cluster + ':' + idValue.position : idValue; - } else { - newVal = []; - idValue.forEach(function(rid){ - var value = _.isObject(rid) ? '#' + rid.cluster + ':' + rid.position : rid; - newVal.push(value); - }); +Query.prototype.decode = function decode(idValue) { + var self = this; + + if(! idValue || !self.decodeURIComponent) { return idValue; } + + function decodeURI(id){ + var res = id; + if(id.indexOf('%23') === 0){ + res = decodeURIComponent(id); + } + return res; } - return newVal; + + if(_.isArray(idValue)){ + return _.map(idValue, decodeURI); + } + + return decodeURI(idValue); }; @@ -191,15 +184,14 @@ Query.prototype.processIdValue = function processIdValue(idValue) { * @param {String} collection * @returns {Object} */ -Query.prototype.getSelectQuery = function getSelectQuery(collection) { +Query.prototype.getSelectQuery = function getSelectQuery(collection, attributes) { var self = this; - var currentTable = _.find(_.values(self.schema), { tableName: collection }).identity; var _query = self.sequel.find(collection, self.criteria); _query.query[0] = _query.query[0].replace(collection.toUpperCase(), collection); _query.params = _.reduce(_query.values[0], function(accumulator, value, index){ var key = _query.keys[0][index]; - var attribute = utils.getAttributeAsObject(self.schema[currentTable].attributes, key) || {}; + var attribute = utils.getAttributeAsObject(attributes, key) || {}; var foreignKeyOrId = key === '@rid' || key && key.indexOf('.@rid') !== -1 || attribute.foreignKey || attribute.model || false; var paramValue = foreignKeyOrId && _.isString(value) && utils.matchRecordId(value) ? new RID(value) : value; diff --git a/lib/document.js b/lib/record.js similarity index 88% rename from lib/document.js rename to lib/record.js index c57c6d4..ea61057 100644 --- a/lib/document.js +++ b/lib/record.js @@ -6,12 +6,12 @@ var _ = require('lodash'), RID = require('oriento').RID, utils = require('./utils'), hop = utils.object.hop, - log = require('debug-logger')('waterline-orientdb:document'); + log = require('debug-logger')('waterline-orientdb:record'); /** - * Document + * Record * - * Represents a single document in a collection. Responsible for serializing values before + * Represents a single record in a collection. Responsible for serializing values before * writing to a collection. * * @param {Object} values @@ -19,9 +19,9 @@ var _ = require('lodash'), * @api private */ -var Document = module.exports = function Document(values, schema, connection, operation) { +var Record = module.exports = function Record(values, schema, connection, operation) { - // Keep track of the current document's values + // Keep track of the current record's values this.values = {}; // Grab the schema for normalizing values @@ -37,7 +37,7 @@ var Document = module.exports = function Document(values, schema, connection, op if(values){ var newValues = this.setValues(values); this.values = newValues.values; - } + } return this; }; @@ -59,7 +59,7 @@ var Document = module.exports = function Document(values, schema, connection, op * @api private */ -Document.prototype.setValues = function setValues(values) { +Record.prototype.setValues = function setValues(values) { var results = this.serializeValues(values); this.normalizeId(results.values); @@ -75,7 +75,7 @@ Document.prototype.setValues = function setValues(values) { * @param {Object} values * @api private */ -Document.prototype.normalizeId = function normalizeId(values) { +Record.prototype.normalizeId = function normalizeId(values) { if(!values.hasOwnProperty('id') && !values.hasOwnProperty('@rid')) return; @@ -102,7 +102,7 @@ Document.prototype.normalizeId = function normalizeId(values) { * @return {Object} * @api private */ -Document.prototype.serializeValues = function serializeValues(values) { +Record.prototype.serializeValues = function serializeValues(values) { var self = this; var returnResult = {}; diff --git a/lib/utils.js b/lib/utils.js index caea2c8..e7d49e7 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -330,6 +330,64 @@ exports.getAttributeAsObject = function getAttributeAsObject(schema, columnName) return _.find(_.values(schema), { columnName: columnName }); }; +/** + * Extend + * + * Extends a class in a simple manner. + * + * @param {Object} parent + * @param {Object} [source] + * @return {Function} + */ +exports.extend = function extend(parent, source){ + source = source || {}; + var child; + + if (_.isFunction(source)) { + child = function () { return source.apply(this, arguments); }; + } + else if (source.hasOwnProperty('constructor')) { + child = source.constructor; + } + else { + child = function () { return parent.apply(this, arguments); }; + } + + // Inherit parent's prototype + child.prototype = _.create(parent.prototype, { 'constructor': child }); + + var keys, key, i, limit; + + // Inherit parent's properties + for (keys = Object.keys(parent), key = null, i = 0, limit = keys.length; i < limit; i++) { + key = keys[i]; + if (key !== 'prototype') { + child[key] = parent[key]; + } + } + + // Overwrite with source's properties + for (keys = Object.keys(source), key = null, i = 0, limit = keys.length; i < limit; i++) { + key = keys[i]; + if (key !== 'constructor' && key !== 'prototype' && source.hasOwnProperty(key)) { + child[key] = source[key]; + } + } + + // Overwrite with source's prototype properties + if(source.prototype){ + for (keys = Object.keys(source.prototype), key = null, i = 0, limit = keys.length; i < limit; i++) { + key = keys[i]; + if (key !== 'constructor') { + child.prototype[key] = source.prototype[key]; + } + } + } + + child.prototype.$super = parent; + + return child; +}; /* istanbul ignore next: debug code */ /** diff --git a/package.json b/package.json index 693f5a2..690ec55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "waterline-orientdb", - "version": "0.10.33", + "version": "0.10.40", "description": "OrientDB adapter for Waterline / Sails.js ORM", "main": "./lib/adapter.js", "scripts": { @@ -38,20 +38,19 @@ "readmeFilename": "README.md", "dependencies": { "async": "~0.9.0", - "debug": "~2.1.0", - "debug-logger": "~0.2.0", - "lodash": "~3.2.0", + "debug-logger": "^0.3.1", + "lodash": "^3.3.0", "oriento": "~1.1.0", - "q": "^1.0.1", + "waterline-criteria": "~0.11.1", "waterline-cursor": "~0.0.5", - "waterline-sequel-orientdb": "~0.0.24" + "waterline-sequel-orientdb": "~0.0.26" }, "devDependencies": { "codeclimate-test-reporter": "~0.0.4", "istanbul": "~0.3.5", "jshint": "~2.6.0", "mocha": "*", - "waterline": "~0.10.12", + "waterline": "~0.10.18", "waterline-adapter-tests": "~0.10.8" }, "waterlineAdapter": { @@ -59,8 +58,9 @@ "interfaces": [ "semantic", "queryable", - "associations" + "associations", + "migratable" ], - "waterlineVersion": "~0.10.12" + "waterlineVersion": "~0.10.18" } } diff --git a/test/integration-orientdb/bootstrap.js b/test/integration-orientdb/bootstrap.js index 68b0e0b..7899f6a 100644 --- a/test/integration-orientdb/bootstrap.js +++ b/test/integration-orientdb/bootstrap.js @@ -29,13 +29,18 @@ var fixtures = { //VenueFixture: require(fixturesPath + 'hasManyThrough.venue.fixture'), VenueFixture: require('./fixtures/hasManyThrough.venueHack.fixture'), TaxiFixture: require(fixturesPath + 'manyToMany.taxi.fixture'), - DriverFixture: require(fixturesPath + 'manyToMany.driver.fixture'), + //DriverFixture: require(fixturesPath + 'manyToMany.driver.fixture'), + DriverFixture: require('./fixtures/manyToMany.driverHack.fixture.js'), UserOneFixture: require(fixturesPath + 'oneToOne.fixture').user_resource, ProfileOneFixture: require(fixturesPath + 'oneToOne.fixture').profile, FriendFixture: require('./fixtures/hasManyThrough.friend.fixture'), FollowsFixture: require('./fixtures/hasManyThrough.follows.fixture'), - OwnsFixture: require('./fixtures/hasManyThrough.owns.fixture') + OwnsFixture: require('./fixtures/hasManyThrough.owns.fixture'), + + IndexesFixture: require('./fixtures/define.indexes.fixture'), + PropertiesFixture: require('./fixtures/define.properties.fixture'), + SchemalessPropertiesFixture: require('./fixtures/define.schemalessProperties.fixture'), }; @@ -46,6 +51,7 @@ var fixtures = { var waterline, ontology; before(function(done) { + this.timeout(60000); // to prevent travis from breaking the build //globals global.Associations = {}; @@ -64,7 +70,10 @@ before(function(done) { var connections = { associations: _.clone(Connections.test) }; waterline.initialize({ adapters: { wl_tests: Adapter }, connections: connections }, function(err, _ontology) { - if(err) return done(err); + if(err) { + console.log('ERROR:', err); + done(err); + } ontology = _ontology; @@ -86,7 +95,7 @@ after(function(done) { // ontology.collections[item].drop(function(err) { // if(err) return next(err); next(); - // }); +// }); } async.each(Object.keys(ontology.collections), dropCollection, function(err) { diff --git a/test/integration-orientdb/bugs/43-orientdb_requestError/43-orientdb_requestError.js b/test/integration-orientdb/bugs/43-orientdb_requestError/43-orientdb_requestError.js new file mode 100644 index 0000000..092bade --- /dev/null +++ b/test/integration-orientdb/bugs/43-orientdb_requestError/43-orientdb_requestError.js @@ -0,0 +1,174 @@ +var assert = require('assert'), + _ = require('lodash'), + utils = require('../../../../lib/utils'); + +var self = this; + +describe('Bug #43: OrientDB.RequestError on update', function() { + before(function (done) { + var fixtures = { + ImageFixture: require('./image.fixture'), + SubprofileFixture: require('./post.fixture'), + UserFixture: require('./user.fixture') + }; + CREATE_TEST_WATERLINE(self, 'test_bug_43', fixtures, done); + }); + after(function (done) { + DELETE_TEST_WATERLINE('test_bug_43', done); + }); + + describe('rodrigorn: update a created post', function() { + ///////////////////////////////////////////////////// + // TEST SETUP + //////////////////////////////////////////////////// + + var postRecord, imageParent; + + before(function(done) { + self.collections.Post.create({ title: 'a post' }, function(err, post) { + if(err) { return done(err); } + postRecord = post; + + self.collections.Image.create({ name: 'parent', crops: [ { name: 'crop' } ] }, function(err, img) { + if(err) { return done(err); } + imageParent = img; + + self.collections.Post.findOne(postRecord.id, function(err, thePost){ + assert(!err); + assert(thePost); + + thePost.image = img.id; + + self.collections.Post.update(postRecord.id, thePost, function(err, postUpdated){ + if(err) { return done(err); } + done(); + }); + }); + }); + }); + }); + + + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + + it('should update a post', function(done) { + self.collections.Post.findOne(postRecord.id) + .then(function(post){ + assert(post); + + post.title = 'new title'; + + self.collections.Post.update(post.id, post, function(err, post2){ + assert(!err, err); + assert.equal(post.title, 'new title'); + done(); + }); + }) + .error(done); + }); + + it('control test: should have a crop associated', function(done) { + self.collections.Image.findOne(imageParent.id) + .populate('crops') + .exec(function(err, imgParent) { + if(err) { return done(err); } + assert.equal(imgParent.crops[0].name, 'crop'); + done(); + }); + }); + + it('control test: should have a crop associated', function(done) { + self.collections.Image.findOne(imageParent.id) + .exec(function(err, imgParent) { + if(err) { return done(err); } + + imgParent.isCrop = false; + self.collections.Image.update(imageParent.id, imgParent, function(err, res){ + if(err) { return done(err); } + assert.equal(imgParent.isCrop, false); + done(); + }); + }); + }); + + it('control test: should have a crop associated', function(done) { + self.collections.Image.findOne(imageParent.id) + .exec(function(err, imgParent) { + if(err) { return done(err); } + + imgParent.isCrop = false; + self.collections.Image.update(imageParent.id, imgParent, function(err, res){ + if(err) { return done(err); } + assert.equal(imgParent.isCrop, false); + done(); + }); + }); + }); + + }); + + + describe('stackoverflow issue: update a created user', function() { + ///////////////////////////////////////////////////// + // TEST SETUP + //////////////////////////////////////////////////// + + var userParent, userChild; + + before(function(done) { + self.collections.Dbuser.create({ username: 'parent' }, function(err, user) { + if(err) { return done(err); } + userParent = user; + + self.collections.Dbuser.create({ username: 'child' }, function(err, user2) { + if(err) { return done(err); } + userChild = user2; + + self.collections.Dbuser.findOne(userParent.id, function(err, dbUser){ + if(err) { return done(err); } + dbUser.follows.add(user2.id); + dbUser.save(done); + }) + + }); + }); + }); + + + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + + it('control test: should have created child user', function(done) { + self.collections.Dbuser.findOne({ username: 'child' }) + .populate('followed') + .exec(function(err, user) { + assert(!err, err); + assert.equal(user.username, 'child'); + assert.equal(user.followed[0].username, 'parent'); + done(); + }); + }); + + it('should update user', function(done) { + userParent.token = 'iasbdasgdpsabçefbe'; + self.collections.Dbuser.update(userParent.id, userParent, function(err, user) { + if(err) { return done(err); } + assert(user); + done(); + }); + }); + + xit('should create 2 users who reference each other', function(done) { + self.collections.Dbuser.update({ username: 'user1', follows: [ { username: 'user2' } ] }, function(err, user) { + if(err) { return done(err); } + assert(user); + done(); + }); + }); + + }); + +}); diff --git a/test/integration-orientdb/bugs/43-orientdb_requestError/image.fixture.js b/test/integration-orientdb/bugs/43-orientdb_requestError/image.fixture.js new file mode 100644 index 0000000..fb6b6f4 --- /dev/null +++ b/test/integration-orientdb/bugs/43-orientdb_requestError/image.fixture.js @@ -0,0 +1,33 @@ +module.exports = { + + identity: 'image', + + attributes: { + name: { + type: 'string' + }, + file: { + type: 'json', + isFile: true + }, + footer: { + type: 'string' + }, + // author: { + // model: 'author' + // }, + area: { + type: 'string' + }, + isCrop: { + type: 'boolean' + }, + parent: { + model: 'image' + }, + crops: { + collection: 'image', + via: 'parent' + } + } +}; \ No newline at end of file diff --git a/test/integration-orientdb/bugs/43-orientdb_requestError/post.fixture.js b/test/integration-orientdb/bugs/43-orientdb_requestError/post.fixture.js new file mode 100644 index 0000000..9eed020 --- /dev/null +++ b/test/integration-orientdb/bugs/43-orientdb_requestError/post.fixture.js @@ -0,0 +1,65 @@ + +module.exports = { + + identity: 'post', + + attributes: { + title: { + type: 'string' + }, + slug: { + type: 'string' + }, + editorialPriority: { + type: 'string' + }, + sectionPriority: { + type: 'string' + }, + html: { + type: 'string' + }, + editor_html: { + type: 'string' + }, + featureImage: { + model: 'image' + }, + area: { + type: 'string' + }, + excerpt: { + type: 'string' + }, + content: { + type: 'string', + }, + publicationDate: { + type: 'datetime' + }, + // categories:{ + // collection: 'category', + // through: 'post_category', + // via: 'post', + // dominant: true + // }, + // author:{ + // model:'author' + // }, + // status:{ + // model:'postStatus' + // }, + address: { + type: 'string' + }, + addressReference: { + type: 'string' + }, + latitude: { + type: 'float' + }, + longitude: { + type: 'float' + } + } +}; \ No newline at end of file diff --git a/test/integration-orientdb/bugs/43-orientdb_requestError/user.fixture.js b/test/integration-orientdb/bugs/43-orientdb_requestError/user.fixture.js new file mode 100644 index 0000000..bfb23a7 --- /dev/null +++ b/test/integration-orientdb/bugs/43-orientdb_requestError/user.fixture.js @@ -0,0 +1,33 @@ +module.exports = { + tableName : 'User', + identity : 'dbuser', + schema : true, + attributes : { + id : { + type : 'string', + primaryKey : true, + columnName : '@rid' + }, + username : { + type : 'string', + // required : true, + unique : true + }, + password : { + type : 'string', + // required : false + }, + token : { + type : 'string' + }, + follows : { + collection : 'dbuser', + via : 'followed', + dominant : true + }, + followed : { + collection : 'dbuser', + via : 'follows' + } + } +}; diff --git a/test/integration-orientdb/bugs/47-schema_with_id.js b/test/integration-orientdb/bugs/47-schema_with_id.js index a6ee6f1..4e0d139 100644 --- a/test/integration-orientdb/bugs/47-schema_with_id.js +++ b/test/integration-orientdb/bugs/47-schema_with_id.js @@ -44,10 +44,11 @@ describe('Bug #47: Schema with id (blueprints like)', function() { }); describe('create user', function() { - + ///////////////////////////////////////////////////// - // TEST METHODS + // TEST SETUP //////////////////////////////////////////////////// + var userRecord, passportRecord, passportNullIdRecord; before(function (done) { @@ -70,6 +71,10 @@ describe('Bug #47: Schema with id (blueprints like)', function() { }); + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + it('should be robust against an insertion with id set', function(done) { // we probably should throw an error... self.collections.User.create({ email: 'email@example.com', id: '#13:1' }, function(err, user) { diff --git a/test/integration-orientdb/fixtures/define.indexes.fixture.js b/test/integration-orientdb/fixtures/define.indexes.fixture.js new file mode 100644 index 0000000..5240b35 --- /dev/null +++ b/test/integration-orientdb/fixtures/define.indexes.fixture.js @@ -0,0 +1,34 @@ +/** + * Dependencies + */ + +var Waterline = require('waterline'); + +module.exports = Waterline.Collection.extend({ + + tableName : 'indexesTable', + identity : 'indexes', + connection : 'associations', + + attributes : { + name : 'string', + indexUnique : { + type : 'string', + unique : true + }, + indexNotUnique : { + columnName : 'indexDuplicates', + type : 'string', + index : true + }, + + props: { + model: 'properties' + }, + + schemalessProps: { + model: 'schemaless_properties' + } + } + +}); diff --git a/test/integration-orientdb/fixtures/define.properties.fixture.js b/test/integration-orientdb/fixtures/define.properties.fixture.js new file mode 100644 index 0000000..f96ba42 --- /dev/null +++ b/test/integration-orientdb/fixtures/define.properties.fixture.js @@ -0,0 +1,34 @@ +/** + * Dependencies + */ + +var Waterline = require('waterline'); + +module.exports = Waterline.Collection.extend({ + + tableName : 'propertiesTable', + identity : 'properties', + connection : 'associations', + + attributes : { + stringProp : { + type : 'string' + }, + textProp : 'text', + jsonProp : 'json', + floatProp : 'float', + emailProp : 'email', + propRequired : { + type : 'string', + required : true + }, + modelProp : { + model : 'indexes' + }, + collectionProp : { + collection : 'indexes', + via : 'props' + } + } + +}); diff --git a/test/integration-orientdb/fixtures/define.schemalessProperties.fixture.js b/test/integration-orientdb/fixtures/define.schemalessProperties.fixture.js new file mode 100644 index 0000000..00ebeac --- /dev/null +++ b/test/integration-orientdb/fixtures/define.schemalessProperties.fixture.js @@ -0,0 +1,30 @@ +/** + * Dependencies + */ + +var Waterline = require('waterline'); + +module.exports = Waterline.Collection.extend({ + + tableName : 'schemalessPropertiesTable', + identity : 'schemaless_properties', + connection : 'associations', + + schema: false, + + attributes : { + schemaProp : 'string', + customColumnProp : { + type: 'string', + columnName: 'customCol' + }, + modelProp : { + model : 'indexes' + }, + collectionProp : { + collection : 'indexes', + via : 'props' + } + } + +}); diff --git a/test/integration-orientdb/fixtures/manyToMany.driverHack.fixture.js b/test/integration-orientdb/fixtures/manyToMany.driverHack.fixture.js new file mode 100644 index 0000000..a48ef7d --- /dev/null +++ b/test/integration-orientdb/fixtures/manyToMany.driverHack.fixture.js @@ -0,0 +1,32 @@ +/** + * Dependencies + */ + +var Waterline = require('waterline'); + +module.exports = Waterline.Collection.extend({ + + tableName: 'driverTable', + identity: 'driver', + connection: 'associations', + joinTableNames: { + taxis: 'drives' + }, + + // migrate: 'drop', + attributes: { + name: 'string', + taxis: { + collection: 'taxi', + via: 'drivers', + //joinTableName: 'drives', + dominant: true + }, + + toJSON: function() { + var obj = this.toObject(); + delete obj.name; + return obj; + } + } +}); diff --git a/test/integration-orientdb/bugs/index.js b/test/integration-orientdb/index.js similarity index 73% rename from test/integration-orientdb/bugs/index.js rename to test/integration-orientdb/index.js index f1bc7bd..3bc26d2 100644 --- a/test/integration-orientdb/bugs/index.js +++ b/test/integration-orientdb/index.js @@ -4,9 +4,9 @@ var Waterline = require('waterline'); var _ = require('lodash'); var async = require('async'); -var Adapter = require('../../../'); +var Adapter = require('../../'); -var config = require('../../test-connection.json'); +var config = require('../test-connection.json'); config.database = 'waterline-test-orientdb'; config.options = config.options || {}; config.options.storage = "memory"; @@ -17,21 +17,28 @@ var instancesMap = {}; // TEST SETUP //////////////////////////////////////////////////// -global.CREATE_TEST_WATERLINE = function(context, dbName, fixtures, cb){ +global.CREATE_TEST_WATERLINE = function(context, testConfig, fixtures, cb){ cb = cb || _.noop; var waterline, ontology; var localConfig = _.cloneDeep(config); - localConfig.database = dbName; + if(testConfig){ + if(_.isString(testConfig)){ + localConfig.database = testConfig; + } else if(_.isObject(testConfig)){ + _.merge(localConfig, testConfig); + } + } + + waterline = new Waterline(); // context variable context.collections = {}; - - waterline = new Waterline(); + context.waterline = waterline; Object.keys(fixtures).forEach(function(key) { - fixtures[key].connection = dbName; + fixtures[key].connection = localConfig.database; waterline.loadCollection(Waterline.Collection.extend(fixtures[key])); }); @@ -41,7 +48,7 @@ global.CREATE_TEST_WATERLINE = function(context, dbName, fixtures, cb){ Connections.test.adapter = 'wl_tests'; var connections = {}; - connections[dbName] = _.clone(Connections.test); + connections[localConfig.database] = _.clone(Connections.test); waterline.initialize({ adapters: { wl_tests: Adapter }, connections: connections }, function(err, _ontology) { if(err) return cb(err); @@ -53,20 +60,22 @@ global.CREATE_TEST_WATERLINE = function(context, dbName, fixtures, cb){ context.collections[globalName] = _ontology.collections[key]; }); - instancesMap[dbName] = { + instancesMap[localConfig.database] = { waterline: waterline, ontology: ontology, config: localConfig }; - cb(); + cb(null, context.collections); }); }; -global.DELETE_TEST_WATERLINE = function(dbName, cb){ +global.DELETE_TEST_WATERLINE = function(testConfig, cb){ cb = cb || _.noop; + var dbName = _.isString(testConfig) ? testConfig : testConfig.database; + if(!instancesMap[dbName]) { return cb(new Error('Waterline instance not found for ' + dbName + '! Did you use the correct db name?')); }; var ontology = instancesMap[dbName].ontology; @@ -83,7 +92,10 @@ global.DELETE_TEST_WATERLINE = function(dbName, cb){ } async.each(Object.keys(ontology.collections), dropCollection, function(err) { - if(err) return cb(err); + if(err) { + console.log('ERROR dropping collections:', err); + return cb(err); + }; ontology.collections[Object.keys(ontology.collections)[0]].getServer(function(server){ server.drop({ diff --git a/test/integration-orientdb/tests/adapterCustomMethods/decodeURIComponent.test.js b/test/integration-orientdb/tests/adapterCustomMethods/decodeURIComponent.test.js new file mode 100644 index 0000000..dd8eea3 --- /dev/null +++ b/test/integration-orientdb/tests/adapterCustomMethods/decodeURIComponent.test.js @@ -0,0 +1,112 @@ +var assert = require('assert'), + _ = require('lodash'); + +var self = this; + +describe('decodeURIComponent: decode the id', function() { + + var fixtures = { + UserFixture : { + identity : 'user', + + attributes : { + username : 'string', + email : 'email', + } + }, + BlueprintsUserFixture : { + identity : 'blue_user', + + attributes : { + id : { + type : 'string', + primaryKey : true, + columnName : '@rid' + }, + username : 'string', + email : 'email', + } + } + }; + + var config = { + database: 'test_decodeURIComponent', + options: { + decodeURIComponent: true + } + } + + before(function (done) { + CREATE_TEST_WATERLINE(self, config, fixtures, done); + }); + after(function (done) { + DELETE_TEST_WATERLINE(config, done); + }); + + describe('find user', function() { + + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + var userRecord, encodedUserId, encodedUser2Id, blueUserRecord, encodedBlueUserId; + + before(function (done) { + self.collections.User.create([{ email: 'user1@example.com' }, { email: 'user2@example.com' }], function(err, users) { + if(err) { return done(err); } + userRecord = users[0]; + encodedUserId = encodeURIComponent(userRecord.id); + encodedUser2Id = encodeURIComponent(users[1].id); + self.collections.Blue_user.create({ email: 'blue@example.com' }, function(err, blueUser) { + if(err) { return done(err); } + blueUserRecord = blueUser; + encodedBlueUserId = encodeURIComponent(blueUserRecord.id); + done(); + }); + }); + }); + + + it('regression test: should retrieve user by id', function(done) { + self.collections.User.findOne(userRecord.id, function(err, user) { + if(err) { return done(err); } + assert.equal(user.email, 'user1@example.com'); + done(); + }); + }); + + it('should retrieve user with encoded id', function(done) { + self.collections.User.findOne(encodedUserId, function(err, user) { + if(err) { return done(err); } + assert.equal(user.email, 'user1@example.com'); + done(); + }); + }); + + it('should retrieve 2 users with encoded id', function(done) { + self.collections.User.find([encodedUserId, encodedUser2Id], function(err, users) { + if(err) { return done(err); } + assert.equal(users[0].email, 'user1@example.com'); + assert.equal(users[1].email, 'user2@example.com'); + done(); + }); + }); + + it('regression test: should retrieve blueprints user by id', function(done) { + self.collections.Blue_user.findOne(blueUserRecord.id, function(err, user) { + if(err) { return done(err); } + assert.equal(user.email, 'blue@example.com'); + done(); + }); + }); + + it('should retrieve blueprints user with encoded id', function(done) { + self.collections.Blue_user.findOne(encodedBlueUserId, function(err, user) { + if(err) { return done(err); } + assert.equal(user.email, 'blue@example.com'); + done(); + }); + }); + + + }); +}); diff --git a/test/integration-orientdb/tests/adapterCustomMethods/getDB_Server.js b/test/integration-orientdb/tests/adapterCustomMethods/getDB_Server.js index cf90241..aaf9cac 100644 --- a/test/integration-orientdb/tests/adapterCustomMethods/getDB_Server.js +++ b/test/integration-orientdb/tests/adapterCustomMethods/getDB_Server.js @@ -66,4 +66,21 @@ describe('Adapter Custom Methods', function() { }); }); }); + + + describe('native', function() { + describe('get native oriento collection', function() { + + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + + it('should return the collection\'s class name', function(done) { + Associations.Friend.native(function(collection){ + assert(collection.name, 'friendTable'); + done(); + }); + }); + }); + }); }); \ No newline at end of file diff --git a/test/integration-orientdb/tests/associations/manyThrough.find.js b/test/integration-orientdb/tests/associations/manyThrough.find.js index 379d95b..414915b 100644 --- a/test/integration-orientdb/tests/associations/manyThrough.find.js +++ b/test/integration-orientdb/tests/associations/manyThrough.find.js @@ -87,7 +87,7 @@ describe('Association Interface', function() { .populate('teams') .populate('sponsor') .then(function(stadium){ - assert(typeof stadium.id === 'string'); + assert.equal(typeof stadium.id, 'string'); assert(stadium.teams.length === 1); assert(stadium.owners.length === 0); assert(!stadium.out_venueTable); diff --git a/test/integration-orientdb/tests/associations/manyToMany.joinTableName.find.js b/test/integration-orientdb/tests/associations/manyToMany.joinTableName.find.js new file mode 100644 index 0000000..c7c7b3e --- /dev/null +++ b/test/integration-orientdb/tests/associations/manyToMany.joinTableName.find.js @@ -0,0 +1,99 @@ +var assert = require('assert'), + _ = require('lodash'); + +describe('Association Interface', function() { + + describe('n:m association :: tableName attribute', function() { + + ///////////////////////////////////////////////////// + // TEST SETUP + //////////////////////////////////////////////////// + + var driverRecord; + + before(function(done) { + Associations.Driver.create({ name: 'manymany find'}, function(err, driver) { + if(err) return done(err); + + driverRecord = driver; + + var taxis = []; + for(var i=0; i<2; i++) { + driverRecord.taxis.add({ medallion: i }); + } + + driverRecord.save(function(err) { + if(err) return done(err); + done(); + }); + }); + }); + + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + + it('should return "drives" as join table name', function(done) { + Associations.Driver_taxis__taxi_drivers.native(function(collection){ + assert.equal(collection.name, 'drives'); + done(); + }); + }); + + it('should return "E" (edge) as join table\'s super class', function(done) { + Associations.Driver_taxis__taxi_drivers.native(function(collection){ + assert.equal(collection.superClass, 'E'); + done(); + }); + }); + + it('should return taxis when the populate criteria is added', function(done) { + Associations.Driver.find({ name: 'manymany find' }) + .populate('taxis') + .exec(function(err, drivers) { + assert(!err); + + assert(Array.isArray(drivers)); + assert(drivers.length === 1); + assert(Array.isArray(drivers[0].taxis)); + assert(drivers[0].taxis.length === 2); + + done(); + }); + }); + + it('should not return a taxis object when the populate is not added', function(done) { + Associations.Driver.find() + .exec(function(err, drivers) { + assert(!err); + + var obj = drivers[0].toJSON(); + assert(!obj.taxis); + + done(); + }); + }); + + it('should call toJSON on all associated records if available', function(done) { + Associations.Driver.find({ name: 'manymany find' }) + .populate('taxis') + .exec(function(err, drivers) { + assert(!err); + + var obj = drivers[0].toJSON(); + assert(!obj.name); + + assert(Array.isArray(obj.taxis)); + assert(obj.taxis.length === 2); + + assert(obj.taxis[0].hasOwnProperty('createdAt')); + assert(!obj.taxis[0].hasOwnProperty('medallion')); + assert(obj.taxis[1].hasOwnProperty('createdAt')); + assert(!obj.taxis[1].hasOwnProperty('medallion')); + + done(); + }); + }); + + }); +}); diff --git a/test/integration-orientdb/tests/config/database.test.js b/test/integration-orientdb/tests/config/database.test.js new file mode 100644 index 0000000..1649096 --- /dev/null +++ b/test/integration-orientdb/tests/config/database.test.js @@ -0,0 +1,98 @@ +var assert = require('assert'), + _ = require('lodash'); + +var self = this, + fixtures, + config; + +describe('Config tests)', function() { + before(function (done) { + + fixtures = { + UserFixture : { + identity : 'user', + attributes : { + name : 'string' + } + }, + ThingFixture : { + identity : 'thing', + + attributes : { + name : 'string' + } + } + }; + + config = { + user : 'root', + password : 'root', + database : 'test_config_db' + }; + + CREATE_TEST_WATERLINE(self, config, fixtures, done); + }); + after(function (done) { + DELETE_TEST_WATERLINE(config, done); + }); + + describe('database', function() { + + describe('username', function() { + + ///////////////////////////////////////////////////// + // TEST SETUP + //////////////////////////////////////////////////// + + before(function (done) { + // db created, let's close the connection so we can test logins + self.waterline.teardown(done); + }); + + after(function (done) { + // let's log off last user because it may not have privileges to drop the db later on + self.waterline.teardown(function(err){ + if(err) { return done(err); } + // and now we logon with original config + CREATE_TEST_WATERLINE(self, config, fixtures, done); + }); + }); + + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + + it('should be the same as connection username', function(done) { + CREATE_TEST_WATERLINE(self, config, fixtures, function(err){ + if(err) { return done(err); } + self.collections.User.getDB(function(db){ + assert.equal(db.username, 'root'); + done(); + }); + }); + }); + + it('should be the same as databaseUser', function(done) { + self.waterline.teardown(function(err){ + if(err) { return done(err); } + + var newConfig = _.cloneDeep(config); + + newConfig.options = { + databaseUser : 'admin', + databasePassword : 'admin', + }; + + CREATE_TEST_WATERLINE(self, newConfig, fixtures, function(err){ + if(err) { return done(err); } + self.collections.User.getDB(function(db){ + assert.equal(db.username, 'admin'); + done(); + }); + }); + }); + }); + + }); + }); +}); diff --git a/test/integration-orientdb/tests/define/indexes.js b/test/integration-orientdb/tests/define/indexes.js new file mode 100644 index 0000000..de35ca3 --- /dev/null +++ b/test/integration-orientdb/tests/define/indexes.js @@ -0,0 +1,43 @@ +var assert = require('assert'), + _ = require('lodash'); + +describe('Define related Operations', function() { + + describe('Indexes', function() { + + ///////////////////////////////////////////////////// + // TEST SETUP + //////////////////////////////////////////////////// + + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + + it('should properly create unique index', function(done) { + Associations.Indexes.getDB(function(db) { + db.index.get('indexesTable.indexUnique') + .then(function(index) { + assert.equal(index.name, 'indexesTable.indexUnique'); + assert.equal(index.type, 'UNIQUE'); + + done(); + }) + .error(done); + }); + }); + + it('should properly create not unique index', function(done) { + Associations.Indexes.getDB(function(db) { + db.index.get('indexesTable.indexDuplicates') + .then(function(index) { + assert.equal(index.name, 'indexesTable.indexDuplicates'); + assert.equal(index.type, 'NOTUNIQUE'); + + done(); + }) + .error(done); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/integration-orientdb/tests/define/properties.js b/test/integration-orientdb/tests/define/properties.js new file mode 100644 index 0000000..e616c6d --- /dev/null +++ b/test/integration-orientdb/tests/define/properties.js @@ -0,0 +1,108 @@ +var assert = require('assert'), + _ = require('lodash'), + Oriento = require('oriento'); + +describe('Define related Operations', function() { + + describe('Property creation', function() { + + ///////////////////////////////////////////////////// + // TEST SETUP + //////////////////////////////////////////////////// + + var klass; + + before(function(done) { + Associations.Properties.native(function(nativeClass) { + klass = nativeClass; + done(); + }); + }); + + + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + + it('should properly create mandatory property', function(done) { + klass.property.get('propRequired') + .then(function(property) { + assert.equal(property.name, 'propRequired'); + assert.equal(property.mandatory, true); + done(); + }) + .error(done); + }); + + it('should properly create string property from string', function(done) { + klass.property.get('stringProp') + .then(function(property) { + assert.equal(property.name, 'stringProp'); + assert.equal(Oriento.types[property.type], 'String'); + done(); + }) + .error(done); + }); + + it('should properly create string property from text', function(done) { + klass.property.get('textProp') + .then(function(property) { + assert.equal(Oriento.types[property.type], 'String'); + done(); + }) + .error(done); + }); + + it('should properly create float property from float', function(done) { + klass.property.get('floatProp') + .then(function(property) { + assert.equal(Oriento.types[property.type], 'Float'); + done(); + }) + .error(done); + }); + + it('should properly create Embedded property from json', function(done) { + klass.property.get('jsonProp') + .then(function(property) { + assert.equal(Oriento.types[property.type], 'Embedded'); + done(); + }) + .error(done); + }); + + it('should properly create Link property from model', function(done) { + klass.property.get('modelProp') + .then(function(property) { + assert.equal(Oriento.types[property.type], 'Link'); + assert.equal(property.linkedClass, 'indexesTable'); + done(); + }) + .error(done); + }); + + it('should properly create String property from email', function(done) { + klass.property.get('emailProp') + .then(function(property) { + assert.equal(Oriento.types[property.type], 'String'); + done(); + }) + .error(done); + }); + + + + // Not sure this can happen seen it's only required a Links exists on the associated table + // it('should properly create LinkSet property from collection', function(done) { + // klass.property.get('collectionProp') + // .then(function(property) { + // assert.equal(Oriento.types[property.type], 'LinkSet'); + // assert.equal(property.linkedClass, 'indexesTable'); + // done(); + // }) + // .error(done); + // }); + + + }); +}); \ No newline at end of file diff --git a/test/integration-orientdb/tests/define/schemalessProperties.js b/test/integration-orientdb/tests/define/schemalessProperties.js new file mode 100644 index 0000000..987737d --- /dev/null +++ b/test/integration-orientdb/tests/define/schemalessProperties.js @@ -0,0 +1,113 @@ +var assert = require('assert'), + _ = require('lodash'), + Oriento = require('oriento'); + +describe('Define related Operations', function() { + + describe('Property creation schemaless', function() { + + ///////////////////////////////////////////////////// + // TEST SETUP + //////////////////////////////////////////////////// + + var klass; + + before(function(done) { + Associations.Schemaless_properties.native(function(nativeClass) { + klass = nativeClass; + + Associations.Schemaless_properties + .create({ + schemaProp: 'schemaProp', + customColumnProp: 'customColumnProp', + schemaless: 'schemaless' + }).exec(function(err, values){ + if(err) { return done(err); } + + Associations.Properties.create({ + stringProp: 'stringProp', + textProp: 'textProp', + propRequired: 'propRequired' + }).exec(function(err, props){ + if(err) { return done(err); } + + assert.equal(props.textProp, 'textProp'); + done(); + }); + }); + }); + }); + + + ///////////////////////////////////////////////////// + // TEST METHODS + //////////////////////////////////////////////////// + + it('should properly create string property from string', function(done) { + klass.property.get('schemaProp') + .then(function(property) { + assert.equal(property.name, 'schemaProp'); + assert.equal(Oriento.types[property.type], 'String'); + done(); + }) + .error(done); + }); + + it('should properly create property with custom column name', function(done) { + klass.property.get('customCol') + .then(function(property) { + assert.equal(property.name, 'customCol'); + assert.equal(Oriento.types[property.type], 'String'); + done(); + }) + .error(done); + }); + + it('should properly create Link property from model', function(done) { + klass.property.get('modelProp') + .then(function(property) { + assert.equal(Oriento.types[property.type], 'Link'); + assert.equal(property.linkedClass, 'indexesTable'); + done(); + }) + .error(done); + }); + + it('should return schemaless properties', function(done) { + Associations.Schemaless_properties.findOne({ schemaProp: 'schemaProp' }).exec(function(err, record){ + if(err) { return done(err); } + + assert.equal(record.schemaProp, 'schemaProp'); + assert.equal(record.customColumnProp, 'customColumnProp'); + assert.equal(record.schemaless, 'schemaless'); + done(); + }); + }); + + it('should not return schemaless properties with select query', function(done) { + Associations.Schemaless_properties.findOne({ select: ['customColumnProp'], where: { schemaProp: 'schemaProp' } }) + .exec(function(err, record){ + if(err) { return done(err); } + + assert.equal(record.schemaProp, undefined); + assert.equal(record.customColumnProp, 'customColumnProp'); + assert.equal(record.schemaless, undefined); + done(); + }); + }); + + it('schemaful regression test: should not return properties ommitted in projection', function(done) { + Associations.Properties.findOne({ select: ['stringProp'], where: { stringProp: 'stringProp' } }) + .exec(function(err, record){ + if(err) { return done(err); } + + assert.equal(record.stringProp, 'stringProp'); + assert.equal(record.textProp, undefined); + assert.equal(record.propRequired, undefined); + done(); + }); + }); + + + }); +}); \ No newline at end of file diff --git a/test/integration/runner.js b/test/integration/runner.js index 513d39b..6fef91f 100644 --- a/test/integration/runner.js +++ b/test/integration/runner.js @@ -19,10 +19,14 @@ var log = require('debug-logger')('waterline-orientdb:test'); var TestRunner = require('waterline-adapter-tests'); var Adapter = require('../../'); +var argvDatabaseType; +if(process.argv.length > 2){ + argvDatabaseType = process.argv[2]; +} var config = require('../test-connection.json'); config.database = 'waterline-test-integration'; // We need different DB's due to https://github.com/orientechnologies/orientdb/issues/3301 - +config.options.databaseType = argvDatabaseType || process.env.DATABASE_TYPE || config.options.databaseType || Adapter.defaults.options.databaseType; // Grab targeted interfaces from this adapter's `package.json` file: var package = {}; @@ -41,12 +45,10 @@ catch (e) { } - - - log.info('Testing `' + package.name + '`, a Sails/Waterline adapter.'); log.info('Running `waterline-adapter-tests` against ' + interfaces.length + ' interfaces...'); log.info('( ' + interfaces.join(', ') + ' )'); +log.info('With database type: ' + config.options.databaseType); console.log(); log.info('Latest draft of Waterline adapter interface spec:'); log.info('https://github.com/balderdashy/sails-docs/blob/master/contributing/adapter-specification.md'); @@ -107,3 +109,6 @@ new TestRunner({ // Full interface reference: // https://github.com/balderdashy/sails-docs/blob/master/contributing/adapter-specification.md }); + + + diff --git a/test/unit/associations.test.js b/test/unit/associations.test.js index 2da8b12..e7a4362 100644 --- a/test/unit/associations.test.js +++ b/test/unit/associations.test.js @@ -3,6 +3,7 @@ */ var assert = require('assert'), util = require('util'), + Collection = require('../../lib/collection'), Associations = require('../../lib/associations'), _ = require('lodash'); @@ -14,12 +15,36 @@ var collections = { comment_parent: require('./fixtures/commentParent.model'), comment_recipe: require('./fixtures/commentRecipe.model') }; - -var associations = new Associations({ - config: { options: {fetchPlanLevel: 1} }, - collections: collections, - collectionsByIdentity: collections - }); + +var waterlineSchema = _.cloneDeep(collections); +Object.keys(waterlineSchema).forEach(function(key){ + var collection = waterlineSchema[key]; + Object.keys(collection.attributes).forEach(function(id){ + var attribute = collection.attributes[id]; + if(attribute.through){ + attribute.collection = attribute.through; + } + }); +}); + +var connectionMock = { + config: { options: {fetchPlanLevel: 1} }, + waterlineSchema: waterlineSchema +}; + +var newCollections = {}; +Object.keys(collections).forEach(function(key){ + collections[key].definition = collections[key].attributes; + newCollections[key] = new Collection(collections[key], connectionMock, collections); +}); +newCollections.authored_comment = new Collection.Edge(collections.authored_comment, connectionMock, collections); +newCollections.comment_parent = new Collection.Edge(collections.comment_parent, connectionMock, collections); +newCollections.comment_recipe = new Collection.Edge(collections.comment_recipe, connectionMock, collections); +connectionMock.collections = newCollections; +connectionMock.collectionsByIdentity = newCollections; + + +var associations = new Associations(connectionMock); describe('associations class', function () { diff --git a/test/unit/collection.test.js b/test/unit/collection.test.js new file mode 100644 index 0000000..a59227a --- /dev/null +++ b/test/unit/collection.test.js @@ -0,0 +1,116 @@ +/** + * Test dependencies + */ +var assert = require('assert'), + Collection = require('../../lib/collection'), + _ = require('lodash'); + + +describe('collection class', function () { + + var defaultModel = { + identity: 'default', + attributes: { name: 'string' }, + definition: { name: 'string' } + }; + + var collections = {}; + collections.defaultModel = _.defaults({ }, defaultModel); + collections.documentModel1 = _.defaults({ orientdbClass: '' }, defaultModel); + collections.documentModel2 = _.defaults({ orientdbClass: 'document' }, defaultModel); + collections.vertexModel = _.defaults({ orientdbClass: 'V' }, defaultModel); + collections.edgeModel = _.defaults({ orientdbClass: 'E' }, defaultModel); + collections.junctionModelThrough = _.defaults({ junctionTable: true }, defaultModel); + collections.junctionModelThroughD = _.defaults({ orientdbClass: '', junctionTable: true }, defaultModel); + collections.junctionModelThroughV = _.defaults({ orientdbClass: 'V', junctionTable: true }, defaultModel); + collections.junctionModel = _.defaults({ + identity : 'driver_taxis__taxi_drivers', + tableName : 'driver_taxis__taxi_drivers', + junctionTable : true + }, defaultModel); + collections.junctionModelE = _.defaults({ + orientdbClass: 'E', + identity : 'driver_taxis__taxi_drivers', + tableName : 'driver_taxis__taxi_drivers', + junctionTable : true + }, defaultModel); + + junctionTable: true, + + before(function(done){ + done(); + }); + + describe('document database', function () { + + var documentConnectionMock = { config: { options: { databaseType: 'document' } } }; + + it('constructor: should instantiate a document regardless of orientdbClass value', function (done) { + _.values(collections).forEach(function(collection){ + var doc = new Collection(collection, documentConnectionMock, null); + assert(doc instanceof Collection.Document); + assert(!(doc instanceof Collection.Vertex)); + assert(!(doc instanceof Collection.Edge)); + }); + done(); + }); + }); + + describe('graph database', function () { + + var graphConnectionMock = { config: { options: { databaseType: 'graph' } } }; + + it('constructor: should instantiate a document if orientdbClass is "" or "document"', function (done) { + var doc = new Collection(collections.documentModel1, graphConnectionMock, null); + assert(doc instanceof Collection.Document); + assert(!(doc instanceof Collection.Vertex)); + assert(!(doc instanceof Collection.Edge)); + doc = new Collection(collections.documentModel2, graphConnectionMock, null); + assert(doc instanceof Collection.Document); + assert(!(doc instanceof Collection.Vertex)); + assert(!(doc instanceof Collection.Edge)); + doc = new Collection(collections.junctionModelThroughD, graphConnectionMock, null); + assert(doc instanceof Collection.Document); + assert(!(doc instanceof Collection.Vertex)); + assert(!(doc instanceof Collection.Edge)); + + done(); + }); + + it('constructor: should instantiate a vertex if orientdbClass is undefined or "V"', function (done) { + var vertex = new Collection(collections.defaultModel, graphConnectionMock, null); + assert(vertex instanceof Collection.Document); + assert(vertex instanceof Collection.Vertex); + vertex = new Collection(collections.vertexModel, graphConnectionMock, null); + assert(vertex instanceof Collection.Vertex); + vertex = new Collection(collections.junctionModelThroughV, graphConnectionMock, null); + assert(vertex instanceof Collection.Vertex); + + done(); + }); + + it('constructor: should instantiate an edge if orientdbClass is "E"', function (done) { + var edge = new Collection(collections.edgeModel, graphConnectionMock, null); + assert(edge instanceof Collection.Edge); + edge = new Collection(collections.junctionModelE, graphConnectionMock, null); + assert(edge instanceof Collection.Edge); + + done(); + }); + + it('constructor: should instantiate an edge if table is junction table for a many-to-many association', function (done) { + var edge = new Collection(collections.junctionModel, graphConnectionMock, null); + assert(edge instanceof Collection.Edge); + done(); + }); + + it('constructor: should instantiate an edge if table is junction table for a many-to-many through association', function (done) { + var edge = new Collection(collections.junctionModelThrough, graphConnectionMock, null); + assert(edge instanceof Collection.Edge); + done(); + }); + + }); + + +}); diff --git a/test/unit/document.test.js b/test/unit/record.test.js similarity index 82% rename from test/unit/document.test.js rename to test/unit/record.test.js index 13ed28e..8102eb0 100644 --- a/test/unit/document.test.js +++ b/test/unit/record.test.js @@ -2,18 +2,18 @@ * Test dependencies */ var assert = require('assert'), - Document = require('../../lib/document'), + Record = require('../../lib/record'), RID = require('oriento').RID, _ = require('lodash'), util = require('util'); -describe('document helper class', function () { +describe('record helper class', function () { - var doc; + var record; before(function(done){ - doc = new Document(); + record = new Record(); done(); }); @@ -22,7 +22,7 @@ describe('document helper class', function () { name: 'no id collection' }; var testCollection1 = _.clone(collection1); - doc.normalizeId(testCollection1); + record.normalizeId(testCollection1); assert(_.isEqual(testCollection1, collection1)); @@ -30,7 +30,7 @@ describe('document helper class', function () { name: 'id collection', id: '#1:0' }; - doc.normalizeId(testCollection2); + record.normalizeId(testCollection2); assert(_.isUndefined(testCollection2.id)); assert(testCollection2['@rid'] instanceof RID); assert.equal(testCollection2['@rid'].cluster, 1); @@ -41,7 +41,7 @@ describe('document helper class', function () { name: 'id collection', '@rid': new RID('#2:0') }; - doc.normalizeId(testCollection3); + record.normalizeId(testCollection3); assert(_.isUndefined(testCollection3.id)); assert(_.isEqual(testCollection3['@rid'], new RID('#2:0'))); @@ -50,7 +50,7 @@ describe('document helper class', function () { id: '#1:0', '@rid': new RID('#2:0') }; - doc.normalizeId(testCollection4); + record.normalizeId(testCollection4); assert(_.isUndefined(testCollection4.id)); assert(_.isEqual(testCollection4['@rid'], new RID('#1:0'))); diff --git a/test/unit/utils.extend.test.js b/test/unit/utils.extend.test.js new file mode 100644 index 0000000..ac5fcb0 --- /dev/null +++ b/test/unit/utils.extend.test.js @@ -0,0 +1,122 @@ +/** + * Test dependencies + */ +var assert = require('assert'), + utils = require('../../lib/utils'); + + +describe('utils helper class', function() { + + var Shape; + + before(function(done){ + Shape = function(val1, val2) { + this.x = val1; + this.y = val2; + }; + Shape.shapeProperty = 'shape'; + Shape.willBeOverriden = 'shape'; + Shape.prototype.protoShape = 'shape'; + Shape.prototype.protoOverride = 'shape'; + Shape.prototype.overrideMethod = function(){ + return 'shape'; + }; + + done(); + }); + + describe('extend:', function() { + + it('should extend classes without source', function(done) { + var Extended = utils.extend(Shape); + var circle = new Extended(1, 2); + assert(circle instanceof Extended); + assert(circle instanceof Shape); + assert.equal(circle.y, 2); + done(); + }); + + it('should extend classes from object with constructor', function(done) { + var Extended = utils.extend(Shape, { constructor: function(val){ this.z = val; } }); + var circle = new Extended(1); + assert(circle instanceof Extended); + assert(circle instanceof Shape); + assert.equal(circle.z, 1); + done(); + }); + + it('should extend classes from object', function(done) { + var Circle = { + willBeOverriden: 'circle', + prototype: { + foo: function(){ return 'foo'; }, + protoOverride: 'circle' + } + }; + Circle.willBeOverriden = 'circle'; + Circle.prototype.foo = function(){ return 'foo'; }; + Circle.prototype.protoOverride = 'circle'; + + var Extended = utils.extend(Shape, Circle); + assert.equal(Extended.shapeProperty, 'shape'); + assert.equal(Extended.willBeOverriden, 'circle'); + + var circle = new Extended(1, 2); + assert(circle instanceof Extended); + assert(circle instanceof Shape); + + assert.equal(circle.y, 2); + assert.equal(circle.foo(), 'foo'); + assert.equal(circle.protoOverride, 'circle'); + + done(); + }); + + it('should extend classes from anonymous function', function(done) { + var Extended = utils.extend(Shape, function () { + Shape.apply(this, arguments); + this.z = arguments[0]; + }); + assert.equal(Extended.shapeProperty, 'shape'); + + var circle = new Extended(1, 2); + assert(circle instanceof Extended); + assert(circle instanceof Shape); + assert.equal(circle.y, 2); + assert.equal(circle.z, 1); + done(); + }); + + + it('should extend classes from function', function(done) { + function Circle() { + Shape.apply(this, arguments); + this.z = arguments[0]; + } + Circle.willBeOverriden = 'circle'; + Circle.prototype.foo = function(){ return 'foo'; }; + Circle.prototype.protoOverride = 'circle'; + Circle.prototype.overrideMethod = function(){ + return this.$super.prototype.overrideMethod.call(this) + ' is Circle'; + }; + + var Extended = utils.extend(Shape, Circle); + assert.equal(Extended.shapeProperty, 'shape'); + assert.equal(Extended.willBeOverriden, 'circle'); + + var circle = new Extended(1, 2); + assert(circle instanceof Extended); + assert(circle instanceof Shape); + + assert.equal(circle.y, 2); + assert.equal(circle.z, 1); + assert.equal(circle.foo(), 'foo'); + assert.equal(circle.protoOverride, 'circle'); + assert.equal(circle.overrideMethod(), 'shape is Circle'); + + done(); + }); + + }); + +});