diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..3403c9a --- /dev/null +++ b/.jshintignore @@ -0,0 +1 @@ +lib/long.js \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..f89c880 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,79 @@ +{ + "maxerr" : 50, + + "bitwise" : false, + "camelcase" : true, + "curly" : true, + "eqeqeq" : false, + "forin" : true, + "immed" : true, + "indent" : 2, + "latedef" : false, + "newcap" : true, + "noarg" : true, + "noempty" : true, + "nonew" : true, + "plusplus" : false, + "quotmark" : false, + "undef" : true, + "unused" : false, // $scope variables are often created but not read in js files + "strict" : true, + "devel" : true, + "node" : true, + "maxlen" :120, + "nonbsp" :true, + + "trailing" : true, + "maxparams" : false, + "maxdepth" : false, + "maxstatements" : false, + "maxcomplexity" : false, + + "asi" : false, + "boss" : false, + "debug" : false, + "eqnull" : true, + "esnext" : false, + "moz" : false, + + "evil" : false, + "expr" : false, + "funcscope" : false, + "globalstrict" : true, + "iterator" : false, + "lastsemic" : false, + "laxbreak" : false, + "laxcomma" : false, + "loopfunc" : true, + "multistr" : false, + "proto" : false, + "scripturl" : false, + "smarttabs" : false, + "shadow" : false, + "sub" : false, + "supernew" : false, + "validthis" : true, + + "browser" : false, + "couch" : false, + "dojo" : false, + "jquery" : false, + "mootools" : false, + "nonstandard" : false, + "prototypejs" : false, + "rhino" : false, + "worker" : false, + "wsh" : false, + "yui" : false, + "predef" : ["-Promise"], + "globals" : { + "angular" : false, + "$" : false, + "sinon" : false, + "describe" : false, + "beforeEach" : false, + "inject" : false, + "it" : false, + "expect" : false + } +} \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..8e92e97 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +ci/environment +coverage + diff --git a/.travis.yml b/.travis.yml index 9d128e4..7b188f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,7 @@ language: node_js node_js: - "0.10" before_script: - - sh -c ./ci/initialize-ci.sh 1.7-rc2 + - ./ci/initialize-ci.sh $ORIENTDB_VERSION +env: + - ORIENTDB_VERSION=1.7.10 + - ORIENTDB_VERSION=2.0.2 diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 0bcd21c..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,22 +0,0 @@ -# 0.2.0 - -- add fetch plans in query builder via `fetch()`. -- add support for standalone orientdb, not just dserver. -- better RIDBag support. -- travis ci integration. -- connection pools. -- add `Query::column()` and `Query::transform()`. -- add support for getting / setting custom fields on classes and properties. -- add index support. -- remove in-band `@options` key in record operations. -- switch to mocha 1.8.x (yay promise support). - -# 0.1.0 - -- Graph mode by default. -- Add vertex / edge helpers -- Add db query builder - -# 0.0.1 - -- Total refactor of [node-orientdb](https://github.com/nitrog7/node-orientdb). \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a93b8a9..70120ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,13 +14,13 @@ Before running the tests, ensure you've configured your orientdb server to use t To run the tests: -``` +```sh npm test ``` To generate the code coverage report, run: -``` +```sh npm run coverage ``` @@ -35,16 +35,20 @@ And have a look at `coverage/lcov-report/index.html`. ### 1. [Fork](http://help.github.com/fork-a-repo/) the oriento repository on github and clone your fork to your development environment -
+
+```sh
 git clone git@github.com:YOUR-GITHUB-USERNAME/oriento.git
-
+``` + If you have trouble setting up GIT with GitHub in Linux, or are getting errors like "Permission Denied (publickey)", then you must [setup your GIT installation to work with GitHub](http://help.github.com/linux-set-up-git/) ### 2. Add the main oriento repository as an additional git remote called "upstream" Change to the directory where you cloned oriento normally, "oriento". Then enter the following command: -
+
+```sh
 git remote add upstream git://github.com/codemix/oriento.git
-
+``` + ### 3. Make sure there is an issue created for the thing you are working on. @@ -53,9 +57,11 @@ All new features and bug fixes should have an associated issue to provide a sing > For small changes or documentation issues, you don't need to create an issue, a pull request is enough in this case. ### 4. Fetch the latest code from the main oriento branch -
+
+```sh
 git fetch upstream
-
+``` + You should start at this point for every new contribution to make sure you are working on the latest code. ### 5. Create a new branch for your feature based on the current oriento master branch @@ -63,10 +69,11 @@ You should start at this point for every new contribution to make sure you are w > That's very important since you will not be able to submit more than one pull request from your account if you'll use master. Each separate bug fix or change should go in its own branch. Branch names should be descriptive and start with the number of the issue that your code relates to. If you aren't fixing any particular issue, just skip number. For example: -
+
+```sh
 git checkout upstream/master
 git checkout -b 999-name-of-your-branch-goes-here
-
+``` ### 6. Do your magic, write your code Make sure it works and run the tests :) @@ -76,26 +83,33 @@ Unit tests are always welcome. Tested and well covered code greatly simplifies t ### 7. Commit your changes add the files/changes you want to commit to the [staging area](http://gitref.org/basic/#add) with -
+
+```sh
 git add path/to/my/file.js
-
+``` + You can use the -p option to select the changes you want to have in your commit. Commit your changes with a descriptive commit message. Make sure to mention the ticket number with #XXX so github will automatically link your commit with the ticket: -
+
+```sh
 git commit -m "A brief description of this change which fixes #42 goes here"
-
+``` ### 8. Pull the latest oriento code from upstream into your branch -
+
+```sh
 git pull upstream master
-
+``` + This ensures you have the latest code in your branch before you open your pull request. If there are any merge conflicts, you should fix them now and commit the changes again. This ensures that it's easy for the oriento team to merge your changes with one click. ### 9. Having resolved any conflicts, push your code to github -
+
+```sh
 git push -u origin 999-name-of-your-branch-goes-here
-
+``` + The `-u` parameter ensures that your branch will now automatically push and pull from the github branch. That means if you type `git push` the next time it will know where to push to. ### 10. Open a [pull request](http://help.github.com/send-pull-requests/) against upstream. @@ -109,27 +123,27 @@ Someone will review your code, and you might be asked to make some changes, if s ### 12. Cleaning it up After your code was either accepted or declined you can delete branches you've worked with from your local repository and `origin`. -
+
+```sh
 git checkout master
 git branch -D 999-name-of-your-branch-goes-here
 git push origin --delete 999-name-of-your-branch-goes-here
-
+``` ### Command overview (for advanced contributors) -
+```sh
 git clone git@github.com:YOUR-GITHUB-USERNAME/oriento.git
 git remote add upstream git://github.com/codemix/oriento.git
-
-
+
 git fetch upstream
 git checkout upstream/master
 git checkout -b 999-name-of-your-branch-goes-here
 
-/* do your magic, update changelog if needed */
+# do your magic, update changelog if needed
 
 git add path/to/my/file.js
 git commit -m "A brief description of this change which fixes #42 goes here"
 git pull upstream master
 git push -u origin 999-name-of-your-branch-goes-here
-
\ No newline at end of file +``` diff --git a/README.md b/README.md index b32390b..46a17d8 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,29 @@ -# Oriento +# NOTE: Oriento is deprecated, development continues at https://github.com/orientechnologies/orientjs -A lightweight node.js driver for [orientdb](http://www.orientechnologies.com/orientdb/) using orient's binary protocol. -[![Build Status](https://travis-ci.org/codemix/oriento.svg?branch=master)](https://travis-ci.org/codemix/oriento) +# Oriento -> **status: alpha** -> This is work in progress, alpha quality software. -> Please [report any bugs](https://github.com/codemix/oriento/issues) you find so that we can improve the library for everyone. +Official [orientdb](http://www.orientechnologies.com/orientdb/) driver for node.js. Fast, lightweight, uses the binary protocol. +[![Build Status](https://travis-ci.org/codemix/oriento.svg?branch=master)](https://travis-ci.org/codemix/oriento) +[![Gitter chat](https://badges.gitter.im/codemix/oriento.png)](https://gitter.im/codemix/oriento) # Supported Versions -Oriento aims to work with version 1.7 of orientdb and later. While it may work with earlier versions, they are not currently supported, [pull requests are welcome!](./CONTRIBUTING.md) - +Oriento aims to work with version 1.7.1 of orientdb and later. While it may work with earlier versions, they are not currently supported, [pull requests are welcome!](./CONTRIBUTING.md) +> **IMPORTANT**: Oriento does not currently support OrientDB's Tree Based [RIDBag](https://github.com/orientechnologies/orientdb/wiki/RidBag) feature because it relies on making additional network requests. +> This means that by default, the result of e.g. `JSON.stringify(record)` for a record with up to 119 edges will be very different from a record with 120+ edges. +> This can lead to very nasty surprises which may not manifest themselves during development but could appear at any time in production. +> There is an [open issue](https://github.com/orientechnologies/orientdb/issues/2315) for this in OrientDB, until that gets fixed, it is **strongly recommended** that you set `RID_BAG_EMBEDDED_TO_SBTREEBONSAI_THRESHOLD` to a very large value, e.g. 2147483647. +> Please see the [relevant section in the OrientDB manual](http://www.orientechnologies.com/docs/2.0/orientdb.wiki/RidBag.html#configuration) for more information. # Installation Install via npm. -``` +```sh npm install oriento ``` @@ -28,12 +31,13 @@ npm install oriento To run the test suite, first invoke the following command within the repo, installing the development dependencies: -``` +```sh npm install ``` Then run the tests: -``` + +```sh npm test ``` @@ -52,7 +56,7 @@ npm test ### Configuring the client. ```js -var Oriento = require('oriento'); +var Oriento = require('oriento'); var server = Oriento({ host: 'localhost', @@ -61,23 +65,6 @@ var server = Oriento({ password: 'yourpassword' }); ``` -### Configuring the client to use a connection pool. - -By default oriento uses one socket per server, but it is also possible to use a connection pool. -You should carefully benchmark this against the default setting for your use case, -there are scenarios where a connection pool is actually slightly worse for performance than a single connection. - -```js -var server = Oriento({ - host: 'localhost', - port: 2424, - username: 'root', - password: 'yourpassword', - pool: { - max: 10 // 1 by default - } -}); -``` ### Listing the databases on the server @@ -120,6 +107,51 @@ var db = server.use({ console.log('Using database: ' + db.name); ``` +### Execute an Insert Query + +```js +db.query('insert into OUser (name, password, status) values (:name, :password, :status)', + { + params: { + name: 'Radu', + password: 'mypassword', + status: 'active' + } + } +).then(function (response){ + console.log(response); //an Array of records inserted +}); + +``` + + +### Execute a Select Query with Params + +```js +db.query('select from OUser where name=:name', { + params: { + name: 'Radu' + }, + limit: 1 +}).then(function (results){ + console.log(results); +}); + +``` + +### Raw Execution of a Query String with Params + +```js +db.exec('select from OUser where name=:name', { + params: { + name: 'Radu' + } +}).then(function (response){ + console.log(response.results); +}); + +``` + ### Query Builder: Insert Record ```js @@ -157,6 +189,15 @@ db.select().from('OUser').where({status: 'ACTIVE'}).all() }); ``` +### Query Builder: Text Search + +```js +db.select().from('OUser').containsText({name: 'er'}).all() +.then(function (users) { + console.log('found users', users); +}); +``` + ### Query Builder: Select Records with Fetch Plan ```js @@ -255,6 +296,22 @@ db ``` +### Query Builder: Put a map entry into a map + +```js +db +.update('#1:1') +.put('mapProperty', { + key: 'value', + foo: 'bar' +}) +.scalar() +.then(function (total) { + console.log('updated', total, 'records'); +}); +``` + + ### Loading a record by RID. ```js @@ -309,6 +366,18 @@ db.class.get('MyClass') }); ``` +### Updating an existing class + +```js +db.class.update({ + name: 'MyClass', + superClass: 'V' +}) +.then(function (MyClass) { + console.log('Updated class: ' + MyClass.name + ' that extends ' + MyClass.superClass); +}); +``` + ### Listing properties in a class ```js @@ -333,12 +402,21 @@ MyClass.property.create({ ### Deleting a property from a class ```js -MyClass.property.delete('myprop') +MyClass.property.drop('myprop') .then(function () { console.log('Property deleted.'); }); ``` +### Renaming a property on a class + +```js +MyClass.property.rename('myprop', 'mypropchanged'); +.then(function () { + console.log('Property renamed.'); +}); +``` + ### Creating a record for a class ```js @@ -360,10 +438,31 @@ MyClass.list() }); ``` +### Create a new index for a class property + +```js +db.index.create({ + name: 'MyClass.myProp', + type: 'unique' +}) +.then(function(index){ + console.log('Created index: ', index); +}); +``` + +### Get entry from class property index + +```js +db.index.get('MyClass.myProp') +.then(function (index) { + index.get('foo').then(console.log.bind(console)); +}); +``` + ### Creating a new, empty vertex ```js -db.vertex.create('V') +db.create('VERTEX', 'V').one() .then(function (vertex) { console.log('created vertex', vertex); }); @@ -372,11 +471,12 @@ db.vertex.create('V') ### Creating a new vertex with some properties ```js -db.vertex.create({ - '@class': 'V', +db.create('VERTEX', 'V') +.set({ key: 'value', foo: 'bar' }) +.one() .then(function (vertex) { console.log('created vertex', vertex); }); @@ -384,7 +484,9 @@ db.vertex.create({ ### Deleting a vertex ```js -db.vertex.delete('#12:12') +db.delete('VERTEX') +.where('@rid = #12:12') +.one() .then(function (count) { console.log('deleted ' + count + ' vertices'); }); @@ -393,7 +495,10 @@ db.vertex.delete('#12:12') ### Creating a simple edge between vertices ```js -db.edge.from('#12:12').to('#12:13').create('E') +db.create('EDGE', 'E') +.from('#12:12') +.to('#12:13') +.one() .then(function (edge) { console.log('created edge:', edge); }); @@ -403,11 +508,14 @@ db.edge.from('#12:12').to('#12:13').create('E') ### Creating an edge with properties ```js -db.edge.from('#12:12').to('#12:13').create({ - '@class': 'E', +db.create('EDGE', 'E') +.from('#12:12') +.to('#12:13') +.set({ key: 'value', foo: 'bar' }) +.one() .then(function (edge) { console.log('created edge:', edge); }); @@ -416,12 +524,38 @@ db.edge.from('#12:12').to('#12:13').create({ ### Deleting an edge between vertices ```js -db.edge.from('#12:12').to('#12:13').delete({ +db.delete('EDGE', 'E') +.from('#12:12') +.to('#12:13') +.scalar() .then(function (count) { console.log('deleted ' + count + ' edges'); }); ``` +### Creating a function +You can create a function by supplying a plain javascript function. Please note that the method stringifies the `function` passed so you can't use any varaibles outside the function closure. + +```js +db.createFn("nameOfFunction", function(arg1, arg2) { + return arg1 + arg2; +}) +.then(function (count) { + // Function created! +}); +``` + +You can also omit the name and it'll default to the `Function#name` + +```js +db.createFn(function nameOfFunction(arg1, arg2) { + return arg1 + arg2; +}) +.then(function (count) { + // Function created! +}); +``` + # CLI @@ -432,7 +566,7 @@ To be useful, oriento requires some arguments to authenticate against the server You can get a list of the supported arguments using `oriento --help`. -``` +```sh -d, --cwd The working directory to use. -h, --host The server hostname or IP address. -p, --port The server port. @@ -454,15 +588,21 @@ For an example of such a file, see [test/fixtures/oriento.opts](./test/fixtures/ ### Listing all the databases on the server. -`oriento db list` +```sh +oriento db list +``` ### Creating a new database -`oriento db create mydb graph plocal` +```sh +oriento db create mydb graph plocal +``` ### Destroying an existing database -`oriento db delete mydb` +```sh +oriento db drop mydb +``` ## Migrations @@ -498,11 +638,15 @@ manager.up(1) To list all the unapplied migrations: -`oriento migrate list` +```sh +oriento migrate list +``` ### Creating a new migration -`oriento migrate create my new migration` +```sh +oriento migrate create my new migration +``` creates a file called something like `m20140318_200948_my_new_migration` which you should edit to specify the migration up and down methods. @@ -511,23 +655,65 @@ creates a file called something like `m20140318_200948_my_new_migration` which y To apply all the migrations: -`oriento migrate up` +```sh +oriento migrate up +``` ### Migrating up by 1 To apply only the first migration: -`oriento migrate up 1` +```sh +oriento migrate up 1 +``` ### Migrating down fully To revert all migrations: -`oriento migrate down` +```sh +oriento migrate down +``` ### Migrating down by 1 -`oriento migrate down 1` +```sh +oriento migrate down 1 +``` + +## Events +You can also bind to the following events + +### `beginQuery` +Given the query + + db.select('name, status').from('OUser').where({"status": "active"}).limit(1).fetch({"role": 1}).one(); + +The following event will be triggered + + db.on("beginQuery", function(obj) { + // => { + // query: 'SELECT name, status FROM OUser WHERE status = :paramstatus0 LIMIT 1', + // mode: 'a', + // fetchPlan: 'role:1', + // limit: -1, + // params: { params: { paramstatus0: 'active' } } + // } + }); + + +### `endQuery` +After a query has been run, you'll get the the following event emitted + + db.on("endQuery", function(obj) { + // => { + // "err": errObj, + // "result": resultObj, + // "perf": { + // "query": timeInMs + // } + // } + }); # History @@ -536,8 +722,7 @@ In 2012, [Gabriel Petrovay](https://github.com/gabipetrovay) created the origina In early 2014, [Giraldo Rosales](https://github.com/nitrog7) made a [whole host of improvements](https://github.com/nitrog7/node-orientdb), including support for orientdb 1.7 and switched to a promise based API. -Later in 2014, codemix refactored the library to make it easier to extend and maintain, and introduced an API similar to [nano](https://github.com/dscape/nano). The result is so different from the original codebase that it warranted its own name and npm package. This also gave us the opportunity to switch to semantic versioning. - +Later in 2014, [codemix](http://codemix.com/) refactored the library to make it easier to extend and maintain, and introduced an API similar to [nano](https://github.com/dscape/nano). The result is so different from the original codebase that it warranted its own name and npm package. This also gave us the opportunity to switch to semantic versioning. # Notes for contributors diff --git a/ci/initialize-ci.sh b/ci/initialize-ci.sh index 2bc09df..9294b6a 100755 --- a/ci/initialize-ci.sh +++ b/ci/initialize-ci.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash PARENT_DIR=$(dirname $(cd "$(dirname "$0")"; pwd)) CI_DIR="$PARENT_DIR/ci/environment" @@ -22,7 +22,11 @@ if [ ! -d "$ODB_DIR" ]; then echo "--- Setting up OrientDB ---" chmod +x $ODB_LAUNCHER chmod -R +rw "${ODB_DIR}/config/" - cp $PARENT_DIR/ci/orientdb-server-config.xml "${ODB_DIR}/config/" + if [[ $ODB_VERSION == *"1.7"* ]]; then + cp $PARENT_DIR/ci/orientdb-server-config-1.7.xml "${ODB_DIR}/config/orientdb-server-config.xml" + else + cp $PARENT_DIR/ci/orientdb-server-config.xml "${ODB_DIR}/config/" + fi cp $PARENT_DIR/ci/orientdb-server-log.properties "${ODB_DIR}/config/" else echo "!!! Found OrientDB v${ODB_VERSION} in ${ODB_DIR} !!!" diff --git a/ci/odb-shared.sh b/ci/odb-shared.sh index 8cfed09..8fc3614 100755 --- a/ci/odb-shared.sh +++ b/ci/odb-shared.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash odb_compare_version () { # TODO: this function does not handle well versions with additional rank @@ -36,32 +36,33 @@ odb_download () { } odb_download_server () { - #http://www.orientdb.org/portal/function/portal/download/phpuser@unknown.com/%20/%20/%20/%20/unknown/orientdb-community-1.6.2.tar.gz/false/false - - COMMIT_HASH=$(git rev-parse HEAD) - DOWN_USER=oriento+travis${COMMIT_HASH}@codemix.com ODB_VERSION=$1 CI_DIR=$2 ODB_PACKAGE="orientdb-community-${ODB_VERSION}" - # We need to resort to tricks to automate our CI environment as much as - # possible since the OrientDB guys keep changing the compressed archive - # format and moving the downloadable packages URLs. Luckily for us, we - # are smart enough to cope with that... at least until the next change. - ODB_PACKAGE_EXT="tar.gz" - ODB_PACKAGE_URL="http://www.orientdb.org/portal/function/portal/download/${DOWN_USER}/%20/%20/%20/%20/unknown/${ODB_PACKAGE}.${ODB_PACKAGE_EXT}/false/false" + ODB_PACKAGE_EXT="zip" ODB_C_PACKAGE=${ODB_PACKAGE}.${ODB_PACKAGE_EXT} - echo ${ODB_PACKAGE_URL} + OUTPUT_DIR="${2:-$(pwd)}" + + if [ ! -d "$OUTPUT_DIR" ]; then + mkdir "$OUTPUT_DIR" + fi + + if odb_command_exists "mvn" ; then + mvn org.apache.maven.plugins:maven-dependency-plugin:2.8:get -Dartifact=com.orientechnologies:orientdb-community:$ODB_VERSION:$ODB_PACKAGE_EXT:distribution -DremoteRepositories="https://oss.sonatype.org/content/repositories/snapshots/,https://oss.sonatype.org/content/repositories/releases/" -Ddest=$OUTPUT_DIR/$ODB_C_PACKAGE + else + echo "Cannot download $1 [maven is not installed]" + exit 1 + fi - odb_download $ODB_PACKAGE_URL $CI_DIR ODB_PACKAGE_PATH="${CI_DIR}/${ODB_PACKAGE}.${ODB_PACKAGE_EXT}" if [ $ODB_PACKAGE_EXT = "zip" ]; then unzip -q $ODB_PACKAGE_PATH -d ${CI_DIR} elif [ $ODB_PACKAGE_EXT = "tar.gz" ]; then tar xf $ODB_PACKAGE_PATH -C $CI_DIR - fi + fi; } diff --git a/ci/orientdb-server-config-1.7.xml b/ci/orientdb-server-config-1.7.xml new file mode 100644 index 0000000..2924c11 --- /dev/null +++ b/ci/orientdb-server-config-1.7.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ci/orientdb-server-config.xml b/ci/orientdb-server-config.xml index 5e29b58..c5ed932 100644 --- a/ci/orientdb-server-config.xml +++ b/ci/orientdb-server-config.xml @@ -7,6 +7,14 @@ + + + + + + + + @@ -18,27 +26,66 @@ - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + - + @@ -54,18 +101,15 @@ - + - - - - - + + diff --git a/example/transactions.js b/example/transactions.js new file mode 100644 index 0000000..d2e3b45 --- /dev/null +++ b/example/transactions.js @@ -0,0 +1,46 @@ +var config = require('../test/test-server.json'), + Oriento = require('../lib'), + oriento = Oriento(config), + db = oriento.use('GratefulDeadConcerts'); + + +db +.let('firstVertex', function (s) { + s + .create('vertex', 'V') + .set({ + foo: 'bar', + when: new Date() + }); +}) +.let('secondVertex', function (s) { + s + .create('vertex', 'V') + .set({ + greeting: 'Hello World', + nested: { + a: 1, + b: 2, + c: 3 + } + }); +}) +.let('joiningEdge', function (s) { + s + .create('edge', 'E') + .from('$firstVertex') + .to('$secondVertex') + .set({ + edgeProp1: 'a', + edgeProp2: 'b', + wat: true + }); +}) +.commit() +.return('$joiningEdge') +.all() +.then(function (results) { + console.log(results); + process.exit(); +}) +.done(); diff --git a/lib/bag.js b/lib/bag.js new file mode 100644 index 0000000..1c8a23f --- /dev/null +++ b/lib/bag.js @@ -0,0 +1,194 @@ +"use strict"; + +var RID = require('./recordid'), + Long = require('./long').Long; + +/** + * # RID Bag + * + * A bag of Record IDs, can come in two formats: + * + * * embedded - just a list of record ids. + * * tree based - a remote tree based data structure + * + * for more details on the RID Bag structure, see https://github.com/orientechnologies/orientdb/wiki/RidBag + * + * + * @param {String} serialized The base64 encoded RID Bag + */ +function Bag (serialized) { + this.serialized = serialized; + this.uuid = null; + this._content = []; + this._buffer = null; + this._type = null; + this._offset = 0; + this._current = -1; + this._size = null; + this._prefetchedRecords = null; +} + +Bag.BAG_EMBEDDED = 0; +Bag.BAG_TREE = 1; + +module.exports = Bag; + + +Object.defineProperties(Bag.prototype, { + /** + * The bag type. + * @type {String} + */ + type: { + get: function () { + if (this._type === null) { + this._parse(); + } + return this._type; + } + }, + /** + * The size of the bag. + * @type {Integer} + */ + size: { + get: function () { + if (this._size === null) { + this._parse(); + } + return this._size; + } + } +}); + + +/** + * Parse the bag content. + */ +Bag.prototype._parse = function () { + var buffer = new Buffer(this.serialized, 'base64'), + mode = buffer.readUInt8(0); + + if ((mode & 1) === 1) { + this._type = Bag.BAG_EMBEDDED; + } + else { + this._type = Bag.BAG_TREE; + } + + if ((mode & 2) === 2) { + this.uuid = buffer.slice(1, 16); + this._offset = 17; + } + else { + this._offset = 1; + } + + + if (this._type === Bag.BAG_EMBEDDED) { + this._size = buffer.readInt32BE(this._offset); + this._offset += 4; + } + else { + this._fileId = readLong(buffer, this._offset); + this._offset += 8; + this._pageIndex = readLong(buffer, this._offset); + this._offset += 8; + this._pageOffset = buffer.readInt32BE(this._offset); + this._offset += 4; + this._size = buffer.readInt32BE(this._offset); + this._offset += 4; + this._changeSize = buffer.readInt32BE(this._offset); + this._offset += 4; + } + this._buffer = buffer; +}; + +/** + * Return a representation of the bag that can be serialized to JSON. + * + * @return {Array} The JSON representation of the bag. + */ +Bag.prototype.toJSON = function () { + if (this.type === Bag.BAG_EMBEDDED) { + return this.all(); + } + else { + return undefined; // because we don't yet know how to serialize a tree bag to JSON. + } +}; + +/** + * Retrieve the next RID in the bag, or null if we're at the end. + * + * @return {RID|null} The next Record ID, or null. + */ +Bag.prototype.next = function () { + var rid; + if (!this._buffer) { + this._parse(); + } + if (this._type === Bag.BAG_EMBEDDED) { + if (this._current >= this._size - 1) { + return null; + } + this._current++; + rid = this._consume(); + if (this._prefetchedRecords && this._prefetchedRecords[rid]) { + this._content.push(this._prefetchedRecords[rid]); + } + else { + this._content.push(rid); + } + return rid; + } +}; + + +/** + * Retreive all the RIDs in the bag. + * + * @return {RID[]} The record ids. + */ +Bag.prototype.all = function () { + var length = this.length; + if (this._content.length !== length) { + while(this.next()); // jshint ignore:line + } + return this._content; +}; + +/** + * Consume the next RID in the bag. + * + * @return {RID|null} The next record id, or null if the bag is exhausted. + */ +Bag.prototype._consume = function () { + var rid; + if (this._type === Bag.BAG_EMBEDDED) { + if (this._offset >= this._buffer.length) { + return null; + } + rid = new RID(); + rid.cluster = this._buffer.readInt16BE(this._offset); + this._offset += 2; + rid.position = readLong(this._buffer, this._offset); + this._offset +=8; + return rid; + } +}; + + +/** + * Read a Long from the given buffer. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading at. + * @return {Number} The Long number. + */ +function readLong (buffer, offset) { + return Long.fromBits( + buffer.readUInt32BE(offset + 4), + buffer.readInt32BE(offset) + ).toNumber(); +} \ No newline at end of file diff --git a/lib/cli/command.js b/lib/cli/command.js index 286a00e..d6b4279 100644 --- a/lib/cli/command.js +++ b/lib/cli/command.js @@ -1,3 +1,5 @@ +"use strict"; + var utils = require('../utils'); /** @@ -13,13 +15,18 @@ function Command (server, options) { Object.defineProperty(this, 'db', { get: function () { if (!this._db && this.options.dbname) { - this._db = this.server.use(this.options.dbname); + this._db = this.server.use({ + name: this.options.dbname, + username: this.options.dbuser, + password: this.options.dbpassword + }); } return this._db; }, set: function (val) { - if (typeof val === 'string') + if (typeof val === 'string') { val = this.server.use(val); + } this._db = val; } }); @@ -38,10 +45,12 @@ Command.prototype.requiredArgv = ['password']; */ Command.prototype.run = function () { var subcommand = this.options._[1]; - if (!subcommand || subcommand === 'run' || typeof this[subcommand] !== 'function') + if (!subcommand || subcommand === 'run' || typeof this[subcommand] !== 'function') { return this.help(); - else + } + else { return this[subcommand].apply(this, this.options._.slice(2)); + } }; /** diff --git a/lib/cli/commands/db.js b/lib/cli/commands/db.js index 1a7ba83..882f829 100644 --- a/lib/cli/commands/db.js +++ b/lib/cli/commands/db.js @@ -1,3 +1,5 @@ +"use strict"; + /** * Create a database. * @@ -21,24 +23,23 @@ exports.create = function (name, type, storage) { * List the databases on the server. */ exports.list = function () { - console.log('The following databases exist on the server:'); return this.server.list() .then(function (dbs) { - Object.keys(dbs).forEach(function (name) { - console.log('\t\t' + name); + dbs.forEach(function (db) { + console.log(db.name); }); }); }; /** - * Delete a database. + * Drop a database. * * @param {String} name The name of the database. * @param {String} storage The storage type, defaults to plocal. */ -exports.delete = function (name, storage) { +exports.drop = function (name, storage) { console.log('Deleting database with name: ' + name); - return this.server.delete({ + return this.server.drop({ name: name, storage: storage }) diff --git a/lib/cli/commands/migrate.js b/lib/cli/commands/migrate.js index 0cc0a53..dd18b65 100644 --- a/lib/cli/commands/migrate.js +++ b/lib/cli/commands/migrate.js @@ -1,3 +1,5 @@ +"use strict"; + var path = require('path'), MigrationManager = require('../../migration/manager'); @@ -5,7 +7,7 @@ var path = require('path'), * The required CLI arguments for the command. * @type {Array} */ -exports.requiredArgv = ['dbname', 'password']; +exports.requiredArgv = ['dbname', 'password|dbpassword']; /** * Get the migration manager instance @@ -52,7 +54,9 @@ exports.applied = function () { */ exports.create = function () { var name = Array.prototype.join.call(arguments, ' '); - if (!name) throw new Error('Name is required'); + if (!name) { + throw new Error('Name is required'); + } console.log('Creating a new migration called: ' + name); return this.manager().create(name) .then(function (filename) { @@ -76,7 +80,7 @@ exports.up = function (limit) { results.forEach(function (item) { console.log('\t\t' + item); }); - }) + }); }; @@ -84,17 +88,13 @@ exports.up = function (limit) { * Migrate down. */ exports.down = function (limit) { - if (limit) { - console.log('Reverting a maximum of ' + limit + ' migration(s)...'); - } - else { - console.log('Reverting all available migrations...'); - } + limit = limit || 1; + console.log('Reverting a maximum of ' + limit + ' migration(s)...'); return this.manager().down(limit) .then(function (results) { console.log('Reverted ' + results.length + ' migration(s):'); results.forEach(function (item) { console.log('\t\t' + item); }); - }) -}; \ No newline at end of file + }); +}; diff --git a/lib/cli/index.js b/lib/cli/index.js index 7145f5d..bb48e57 100644 --- a/lib/cli/index.js +++ b/lib/cli/index.js @@ -1,3 +1,5 @@ +"use strict"; + var Promise = require('bluebird'), fs = Promise.promisifyAll(require('fs')), path = require('path'), @@ -31,8 +33,15 @@ CLI.prototype.run = function (argv) { command = this.createCommand(command, argv); total = command.requiredArgv.length; for (i = 0; i < total; i++) { - if (argv[command.requiredArgv[i]] === undefined) { - yargs.demand(command.requiredArgv[i]); + var possibleOpts = command.requiredArgv[i].split('|'); + var found = false; + for (var j = 0; j < possibleOpts.length; ++j) { + if (argv[possibleOpts[j]] !== undefined) { + found = true; + break; + } + } + if (!found) { return command.help(); } } @@ -53,6 +62,7 @@ CLI.prototype.createCommand = function (name, argv) { var Constructor = Command.extend(require(filename)), server = this.createServer(argv); + return new Constructor(server, argv); } catch (e) { @@ -190,4 +200,4 @@ CLI.prototype.list = function () { .map(function (filename) { return filename.slice(0, -3); }); -}; \ No newline at end of file +}; diff --git a/lib/db/class/custom.js b/lib/db/class/custom.js index ef55fb9..22e53ea 100644 --- a/lib/db/class/custom.js +++ b/lib/db/class/custom.js @@ -1,3 +1,5 @@ +"use strict"; + var Promise = require('bluebird'), Class = require('./index'); @@ -52,7 +54,9 @@ exports.set = function (key, value) { statements.push(this.db.query('ALTER CLASS ' + this.name + ' CUSTOM ' + key + '=' + obj[key])); } else { - statements.push(this.class.db.query('ALTER PROPERTY ' + this.class.name + '.' + this.name + ' CUSTOM ' + key + '=' + obj[key])); + statements.push(this.class.db.query( + 'ALTER PROPERTY ' + this.class.name + '.' + this.name + ' CUSTOM ' + key + '=' + obj[key] + )); } } return Promise.all(statements) diff --git a/lib/db/class/index.js b/lib/db/class/index.js index b5e9269..0bcec77 100644 --- a/lib/db/class/index.js +++ b/lib/db/class/index.js @@ -1,3 +1,5 @@ +"use strict"; + var Promise = require('bluebird'), Property = require('./property'), RID = require('../../recordid'), @@ -10,7 +12,9 @@ var Promise = require('bluebird'), */ function Class (config) { config = config || {}; - if (!(this instanceof Class)) return new Class(config); + if (!(this instanceof Class)) { + return new Class(config); + } this.augment('property', Property); this.augment('custom', require('./custom')); this.configure(config); @@ -170,8 +174,9 @@ exports.cached = false; * @promise {Object[]} An array of class objects. */ exports.list = function (refresh) { - if (!refresh && this.class.cached) + if (!refresh && this.class.cached) { return Promise.resolve(this.class.cached.items); + } return this.send('record-load', { cluster: 0, @@ -180,10 +185,12 @@ exports.list = function (refresh) { .bind(this) .then(function (response) { var record = response.records[0]; - if (!record || !record.classes) + if (!record || !record.classes) { return []; - else + } + else { return record.classes; + } }) .then(this.class.cacheData) .then(function () { @@ -197,9 +204,10 @@ exports.list = function (refresh) { * @param {String} name The name of the class to create. * @param {String} parentName The name of the parent to extend, if any. * @param {String|Integer} cluster The cluster name or id. + * @param {Boolean} isAbstract The flag for the abstract class * @promise {Object} The created class object */ -exports.create = function (name, parentName, cluster) { +exports.create = function (name, parentName, cluster, isAbstract) { var query = 'CREATE CLASS ' + name; if (parentName) { @@ -210,6 +218,10 @@ exports.create = function (name, parentName, cluster) { query += ' CLUSTER ' + cluster; } + if(isAbstract) { + query += ' ABSTRACT'; + } + return this.query(query) .bind(this) .then(function () { @@ -220,6 +232,31 @@ exports.create = function (name, parentName, cluster) { }); }; +/** + * Update the given class. + * + * @param {Object} class The class settings. + * @param {Boolean} reload Whether to reload the class, default to true. + * @promise {Object} The updated class. + */ +exports.update = function (cls, reload) { + var promises = [], + prefix = 'ALTER CLASS ' + cls.name + ' '; + + if (reload == null) { + reload = true; + } + + if (cls.superClass !== undefined) { + promises.push(this.exec(prefix + 'SUPERCLASS ' + cls.superClass)); + } + + return Promise.all(promises) + .bind(this) + .then(function () { + return this.class.get(cls.name, reload); + }); +}; /** * Delete a class. @@ -227,7 +264,7 @@ exports.create = function (name, parentName, cluster) { * @param {String} name The name of the class to delete. * @promise {Db} The database instance. */ -exports.delete = function (name) { +exports.drop = function (name) { return this.exec('DROP CLASS ' + name) .bind(this) .then(function () { @@ -238,7 +275,6 @@ exports.delete = function (name) { }); }; - /** * Get a class by name. * @@ -257,8 +293,9 @@ exports.get = function (name, refresh) { return this.class.cached.names[name] || Promise.reject(new errors.Request('No such class: ' + name)); }); } - else + else { return Promise.reject(new errors.Request('No such class: ' + name)); + } }; /** @@ -288,4 +325,4 @@ exports.cacheData = function (classes) { } return this; -}; \ No newline at end of file +}; diff --git a/lib/db/class/property.js b/lib/db/class/property.js index 52d51e0..e631b7a 100644 --- a/lib/db/class/property.js +++ b/lib/db/class/property.js @@ -1,3 +1,5 @@ +"use strict"; + var Promise = require('bluebird'), utils = require('../../utils'), errors = require('../../errors'); @@ -8,7 +10,9 @@ var Promise = require('bluebird'), */ function Property (config) { config = config || {}; - if (!(this instanceof Property)) return new Property(config); + if (!(this instanceof Property)) { + return new Property(config); + } this.augment('custom', require('./custom')); this.configure(config); } @@ -31,8 +35,8 @@ Property.prototype.configure = function (config) { this.readonly = config.readonly || false; this.notNull = config.notNull || false; this.collate = config.collate || 'default'; - this.min = config.min || null; - this.max = config.max || null; + this.min = typeof config.min !== 'undefined' ? config.min : null; + this.max = typeof config.max !== 'undefined' ? config.max : null; this.regexp = config.regexp || null; this.linkedClass = config.linkedClass || null; if (config.custom && config.custom.fields) { @@ -92,7 +96,9 @@ Property.list = function () { * @promise {Object} The created property. */ Property.create = function (config, reload) { - if (reload == null) reload = true; + if (reload == null) { + reload = true; + } if (Array.isArray(config)) { return Promise.all(config.map(function (item) { @@ -100,10 +106,12 @@ Property.create = function (config, reload) { }, this)) .bind(this) .then(function (results) { - if (reload) + if (reload) { return this.reload(); - else + } + else { return this; + } }) .then(function () { var total = config.length, @@ -164,8 +172,9 @@ Property.get = function (name) { i, item; for (i = 0; i < total; i++) { item = this.properties[i]; - if (item.name === name) + if (item.name === name) { return Promise.resolve(item); + } } return Promise.resolve(null); @@ -183,7 +192,9 @@ Property.update = function (property, reload) { prefix = 'ALTER PROPERTY ' + this.name + '.' + property.name + ' ', keys, total, key, i; - if (reload == null) reload = true; + if (reload == null) { + reload = true; + } if (property.linkedClass !== undefined) { promises.push(this.db.exec(prefix + 'LINKEDCLASS ' + property.linkedClass)); @@ -203,6 +214,9 @@ Property.update = function (property, reload) { if (property.type !== undefined) { promises.push(this.db.exec(prefix + 'TYPE ' + property.type)); } + if (property.readonly !== undefined) { + promises.push(this.db.exec(prefix + 'READONLY ' + (property.readonly ? 'true' : 'false'))); + } if (property.mandatory !== undefined) { promises.push(this.db.exec(prefix + 'MANDATORY ' + (property.mandatory ? 'true' : 'false'))); } @@ -222,8 +236,9 @@ Property.update = function (property, reload) { return Promise.all(promises) .bind(this) .then(function () { - if (reload) + if (reload) { return this.reload(); + } }) .then(function () { return this.property.get(property.name); @@ -236,7 +251,7 @@ Property.update = function (property, reload) { * @param {String} name The property name. * @promise {Class} The class instance with property removed. */ -Property.delete = function (name) { +Property.drop = function (name) { return this.db.exec('DROP PROPERTY ' + this.name + '.' + name) .bind(this) .then(this.reload) diff --git a/lib/db/cluster.js b/lib/db/cluster.js index 51a2fb3..7d5cd9e 100644 --- a/lib/db/cluster.js +++ b/lib/db/cluster.js @@ -1,3 +1,5 @@ +"use strict"; + var Promise = require('bluebird'), errors = require('../errors'); @@ -14,8 +16,9 @@ exports.cached = false; * @promise {Object[]} An array of cluster objects. */ exports.list = function (refresh) { - if (!refresh && this.cluster.cached) + if (!refresh && this.cluster.cached) { return Promise.resolve(this.cluster.cached.items); + } if (this.sessionId) { // db is already open, reload @@ -92,8 +95,9 @@ exports.getByName = function (name, refresh) { return this.cluster.cached.names[name]; }); } - else + else { return Promise.resolve(undefined); + } }; /** @@ -114,8 +118,9 @@ exports.getById = function (id, refresh) { return this.cluster.cached.ids[id]; }); } - else + else { return Promise.resolve(undefined); + } }; /** @@ -124,7 +129,7 @@ exports.getById = function (id, refresh) { * @param {String} nameOrId The name or id of the cluster. * @promise {Db} The database. */ -exports.delete = function (nameOrId) { +exports.drop = function (nameOrId) { return this.cluster.get(nameOrId) .bind(this) .then(function (cluster) { diff --git a/lib/db/edge/index.js b/lib/db/edge/index.js index 2e0132d..d593126 100644 --- a/lib/db/edge/index.js +++ b/lib/db/edge/index.js @@ -1,3 +1,5 @@ +"use strict"; + var RID = require('../../recordid'); // db.edge.from('#1:1').to('#2:1').create('Foo') @@ -36,6 +38,7 @@ exports.to = function (to) { */ function fluent (args) { args = args || {}; + /*jshint validthis:true */ return { to: function (to) { args.to = to; @@ -71,8 +74,9 @@ function createEdge (db, config, from, to) { attributes = config[1]; command += ' ' + className + ' FROM ' + edgeReference(from) + ' TO ' + edgeReference(to); - if (attributes) + if (attributes) { command += ' CONTENT ' + JSON.stringify(attributes); + } return db.query(command); } @@ -90,7 +94,7 @@ function createEdge (db, config, from, to) { function deleteEdge (db, config, from, to) { var command = "DELETE EDGE", className = config ? edgeConfig(config)[0] : false; - if (false && className) { + if (className) { command += ' ' + className; } command += ' FROM ' + edgeReference(from) + ' TO ' + edgeReference(to); @@ -146,10 +150,12 @@ function edgeReference (ref) { } else if (typeof ref === 'string') { // could be either an sql statement or an RID - if (rid = RID.parse(ref)) + if ((rid = RID.parse(ref))) { return rid; - else + } + else { return '(' + ref + ')'; + } } else if (ref instanceof RID) { return ref; diff --git a/lib/db/index.js b/lib/db/index.js index 6853be1..e9203ac 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -1,8 +1,18 @@ +"use strict"; + var utils = require('../utils'), errors = require('../errors'), Promise = require('bluebird'), RID = require('../recordid'), - Query = require('./query'); + Statement = require('./statement'), + Query = require('./query'), + Transaction = require('./transaction'), + ArrayLike = utils.ArrayLike, + inherits = require("util").inherits, + EventEmitter = require('events').EventEmitter, + fast = require('fast.js'), + parseFn = require("parse-function"); + /** @@ -11,22 +21,32 @@ var utils = require('../utils'), * @param {Object} config The optional configuration for the database. */ function Db (config) { - if (!config) + if (!config) { throw new errors.Config('Database object requires configuration'); + } this.configure(config); this.init(); this.augment('cluster', require('./cluster')); this.augment('class', require('./class')); this.augment('record', require('./record')); - this.augment('vertex', require('./vertex')); - this.augment('edge', require('./edge')); + utils.deprecate(this, 'vertex', 'db.vertex.* is deprecated, use the query builder instead!', function () { + this.augment('vertex', require('./vertex')); + }); + utils.deprecate(this, 'edge', 'db.edge.* is deprecated, use the query builder instead!', function () { + this.augment('edge', require('./edge')); + }); this.augment('index', require('./index/index')); } +inherits(Db, EventEmitter); + Db.prototype.augment = utils.augment; Db.extend = utils.extend; +Db.Statement = Statement; +Db.Query = Query; + module.exports = Db; /** @@ -36,22 +56,25 @@ module.exports = Db; */ Db.prototype.configure = function (config) { this.sessionId = config.sessionId != null ? config.sessionId : -1; + this.forcePrepare = config.forcePrepare != null ? config.forcePrepare : true; this.name = config.name; this.server = config.server; + this.server.on('reset', function () { + this.sessionId = null; + }.bind(this)); - this.type = config.type === 'document' - ? 'document' - : 'graph'; - - this.storage = (config.storage === 'plocal' || config.storage === 'memory') - ? config.storage - : 'plocal'; + this.type = (config.type === 'document' ? 'document' : 'graph'); + this.storage = ((config.storage === 'plocal' || config.storage === 'memory') ? config.storage : 'plocal'); + this.token = null; + this.useToken = config.useToken != null ? config.useToken : this.server.useToken; this.username = config.username || 'admin'; this.password = config.password || 'admin'; this.dataSegments = []; this.transactionId = 0; + this.transformers = config.transformers || {}; + this.transformerFunctions = {}; return this; }; @@ -68,7 +91,7 @@ Db.prototype.init = function () { * @promise {Db} The open db instance. */ Db.prototype.open = function () { - if (this.sessionId !== -1) { + if (this.sessionId != null && this.sessionId !== -1) { return Promise.resolve(this); } this.server.logger.debug('opening database connection to ' + this.name); @@ -76,7 +99,8 @@ Db.prototype.open = function () { name: this.name, type: this.type, username: this.username, - password: this.password + password: this.password, + useToken: this.useToken }) .bind(this) .then(function (response) { @@ -84,6 +108,7 @@ Db.prototype.open = function () { this.sessionId = response.sessionId; this.cluster.cacheData(response.clusters); this.serverCluster = response.serverCluster; + this.token = response.token; this.server.once('error', function () { this.sessionId = null; }.bind(this)); @@ -91,6 +116,20 @@ Db.prototype.open = function () { }); }; +/** + * Close the database. + * + * @promise {Db} The now closed db instance. + */ +Db.prototype.close = function () { + return this.server.send('db-close') + .bind(this) + .then(function () { + this.sessionId = null; + return this; + }); +}; + /** * Send the given operation to the server, ensuring the * database is open first. @@ -104,7 +143,11 @@ Db.prototype.send = function (operation, data) { .bind(this) .then(function () { data = data || {}; + data.token = data.token || this.token; data.sessionId = this.sessionId; + data.database = this.name; + data.db = this; + data.transformerFunctions = this.transformerFunctions; this.server.logger.debug('sending operation ' + operation + ' for database ' + this.name); return this.server.send(operation, data); }); @@ -117,8 +160,9 @@ Db.prototype.send = function (operation, data) { * @promise {Db} The database with reloaded configuration. */ Db.prototype.reload = function () { - if (this.sessionId === -1) + if (this.sessionId === -1) { return this.open(); + } this.server.logger.debug('Reloading database information'); return this.send('db-reload') .bind(this) @@ -128,6 +172,16 @@ Db.prototype.reload = function () { }); }; +/** + * Begin a new transaction. + * + * @return {Transaction} The transaction instance. + */ +Db.prototype.begin = function () { + this.transactionId++; + return new Transaction(this, this.transactionId); +}; + /** * Execute an SQL query against the database and retreive the raw, parsed response. @@ -137,14 +191,22 @@ Db.prototype.reload = function () { * @promise {Mixed} The results of the query / command. */ Db.prototype.exec = function (query, options) { + if (query instanceof Statement) { + options = query.buildOptions(); + query = query.toString(); + } + else if (!options) { + options = {}; + } var data = { query: query, - mode: 's', + mode: options.mode || 's', fetchPlan: '', limit: -1, - class: 'com.orientechnologies.orient.core.sql.OCommandSQL' + token: options.token, + language: options.language, + class: options.class || 'com.orientechnologies.orient.core.sql.OCommandSQL' }; - options = options || {}; if (options.fetchPlan && typeof options.fetchPlan === 'string') { data.fetchPlan = options.fetchPlan; @@ -152,13 +214,10 @@ Db.prototype.exec = function (query, options) { } if (+options.limit == options.limit) { data.limit = +options.limit; - data.mode = 'a'; - } - if (options.mode === 'a') { - data.mode = options.mode; + data.mode = options.mode || 'a'; } - if (data.mode === 'a') { + if (data.mode === 'a' && !options.class) { data.class = 'com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery'; } @@ -169,7 +228,7 @@ Db.prototype.exec = function (query, options) { params: options.params.reduce(function (params, param, i) { params[i] = param; return params; - }, {}) + }, new ArrayLike()) }; } else if (typeof options.params === 'object') { @@ -181,7 +240,32 @@ Db.prototype.exec = function (query, options) { this.server.logger.debug('executing query against db ' + this.name + ': ' + query); - return this.send('command', data); + if(this.listeners('beginQuery').length > 0) { + this.emit("beginQuery", data); + } + + var promise = this.send('command', data); + + if(this.listeners('endQuery').length > 0) { + var err, e, s = Date.now(); + promise + .bind(this) + .catch(function(_err) { + err = _err; + }) + .tap(function(res) { + e = Date.now(); + this.emit("endQuery", { + err: err, + result: res, + perf: { + query: e-s + } + }); + }); + } + + return promise; }; /** @@ -195,8 +279,9 @@ Db.prototype.query = function (command, options) { return this.exec(command, options) .bind(this) .then(function (response) { - if (response.results.length === 0) - return []; + if (!response.results || response.results.length === 0) { + return [[], []]; + } return response.results .map(this.normalizeResult, this) .reduce(flatten, []) @@ -212,22 +297,70 @@ Db.prototype.query = function (command, options) { }, [[], []]); }) .spread(function (results, preloaded) { - if (preloaded && preloaded.length) { - return results.map(function (result) { - if (result && result['@rid']) { - return this.record.resolveReferences(result, preloaded); + this.record.resolveReferences(results.concat(preloaded)); + return results; + }); +}; + +/** + * Execute a live query against the database + * + * @param {String} query The query or command to execute. + * @param {Object} options The options for the query / command. + * @promise {Mixed} The token of the live query. + */ +Db.prototype.liveQuery = function (command, options) { + options = options || {}; + options.mode = 'l'; + options.class='q'; + this.exec(command, options) + .bind(this) + .then(function (response) { + if (!response.results || response.results.length === 0) { + return [[], []]; } - else { - return result; + return response.results + .map(this.normalizeResult, this) + .reduce(flatten, []) + .reduce(function (list, item) { + if (item && item['@preloaded']) { + delete item['@preloaded']; + list[1].push(item); + } + else { + list[0].push(item); + } + return list; + }, [[], []]); + }) + .spread(function (results, preloaded) { + this.record.resolveReferences(results.concat(preloaded)); + return results; + }) + .then(function (response){ + if(response.length > 0){ + var iToken = response[0].token; + var parentDb = this; + var wrapperCallback = function(currentToken, operation, result){ + if(currentToken == iToken) { + if (operation === 1) { + parentDb.emit("live-update", result); + } else if (operation === 2) { + parentDb.emit("live-delete", result); + } else if (operation === 3) { + parentDb.emit("live-insert", result); + parentDb.emit("live-create", result); + } + } + }; + this.server.transport.connection.on("live-query-result", wrapperCallback); + } - }, this); - } - else { - return results; - } - }); + }); + return this; }; + /** * Normalize a result, where possible. * @param {Object} result The result to normalize. @@ -235,8 +368,12 @@ Db.prototype.query = function (command, options) { */ Db.prototype.normalizeResult = function (result) { var value; - if (!result) return result; - if (Array.isArray(result)) return result.map(this.normalizeResult, this); + if (!result) { + return result; + } + if (Array.isArray(result)) { + return result.map(this.normalizeResult, this); + } if (result.type === 'r' || result.type === 'f') { return this.normalizeResultContent(result.content, this); } @@ -260,8 +397,12 @@ Db.prototype.normalizeResult = function (result) { */ Db.prototype.normalizeResultContent = function (content) { var value; - if (!content) return null; - if (Array.isArray(content)) return content.map(this.normalizeResultContent, this); + if (!content) { + return null; + } + else if (Array.isArray(content)) { + return content.map(this.normalizeResultContent, this); + } if (content.type === 'd') { value = content.value || {}; @@ -276,6 +417,44 @@ Db.prototype.normalizeResultContent = function (content) { } }; +/** + * Register a transformer function for documents of the given class. + * This function will be invoked for each document of the specified class + * in all future result sets. + * + * @param {String} className The name of the document class. + * @param {Function} transformer The transformer function. + * @return {Db} The database instance. + */ +Db.prototype.registerTransformer = function (className, transformer) { + if (!this.transformers[className]) { + this.transformers[className] = []; + this.transformerFunctions[className] = fast.bind(this.transformDocument, this); + } + this.transformers[className].push(transformer); + return this; +}; + + +/** + * Transform a document according to its `@class` property, using the registered transformers. + * @param {Object} document The document to transform. + * @return {Mixed} The transformed document. + */ +Db.prototype.transformDocument = function (document) { + var className = document['@class']; + if (this.transformers[className]) { + return this.transformers[className].reduce(function (document, transformer) { + return transformer(document); + }, document); + } + else { + return document; + } +}; + + + // # Query Builder Methods /** @@ -287,6 +466,16 @@ Db.prototype.createQuery = function () { return new Query(this); }; +/** + * Create a create query. + * + * @return {Query} The query instance. + */ +Db.prototype.create = function () { + var query = this.createQuery(); + return query.create.apply(query, arguments); +}; + /** * Create a select query. * @@ -339,14 +528,98 @@ Db.prototype.delete = function () { return query.delete.apply(query, arguments); }; + +/** + * Create a transactional query. + * + * @return {Query} The query instance. + */ +Db.prototype.let = function () { + var query = this.createQuery(); + return query.let.apply(query, arguments); +}; + /** * Escape the given input. * * @param {String} input The input to escape. * @return {String} The escaped input. */ -Db.prototype.escape = function (input) { - return ('' + input).replace(/([^'\\]*(?:\\.[^'\\]*)*)'/g, '$1\\"'); +Db.prototype.escape = utils.escape; + + +/** + * Create a context for a user, using their authentication token. + * The context includes the query builder methods, which will be executed + * on behalf of the user. + * + * @param {Buffer|String} token The authentication token. + * @return {Object} The object containing the query builder methods. + */ +Db.prototype.createUserContext = function (token) { + var db = this; + return { + /** + * Create a query instance for this database. + * + * @return {Query} The query instance. + */ + createQuery: function () { + return db.createQuery().token(token); + }, + create: function () { + return db.create.apply(db, arguments).token(token); + }, + select: function () { + return db.select.apply(db, arguments).token(token); + }, + traverse: function () { + return db.traverse.apply(db, arguments).token(token); + }, + insert: function () { + return db.insert.apply(db, arguments).token(token); + }, + update: function () { + return db.update.apply(db, arguments).token(token); + }, + delete: function () { + return db.delete.apply(db, arguments).token(token); + }, + let: function () { + return db.let.apply(db, arguments).token(token); + }, + escape: utils.escape + }; +}; + +/** + * Create a orient function from a plain Javascript function + * + * @param {String} name The name of the function + * @param {Object} fn Plain Javascript function to stringify + * @param {Object} options Not currently used but will be used for 'IDEMPOTENT' arg + * @promise {Mixed} The results of the query / command. + */ +Db.prototype.createFn = function (name, fn, options) { + if(typeof(name) === "function") { + options = fn; + fn = name; + name = fn.name; + } + + var fnDef = parseFn(fn); + var params = ""; + var body = fnDef.body + .replace(/\'/g, "\\'") + .replace(/\"/g, '\\"') + .trim(); + + // NOTE: We can't do `PARAMETERS []` because else orientdb throws an error + if(fnDef.arguments.length > 0) { + params = 'PARAMETERS ['+fnDef.params+']'; + } + + return this.query('CREATE FUNCTION '+name+' "'+body+'" '+params+' LANGUAGE Javascript'); }; /** @@ -360,4 +633,4 @@ function flatten (list, item) { list.push(item); } return list; -} \ No newline at end of file +} diff --git a/lib/db/index/index.js b/lib/db/index/index.js index befca10..a10d78a 100644 --- a/lib/db/index/index.js +++ b/lib/db/index/index.js @@ -1,3 +1,5 @@ +"use strict"; + var Promise = require('bluebird'), utils = require('../../utils'), errors = require('../../errors'), @@ -9,7 +11,9 @@ var Promise = require('bluebird'), */ function Index (config) { config = config || {}; - if (!(this instanceof Index)) return new Index(config); + if (!(this instanceof Index)) { + return new Index(config); + } this.configure(config); } @@ -42,7 +46,10 @@ Index.prototype.add = function (args) { args = Array.prototype.slice.call(arguments); } return Promise.map(args, function (item) { - return this.db.query('INSERT INTO index:' + this.name + ' (key, rid) VALUES ("' + this.db.escape(item.key) + '", ' + this.db.escape(item.rid) + ')'); + return this.db.query( + 'INSERT INTO index:' + this.name + + ' (key, rid) VALUES ("' + this.db.escape(item.key) + '", ' + this.db.escape(item.rid) + ')' + ); }.bind(this)); }; @@ -68,18 +75,26 @@ Index.prototype.get = function (key) { * @promise {Index} The index object. */ Index.prototype.set = function (key, value) { - return this.db.query('INSERT INTO index:' + this.name + ' (key, rid) VALUES ("' + this.db.escape(key) + '", ' + this.db.escape(value['@rid'] || value) + ')') + return this.db.query( + 'INSERT INTO index:' + this.name + + ' (key, rid) VALUES ("' + this.db.escape(key) + '", ' + this.db.escape(value['@rid'] || value) + + ')' + ) .return(this); }; /** - * Delete the given rid from the index. + * Delete the given key from the index. * - * @param {String} rid The rid to delete. + * @param {String} key The key to delete. * @promise {Index} The index object. */ -Index.prototype.delete = function (rid) { - return this.db.query('DELETE FROM index:' + this.name + ' WHERE rid = ' + rid) +Index.prototype.delete = function (key) { + return this.db.query('DELETE FROM index:' + this.name + ' WHERE key = :key', { + params: { + key: key + } + }) .return(this); }; @@ -113,8 +128,9 @@ Index.cached = false; * @promise {Object[]} An array of index objects. */ Index.list = function (refresh) { - if (!refresh && this.index.cached) + if (!refresh && this.index.cached) { return Promise.resolve(this.index.cached.items); + } return this.send('record-load', { cluster: 0, @@ -123,10 +139,12 @@ Index.list = function (refresh) { .bind(this) .then(function (response) { var record = response.records[0]; - if (!record || !record.indexes) + if (!record || !record.indexes) { return []; - else + } + else { return record.indexes; + } }) .then(this.index.cacheData) .then(function () { @@ -141,6 +159,9 @@ Index.list = function (refresh) { * @promise {Object} The created index object. */ Index.create = function (config) { + if (Array.isArray(config)) { + return Promise.map(config, this.index.create.bind(this)); + } var query = 'CREATE INDEX ' + config.name; if (config.class) { @@ -173,12 +194,12 @@ Index.create = function (config) { /** - * Delete an index. + * Drop an index. * - * @param {String} name The name of the index to delete. + * @param {String} name The name of the index to drop. * @promise {Db} The database instance. */ -Index.delete = function (name) { +Index.drop = function (name) { return this.exec('DROP INDEX ' + name) .bind(this) .then(function () { @@ -206,8 +227,9 @@ Index.get = function (name, refresh) { return this.index.cached.names[name] || Promise.reject(new errors.Request('No such index: ' + name)); }); } - else + else { return Promise.reject(new errors.Request('No such index: ' + name)); + } }; /** diff --git a/lib/db/query.js b/lib/db/query.js index 450e8f6..9dfd3fe 100644 --- a/lib/db/query.js +++ b/lib/db/query.js @@ -1,3 +1,5 @@ +"use strict"; + var Statement = require('./statement'); module.exports = exports = Statement.extend({ @@ -100,7 +102,9 @@ module.exports = exports = Statement.extend({ * @promise {Mixed} The query results. */ exec: function (params) { - if (params) this.addParams(params); + if (params) { + this.addParams(params); + } return this.db.query(this.buildStatement(), this.buildOptions()) .bind(this) .then(this._processResults); diff --git a/lib/db/record.js b/lib/db/record.js index 4df37e2..2c05557 100644 --- a/lib/db/record.js +++ b/lib/db/record.js @@ -1,5 +1,8 @@ +"use strict"; + var Promise = require('bluebird'), RID = require('../recordid'), + RIDBag = require('../bag'), errors = require('../errors'); /** @@ -10,6 +13,11 @@ var Promise = require('bluebird'), * @promise {Object} The inserted record. */ exports.create = function (record, options) { + if (Array.isArray(record)) { + return Promise.all(record.map(function (record) { + return this.record.create(record, options); + }, this)); + } var className = record['@class'] || '', rid, promise; @@ -25,7 +33,9 @@ exports.create = function (record, options) { else if (className !== '') { promise = this.cluster.getByName(className); } - + else { + return Promise.reject(new errors.Operation('Cannot create record - cluster ID and/or class is invalid.')); + } return promise .bind(this) .then(function (cluster) { @@ -67,8 +77,9 @@ exports.get = function (record, options) { return Promise.all(record.map(this.record.read, this)); } var extracted = extractRecordId(record), - rid = extracted[0], - record = extracted[1]; + rid = extracted[0]; + + record = extracted[1]; options = options || {}; @@ -84,60 +95,71 @@ exports.get = function (record, options) { }) .bind(this) .then(function (response) { - if (response.records.length === 0) + if (response.records.length === 0) { return Promise.reject(new errors.Request('No such record')); - else if (response.records.length === 1) + } + else if (response.records.length === 1) { return response.records[0]; - else - return this.record.resolveReferences(response.records[0], response.records.slice(1)); + } + else { + this.record.resolveReferences(response.records); + return response.records[0]; + } }); }; /** - * Resolve references to child records for the given record. + * Resolve all references within the given collection of records. * - * @param {Object} record The primary record. - * @param {Object[]} children The child records. - * @return {Object} The primary record with references replaced. + * @param {Object[]} records The records to resolve. + * @return {Object} The records with references replaced. */ -exports.resolveReferences = function (record, children) { - var total = children.length, - indexed = {}, - replaceRecordIds = recordIdResolver(), - child, i; +exports.resolveReferences = function (records) { + var total = records.length, + collated = {}, + i; for (i = 0; i < total; i++) { - child = children[i]; - indexed[child['@rid']] = child; + if (records[i]) { + if(!collated[records[i]['@rid']]){ + collated[records[i]['@rid']] = records[i]; + } + } } + var replaceRecordIds = ridReplacer(collated); + for (i = 0; i < total; i++) { - child = children[i]; - replaceRecordIds(indexed, child); + replaceRecordIds(records[i]); } - return replaceRecordIds(indexed, record); + return records; }; -function recordIdResolver () { + +function ridReplacer (collated) { var seen = {}; return replaceRecordIds; /** * Replace references to records with their instances, where possible. * - * @param {Object} records The map of record ids to record instances. * @param {Object} obj The object to replace references within * @return {Object} The object with references replaced. */ - function replaceRecordIds (records, obj) { + function replaceRecordIds (obj) { if (!obj || typeof obj !== 'object') { return obj; } else if (Array.isArray(obj)) { - return obj.map(replaceRecordIds.bind(this, records)); + /*jshint validthis:true */ + return obj.map(replaceRecordIds); + } + else if (obj instanceof RIDBag) { + obj._prefetchedRecords = collated; + return obj; } - else if (obj instanceof RID && records[obj]) { - return records[obj]; + else if (obj instanceof RID && collated[obj]) { + return collated[obj]; } if (obj['@rid']) { if (seen[obj['@rid']]) { @@ -147,23 +169,27 @@ function recordIdResolver () { seen[obj['@rid']] = obj; } } + var keys = Object.keys(obj), total = keys.length, i, key, value; - for (i = 0; i < total; i++) { key = keys[i]; value = obj[key]; - if (!value || typeof value !== 'object' || key[0] === '@') continue; + if (!value || typeof value !== 'object' || key[0] === '@') { + continue; + } if (value instanceof RID) { - if (records[value]) - obj[key] = records[value]; + if (collated[value]) { + obj[key] = collated[value]; + } } else if (Array.isArray(value)) { - obj[key] = value.map(replaceRecordIds.bind(this, records)); + obj[key] = value.map(replaceRecordIds); + } + else { + obj[key] = replaceRecordIds(value); } - else - obj[key] = replaceRecordIds(records, value); } return obj; } @@ -180,8 +206,9 @@ exports.meta = function (record, options) { return Promise.all(record.map(this.record.read, this)); } var extracted = extractRecordId(record), - rid = extracted[0], - record = extracted[1]; + rid = extracted[0]; + + record = extracted[1]; options = options || {}; @@ -211,9 +238,10 @@ exports.meta = function (record, options) { exports.update = function (record, options) { var extracted = extractRecordId(record), rid = extracted[0], - record = extracted[1], promise, data; + record = extracted[1]; + options = options || {}; if (!rid) { @@ -241,8 +269,9 @@ exports.update = function (record, options) { return found; }); } - else + else { promise = Promise.resolve(record); + } return promise .bind(this) @@ -268,8 +297,9 @@ exports.delete = function (record, options) { return Promise.reject(new errors.Operation('Cannot delete - no record specified')); } var extracted = extractRecordId(record), - rid = extracted[0], - record = extracted[1]; + rid = extracted[0]; + + record = extracted[1]; options = options || {}; diff --git a/lib/db/statement.js b/lib/db/statement.js index 89aadd2..a76697e 100644 --- a/lib/db/statement.js +++ b/lib/db/statement.js @@ -1,3 +1,5 @@ +"use strict"; + var RID = require('../recordid'), utils = require('../utils'); @@ -29,6 +31,18 @@ Statement.prototype.select = clause('select', '*'); */ Statement.prototype.traverse = clause('traverse', '*'); +/** + * A 'strategy' clause for traverse query + * @param {String} args The strategy how traverse should go in deep, + * either 'DEPTH_FIRST'|'BREADTH_FIRST', the first one is default + * @return {Statement} The statement object + */ +Statement.prototype.strategy = function (s) { + if (typeof s === 'string' && s.toUpperCase() === 'DEPTH_FIRST' || s.toUpperCase() === 'BREADTH_FIRST') { + this._state.strategy = s.toUpperCase(); + } + return this; +}; /** * Insert expression. @@ -62,14 +76,31 @@ Statement.prototype.delete = clause('delete'); */ Statement.prototype.into = clause('into'); +/** + * Create a class, edge or vertex. + * + * @param {String} type Either `class`, `edge` or `vertex`. + * @param {String} name The entity name. + * @return {Statement} The statement object. + */ +Statement.prototype.create = clause('create'); + /** * Use the given record id or class name. * - * @param {String|String[]} args The record id, class name or expression. - * @return {Statement} The statement object. + * @param {String|String[]|Function} args The record id, class name or expression. + * @return {Statement} The statement object. */ Statement.prototype.from = clause('from'); +/** + * A `to` clause, used when creating edges. + * + * @param {String|Function} arg The target to create the edge to. + * @return {Statement} The statement object. + */ +Statement.prototype.to = clause('to'); + /** * Set the given column names to the given values. * @@ -78,6 +109,93 @@ Statement.prototype.from = clause('from'); */ Statement.prototype.set = clause('set'); +/** + * An `increment` clause. + * + * @param {String} property The property name to put the key to. + * @param {Object} value The value to increment by, defaults to 1 + * @return {Statement} The statement object. + */ +Statement.prototype.increment = function (property, value) { + this._state.increment = this._state.increment || []; + this._state.increment.push([property, value || 1]); + return this; +}; + + +/** + * An `add` clause for set / list properties. + * + * @param {String} property The property name to put the key to. + * @param {Object} ...values The values to add in the set / list property. + * @return {Statement} The statement object. + */ +Statement.prototype.add = function (property/*, ...values*/) { + var totalArguments = arguments.length, + values = new Array(totalArguments - 1), + a; + for (a = 1; a < totalArguments; a++) { + values[a - 1] = arguments[a]; + } + this._state.add = this._state.add || []; + this._state.add.push([property, values]); + return this; +}; + + +/** + * A `remove` clause for map / set / list properties. + * + * @param {String} property The property name to put the key to. + * @param {Object} ...values The keys / values to remove from the map / set / list property. + * @return {Statement} The statement object. + */ +Statement.prototype.remove = function (property/*, ...values*/) { + var totalArguments = arguments.length, + values = new Array(totalArguments - 1), + a; + for (a = 1; a < totalArguments; a++) { + values[a - 1] = arguments[a]; + } + this._state.remove = this._state.remove || []; + this._state.remove.push([property, values]); + return this; +}; + +/** + * A `put` clause for map properties. + * + * @param {String} property The property name to put the key to. + * @param {Object} keysValues The keys and values to add in the map property. + * @return {Statement} The statement object. + */ +Statement.prototype.put = function (property, keysValues) { + this._state.put = this._state.put || []; + this._state.put.push([property, keysValues]); + return this; +}; + + +/** + * Upsert the records to avoid multiple queries. + * + * @param {String|Object} condition The condition clause, if any. + * @param {Object} params The parameters to bind to the statement, if any. + * @param {String} comparisonOperator The operator to use for comparison, defaults to '='. + * @return {Statement} The statement object. + */ +Statement.prototype.upsert = function (condition, params, comparisonOperator) { + this._state.upsert = true; + if (condition) { + this._state.where = this._state.where || []; + this._state.where.push(['and', condition, comparisonOperator || '=']); + if (params) { + this.addParams(params); + } + } + return this; +}; + /** * Specify the where clause for the statement. * @@ -86,6 +204,25 @@ Statement.prototype.set = clause('set'); */ Statement.prototype.where = whereClause('and'); +/** + * Specify the while clause for the statement. + * + * > Note: This is actually just an alias of `WHERE`. + * + * @param {String|String[]} args The while clause + * @return {Statement} The statement object. + */ +Statement.prototype.while = Statement.prototype.where; + + +/** + * Specifiy a where clause, using the `CONTAINSTEXT` comparison operator. + * + * @param {Object} condition The map of field names to values. + * @return {Statement} The statement object. + */ +Statement.prototype.containsText = whereClause('and', 'CONTAINSTEXT'); + /** * Specify an `AND` condition. * @@ -124,11 +261,21 @@ Statement.prototype.order = clause('order'); * @param {Integer} value The offset. * @return {Statement} The statement object. */ -Statement.prototype.offset = function (value) { - this._state.offset = +value; +Statement.prototype.skip = function (value) { + this._state.skip = +value; return this; }; +/** + * Set the offset to start returning results from. + * + * > Note: This is just an alias of `.skip()`. + * + * @param {Integer} value The offset. + * @return {Statement} The statement object. + */ +Statement.prototype.offset = Statement.prototype.skip; + /** * Set the maximum number of results to return. * @@ -143,12 +290,182 @@ Statement.prototype.limit = function (value) { /** * Specify the fetch plan for the statement. * - * @param {String|Object} args The fetch plan clause + * @param {String|Object} args The fetch plan clause. * @return {Statement} The statement object. */ Statement.prototype.fetch = clause('fetchPlan'); +/** + * Assign a value to a variable within an SQL statement. + * + * > Note: The value will **not** be encoded as it may contain arbitrary SQL expressions. + * > use `Oriento.utils.encode()` if you need to allow safe values here. + * + * @param {String} name The name of the variable to assign + * @param {String|Statement} value The value of the variable, can be an SQL statement. + * @return {Statement} The statement object. + */ +Statement.prototype.let = function (name, value) { + this._state['let'] = this._state['let'] || []; + this._state['let'].push([name, value]); + return this; +}; + +/** + * Specifiy a lock clause for the statement. + * + * @param {String|Object} args The lock clause. + * @return {Statement} The statement object. + */ +Statement.prototype.lock = clause('lock'); + +/** + * Commit a transaction. + * + * @param {Integer} retryLimit The maximum number of times to retry, defaults to 0. + * @return {Statement} The statement object. + */ +Statement.prototype.commit = function (retryLimit) { + this._state.commit = retryLimit || 0; + return this; +}; + + +/** + * Specify the number of times to retry a command. + * + * @param {Integer} retryLimit The maximum number of times to retry, defaults to 1. + * @return {Statement} The statement object. + */ +Statement.prototype.retry = function (retryLimit) { + this._state.retry = retryLimit === undefined ? 1 : retryLimit; + return this; +}; + +/** + * Specify the number of milliseconds to wait between retrying a command. + * + * @param {Integer} waitLimit The number of ms to wait. + * @return {Statement} The statement object. + */ +Statement.prototype.wait = function (waitLimit) { + this._state.wait = waitLimit === undefined ? 1 : waitLimit; + return this; +}; + +/** + * Return a certain variable if there is a let clause. + * For update, insert or delete statements it will add a RETURN clause before + * the WHERE clause. + * + * @param {String} value The name of the variable or what to return. + * @return {Statement} The statement object. + */ +Statement.prototype.return = function (value) { + this._state.return = value; + return this; +}; + +/** + * Specify a lucene full text query. + * + * > NOTE: You must have installed the lucene query plugin in OrientDB for this to work. + * + * @param {String|Object} ...property The names of the properties to include in the query. + * @param {String} luceneQuery The lucene query, using lucene QueryParser syntax. + * @return {Statement} The statement object. + */ +Statement.prototype.lucene = function (property, luceneQuery) { + if (typeof property === 'object') { + var keys = Object.keys(property), + length = keys.length, + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + this.lucene(key, property[key]); + } + return this; + } + var properties; + if (arguments.length > 2) { + var totalArguments = arguments.length; + properties = new Array(totalArguments - 1); + for (var a = 0; a < totalArguments - 1; a++) { + properties[a] = arguments[a]; + } + luceneQuery = arguments[totalArguments - 1]; + } + else { + properties = Array.isArray(property) ? property : [property]; + } + + return this.where( + (properties.length === 1 ? properties[0] : '[' + properties.join(',') + ']') + + ' LUCENE ' + JSON.stringify(luceneQuery) + ); +}; + +/** + * Specify a lucene spatial query, find items near the given longitude / latitude. + * + * > NOTE: You must have installed the lucene query plugin in OrientDB for this to work. + * + * @param {String|Object} latitudeProperty Either the name of the longitude property, + * or a map of field names to values. + * @param {String|Number} longitudeProperty Either the name of the latitude property, + * or the optional `maxDistanceInKms`. + * @param {Number} longitude The longitude value to compare against. + * @param {Number} latitude The latitude value to compare against. + * @param {Number} maxDistanceInKms The maximum distance in kilometers. + * @return {Statement} The statement object. + */ +Statement.prototype.near = function (latitudeProperty, longitudeProperty, longitude, latitude, maxDistanceInKms) { + var properties = [], + values = []; + if (typeof latitudeProperty === 'object') { + var keys = Object.keys(latitudeProperty), + length = keys.length, + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + properties.push(key); + values.push(latitudeProperty[key]); + } + maxDistanceInKms = longitudeProperty; + } + else { + properties.push(latitudeProperty, longitudeProperty); + values.push(longitude, latitude); + } + if (maxDistanceInKms) { + properties.push('$spatial'); + values.push(JSON.stringify({maxDistance: maxDistanceInKms})); + } + return this.where( + '['+properties.join(',')+'] NEAR ['+values.join(',')+']' + ); +}; + + +/** + * Specify a lucene spatial query, find items within the given bounding box. + * + * > NOTE: You must have installed the lucene query plugin in OrientDB for this to work. + * + * @param {String|Number} latitudeProperty The name of the latitude property. + * @param {String|Object} longitudeProperty The name of the longitude property. + * @param {Array} box An array of coordinates. + * @return {Statement} The statement object. + */ +Statement.prototype.within = function (latitudeProperty, longitudeProperty, box) { + return this.where( + '['+latitudeProperty+','+longitudeProperty+'] WITHIN '+JSON.stringify(box) + ); +}; + + + /** * Add the given parameter to the query. * @@ -179,15 +496,53 @@ Statement.prototype.addParams = function (params) { }; +/** + * Use a particular Auth Token for this statement. + * + * @param {String} token The token to use + * @return {Statement} The statement object. + */ +Statement.prototype.token = function (token) { + if (typeof token === 'string') { + token = new Buffer(token, 'base64'); + } + this._state.token = token || false; + return this; +}; + + /** * Build the statement. - * @return {String} The SQL statement. + * @return {String} The SQL statement. */ Statement.prototype.buildStatement = function () { var statement = [], - state = this._state; + state = this._state, + self = this, + result; + + if (state.commit !== undefined) { + statement.push('BEGIN\n'); + if (state.let && state.let.length) { + statement.push(state.let.map(function (item) { + if (typeof item[1] === 'function') { + var child = new Statement(self.db); + child._state.paramIndex = self._state.paramIndex; + item[1](child); + return 'LET ' + item[0] + ' = ' + child + "\n"; + } + else { + return "LET " + item[0] + ' = ' + item[1] + "\n"; + } + }).join(' ')); + } + } + - if (state.traverse && state.traverse.length) { + if (state.create && state.create.length) { + statement.push('CREATE ' + state.create.join(' ')); + } + else if (state.traverse && state.traverse.length) { statement.push('TRAVERSE'); statement.push(state.traverse.join(', ')); } @@ -203,17 +558,79 @@ Statement.prototype.buildStatement = function () { statement.push('INSERT'); } else if (state.delete) { - statement.push('DELETE'); + if (state.delete.length) { + statement.push('DELETE ' + state.delete.join(' ')); + } + else { + statement.push('DELETE'); + } } if (state.from && state.from.length) { statement.push('FROM'); statement.push(state.from.map(function (item) { - if (typeof item === 'string') { - if (/(\s+)/.test(item)) + if (typeof item === 'function') { + var child = new Statement(self.db); + child._state.paramIndex = self._state.paramIndex; + item(child); + return '(' + child.toString() + ')'; + } + else if (item instanceof Statement) { + return '(' + item.toString() + ')'; + } + else if (typeof item === 'string') { + if (utils.requiresParens(item)) { return '(' + item + ')'; - else + } + else { return item; + } + } + else { + return ''+item; + } + }).join(', ')); + } + + if (state.commit === undefined && state.let && state.let.length) { + statement.push('LET ' + state.let.map(function (item) { + if (typeof item[1] === 'function') { + var child = new Statement(self.db); + child._state.paramIndex = self._state.paramIndex; + item[1](child); + return item[0] + ' = (' + child + ')'; + } + else if (item[1] instanceof Statement) { + return item[0] + ' = (' + item[1].toString() + ')'; + } + else if (utils.requiresParens(item[1])) { + return item[0] + ' = (' + item[1] + ')'; + } + else { + return item[0] + ' = ' + item[1]; + } + }).join(',')); + } + + if (state.to && state.to.length) { + statement.push('TO'); + statement.push(state.to.map(function (item) { + if (typeof item === 'function') { + var child = new Statement(self.db); + child._state.paramIndex = self._state.paramIndex; + item(child); + return '(' + child.toString() + ')'; + } + else if (item instanceof Statement) { + return '(' + item.toString() + ')'; + } + else if (typeof item === 'string') { + if (utils.requiresParens(item)) { + return '(' + item + ')'; + } + else { + return item; + } } else { return ''+item; @@ -225,10 +642,12 @@ Statement.prototype.buildStatement = function () { statement.push('INTO'); statement.push(state.into.map(function (item) { if (typeof item === 'string') { - if (/(\s+)/.test(item)) + if (utils.requiresParens(item)) { return '(' + item + ')'; - else + } + else { return item; + } } else { return ''+item; @@ -241,10 +660,7 @@ Statement.prototype.buildStatement = function () { statement.push(state.set.map(function (item) { var interim; if (typeof item === 'string') { - if (/(\s+)/.test(item)) - return '(' + item + ')'; - else - return item; + return item; } else { return this._objectToSet(item); @@ -252,11 +668,82 @@ Statement.prototype.buildStatement = function () { }, this).filter(function (item) { return item; }).join(', ')); } + if (state.increment && state.increment.length) { + statement.push('INCREMENT'); + statement.push(state.increment.map(function (item) { + return utils.escape(item[0]) + ' = ' + (+item[1]); + }).join(', ')); + } + + if (state.add && state.add.length) { + statement.push('ADD'); + statement.push(state.add.map(function (item) { + var field = item[0], + values = item[1]; + return values.map(function (value) { + var paramName = 'param' + paramify(field) + (this._state.paramIndex++); + this.addParam(paramName, value); + return field + ' = :' + paramName; + }, this).join(', '); + }, this).join(', ')); + } + + if (state.remove && state.remove.length) { + statement.push('REMOVE'); + statement.push(state.remove.map(function (item) { + var field = item[0], + values = item[1]; + return values.map(function (value) { + var paramName = 'param' + paramify(field) + (this._state.paramIndex++); + this.addParam(paramName, value); + return field + ' = :' + paramName; + }, this).join(', '); + }, this).join(', ')); + } + + if (state.put && state.put.length) { + statement.push('PUT'); + statement.push(state.put.map(function (item) { + var objectToAdd = item[1]; + var keys = Object.keys(objectToAdd); + var propertyName = item[0]; + return keys.map(function (key) { + key = utils.escape(key); + var paramName = 'param' + paramify(propertyName + key) + (this._state.paramIndex++); + this.addParam(paramName, objectToAdd[key]); + return propertyName + ' = "' + key + '", :' + paramName; + }, this).join(', '); + }, this).join(', ')); + } + + if (state.upsert) { + statement.push('UPSERT'); + } + + if ((state.update || state.insert || state.delete) && state.return) { + statement.push('RETURN'); + if (Array.isArray(state.return)) { + statement.push('['+state.return.join(',')+']'); + } + else if (typeof state.return === 'object') { + statement.push(encodeReturnObject(state.return)); + } + else { + statement.push(state.return); + } + } + if (state.where) { - statement.push('WHERE'); + if (state.traverse && state.traverse.length) { + statement.push('WHILE'); + } + else { + statement.push('WHERE'); + } statement.push(state.where.reduce(function (accumulator, item) { var op = item[0], - condition = item[1]; + condition = item[1], + comparisonOperator = item[2]; if (condition == null) { accumulator[0] = op; @@ -264,7 +751,7 @@ Statement.prototype.buildStatement = function () { } if (typeof condition === 'object') { - condition = this._objectToCondition(condition); + condition = this._objectToCondition(condition, comparisonOperator); } if (condition === false) { @@ -294,10 +781,12 @@ Statement.prototype.buildStatement = function () { statement.push('GROUP BY'); statement.push(state.group.map(function (item) { if (typeof item === 'string') { - if (/(\s+)/.test(item)) + if (utils.requiresParens(item)) { return '(' + item + ')'; - else + } + else { return item; + } } else { return ''+item; @@ -309,10 +798,23 @@ Statement.prototype.buildStatement = function () { statement.push('ORDER BY'); statement.push(state.order.map(function (item) { if (typeof item === 'string') { - if (/(\s+)/.test(item)) + if (utils.requiresParens(item)) { return '(' + item + ')'; - else + } + else { return item; + } + } + else if (item && typeof item === 'object') { + var keys = Object.keys(item), + length = keys.length, + parts = new Array(length), + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + parts.push(key, item[key]); + } + return parts.join(' '); } else { return ''+item; @@ -321,23 +823,74 @@ Statement.prototype.buildStatement = function () { } if (state.limit) { - statement.push('LIMIT ' + (+state.limit)) + statement.push('LIMIT ' + (+state.limit)); } - if (state.offset) { - statement.push('OFFSET ' + (+state.offset)) + + if (state.strategy && state.traverse) { + statement.push('STRATEGY ' + state.strategy); + } + + if (state.skip) { + statement.push('SKIP ' + (+state.skip)); + } + + if (state.lock) { + statement.push('LOCK ' + state.lock.join(',')); + } + + if (state.commit !== undefined) { + statement.push('\nCOMMIT'); + if (state.commit) { + statement.push('RETRY ' + (+state.commit)); + } + else if (state.retry) { + statement.push('RETRY ' + (+state.retry)); + } + statement.push('\n'); + } + else if (state.retry) { + statement.push('RETRY ' + (+state.retry)); + } + + if (state.wait) { + statement.push('WAIT ' + (+state.wait)); + } + + if (!(state.update || state.insert || state.delete) && state.return) { + statement.push('RETURN'); + if (Array.isArray(state.return)) { + statement.push('['+state.return.join(',')+']'); + } + else if (typeof state.return === 'object') { + statement.push(encodeReturnObject(state.return)); + } + else { + statement.push(state.return); + } } return statement.join(' '); }; +/** + * Return a string version of the statement, with parameters prepared and bound. + * + * @return {String} The prepared statement. + */ +Statement.prototype.toString = function () { + return utils.prepare(this.buildStatement(), this._state.params); +}; + + /** * Build the options for the statement. * @return {Object} The SQL statement options. */ Statement.prototype.buildOptions = function () { var opts = {}; - if (this._state.params) + if (this._state.params) { opts.params = this._state.params; + } if (this._state.fetchPlan) { opts.fetchPlan = this._state.fetchPlan.reduce(function (list, item) { var keys, total, key, i; @@ -357,20 +910,37 @@ Statement.prototype.buildOptions = function () { return list; }, []).join(' '); } + if (this._state.commit !== undefined) { + opts.class = 's'; + } + if (this._state.token !== undefined) { + opts.token = this._state.token; + } return opts; }; -Statement.prototype._objectToCondition = function (obj) { +Statement.prototype._objectToCondition = function (obj, operator) { var conditions = [], params = {}, keys = Object.keys(obj), total = keys.length, key, i, paramName; + operator = operator || '='; for (i = 0; i < total; i++) { key = keys[i]; - paramName = 'param' + paramify(key) + (this._state.paramIndex++); - conditions.push(key + ' = :' + paramName); - this.addParam(paramName, obj[key] instanceof RID ? ''+obj[key] : obj[key]); + if (obj[key] === null) { + if (operator === '!=' || operator === '<>' || operator === 'NOT') { + conditions.push(key + ' IS NOT NULL'); + } + else { + conditions.push(key + ' IS NULL'); + } + } + else { + paramName = 'param' + paramify(key) + (this._state.paramIndex++); + conditions.push(key + ' ' + operator + ' :' + paramName); + this.addParam(paramName, obj[key]); + } } if (conditions.length === 0) { @@ -382,7 +952,7 @@ Statement.prototype._objectToCondition = function (obj) { else { return '(' + conditions.join(' AND ') + ')'; } -} +}; Statement.prototype._objectToSet = function (obj) { var expressions = [], @@ -393,10 +963,19 @@ Statement.prototype._objectToSet = function (obj) { for (i = 0; i < total; i++) { key = keys[i]; value = obj[key]; - if (value instanceof RID) { + if (typeof value === 'function') { + var child = new Statement(this.db); + child._state.paramIndex = this._state.paramIndex; + value(child); + expressions.push(key + ' = (' + child.toString() + ')'); + } + else if (value instanceof Statement) { + expressions.push(key + ' = (' + value.toString() + ')'); + } + else if (value instanceof RID) { expressions.push(key + ' = ' + value); } - else { + else if (value !== undefined) { paramName = 'param' + paramify(key) + (this._state.paramIndex++); expressions.push(key + ' = :' + paramName); this.addParam(paramName, value); @@ -409,7 +988,7 @@ Statement.prototype._objectToSet = function (obj) { else { return expressions.join(', '); } -} +}; function paramify (key) { return key.replace(/([^A-Za-z0-9])/g, ''); @@ -430,15 +1009,27 @@ function clause (name) { }; } -function whereClause (operator) { +function whereClause (operator, comparisonOperator) { + comparisonOperator = comparisonOperator || '='; return function (condition, params) { - this._state.where = this._state.where || []; - this._state.where.push([operator, condition]); + this._state.where.push([operator, condition, comparisonOperator]); if (params) { this.addParams(params); } return this; - } + }; } + +function encodeReturnObject (obj) { + var keys = Object.keys(obj), + length = keys.length, + parts = new Array(length), + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + parts[i] = utils.encode(key) + ":" + obj[key]; + } + return '{'+parts.join(',')+'}'; +} diff --git a/lib/db/transaction.js b/lib/db/transaction.js new file mode 100644 index 0000000..8c96bc0 --- /dev/null +++ b/lib/db/transaction.js @@ -0,0 +1,191 @@ +"use strict"; + +var Promise = require('bluebird'), + RID = require('../recordid'), + errors = require('../errors'); + +/** + * # Transactions + * + * + * @param {Db} db The database the transaction is for. + * @param {Integer} id The transaction id. + */ +function Transaction (db, id) { + this.db = db; + this.id = id; + this._pos = -1; + this._creates = []; + this._updates = []; + this._deletes = []; +} + +module.exports = Transaction; + +/** + * Commit the transaction. + * + * @promise {Object} The results of the transaction. + */ +Transaction.prototype.commit = function () { + return this.db.cluster.list() + .bind(this) + .then(function () { + var ids = {}; + return [ + this._creates.map(resolveClusterId, this), + this._updates.map(resolveClusterId, this), + this._deletes.map(resolveClusterId, this) + ]; + function resolveClusterId (item) { + var className = (item['@class'] || '').toLowerCase(); + if (className) { + if (ids[className] == null) { + ids[className] = this.db.cluster.cached.names[className].id; + } + item['@rid'].cluster = ids[className]; + } + return item; + } + }) + .spread(function (creates, updates, deletes) { + return this.db.send('tx-commit', { + storageType: this.db.storage, + txLog: true, + txId: this.id, + creates: creates, + updates: updates, + deletes: deletes + }) + .then(function (response) { + return [creates, updates, deletes, response]; + }); + }) + .spread(function (creates, updates, deletes, response) { + return { + created: response.created.map(function (result) { + var total = creates.length, + item, i; + for (i = 0; i < total; i++) { + item = creates[i]; + if (item['@rid'].cluster === result.tmpCluster && + item['@rid'].position === result.tmpPosition) { + + item['@rid'].cluster = result.cluster; + item['@rid'].position = result.position; + return item; + } + } + }) + .filter(notEmpty), + updated: response.updated.map(function (result) { + var total = updates.length, + item, i; + for (i = 0; i < total; i++) { + item = updates[i]; + if (item['@rid'].cluster === result.cluster && + item['@rid'].position === result.position) { + + item['@version'] = result.version; + return item; + } + } + }) + .filter(notEmpty), + deleted: deletes.filter(notEmpty) + }; + }); +}; + +function notEmpty (item) { return item; } + +/** + * Insert the given record into the database. + * + * @param {Object} record The record to insert. + * @return {Transaction} The transaction instance. + */ +Transaction.prototype.create = function (record) { + if (Array.isArray(record)) { + return record.map(this.create, this); + } + record['@rid'] = new RID({ + cluster: -1, + position: (--this._pos) + }); + this._creates.push(record); + return this; +}; + + +/** + * Update the given record. + * + * @param {Object} record The record to update. + * @return {Transaction} The transaction instance. + */ +Transaction.prototype.update = function (record) { + if (Array.isArray(record)) { + return record.map(this.update, this); + } + var extracted = extractRecordId(record), + rid = extracted[0]; + + record = extracted[1]; + + if (!rid) { + throw new errors.Operation('Cannot update record - record ID is not specified or invalid.'); + } + + this._updates.push(record); + return this; +}; + +/** + * Delete the given record. + * + * @param {String|RID|Object} record The record or record id to delete. + * @return {Transaction} The transaction instance. + */ +Transaction.prototype.delete = function (record) { + if (Array.isArray(record)) { + return record.map(this.delete, this); + } + var extracted = extractRecordId(record), + rid = extracted[0]; + + record = extracted[1]; + + if (!rid) { + throw new errors.Operation('Cannot delete record - record ID is not specified or invalid.'); + } + + this._deletes.push(record); + return this; +}; + +/** + * Extract the record id and record from the given argument. + * + * @param {String|RID|Object} record The record. + * @return {[RID, Object]} The record id and object. + */ +function extractRecordId (record) { + var rid = false; + if (typeof record === 'string') { + rid = RID.parse(record); + record = { + '@rid': rid + }; + } + else if (record instanceof RID) { + rid = record; + record = { + '@rid': rid + }; + } + else if (record['@rid']) { + record['@rid'] = rid = RID.parse(record['@rid']); + } + return [rid, record]; +} \ No newline at end of file diff --git a/lib/db/vertex/index.js b/lib/db/vertex/index.js index 83a9dc9..9aba0cb 100644 --- a/lib/db/vertex/index.js +++ b/lib/db/vertex/index.js @@ -1,3 +1,5 @@ +"use strict"; + var Promise = require('bluebird'), RID = require('../../recordid'), errors = require('../../errors'); @@ -9,12 +11,14 @@ var Promise = require('bluebird'), * @promise {Object} The created vertex. */ exports.create = function (config) { - if (Array.isArray(config)) return Promise.map(config, this.vertex.create, this); + if (Array.isArray(config)) { + return Promise.map(config, this.vertex.create, this); + } var command = 'CREATE VERTEX', promise, cluster, className, attributes; config = vertexConfig(config); className = config[0]; - cluster = config[1] + cluster = config[1]; attributes = config[2]; if (cluster && +cluster == cluster) { promise = Promise.resolve(cluster); @@ -38,7 +42,7 @@ exports.create = function (config) { if (attributes) { command += " CONTENT " + JSON.stringify(attributes); } - return this.query(command) + return this.query(command); }) .then(function (results) { return results[0]; @@ -92,8 +96,9 @@ function vertexConfig (config) { if (config['@class']) { className = config['@class']; } - if (config['@cluster']) + if (config['@cluster']) { cluster = config['@cluster']; + } } return [className, cluster, obj]; } \ No newline at end of file diff --git a/lib/errors/base.js b/lib/errors/base.js index e12bdaf..25a6920 100644 --- a/lib/errors/base.js +++ b/lib/errors/base.js @@ -1,16 +1,22 @@ +"use strict"; + /** * A custom error class */ function OrientDBError () { this.init.apply(this, arguments); Error.call(this); - Error.captureStackTrace(this, arguments.callee); + Error.captureStackTrace(this, this.constructor); } /** * Extend the native error class. * @type {Object} */ -OrientDBError.prototype.__proto__ = Error.prototype; +OrientDBError.prototype = Object.create(Error.prototype, { + constructor: { + value: OrientDBError + } +}); /** * The name of the error. @@ -34,9 +40,11 @@ OrientDBError.prototype.init = function (message) { OrientDBError.inherit = function (init) { var parent = this; var child = function () { return parent.apply(this, arguments); }; - var Surrogate = function () {this.constructor = child; }; - Surrogate.prototype = parent.prototype; - child.prototype = new Surrogate; + child.prototype = Object.create(parent.prototype, { + constructor: { + value: child + } + }); child.prototype.init = init; child.prototype.name = init.name; diff --git a/lib/errors/config.js b/lib/errors/config.js index 5700812..65e1bed 100644 --- a/lib/errors/config.js +++ b/lib/errors/config.js @@ -1,3 +1,5 @@ +"use strict"; + var OrientDBError = require('./base'); module.exports = OrientDBError.inherit(function ConfigError (message, data) { diff --git a/lib/errors/connection.js b/lib/errors/connection.js index 826f46a..5cd3359 100644 --- a/lib/errors/connection.js +++ b/lib/errors/connection.js @@ -1,3 +1,5 @@ +"use strict"; + var OrientDBError = require('./base'); module.exports = OrientDBError.inherit(function ConnectionError (code, message, data) { diff --git a/lib/errors/index.js b/lib/errors/index.js index f29622f..3cab68d 100644 --- a/lib/errors/index.js +++ b/lib/errors/index.js @@ -1,3 +1,5 @@ +"use strict"; + exports.Base = exports.OrientDB = exports.OrientDBError = require('./base'); exports.Connection = exports.ConnectionError = require('./connection'); exports.Protocol = exports.ProtocolError = require('./protocol'); diff --git a/lib/errors/operation.js b/lib/errors/operation.js index 2755431..4a4bd4a 100644 --- a/lib/errors/operation.js +++ b/lib/errors/operation.js @@ -1,3 +1,5 @@ +"use strict"; + var OrientDBError = require('./base'); module.exports = OrientDBError.inherit(function OperationError (message, data) { diff --git a/lib/errors/protocol.js b/lib/errors/protocol.js index d80463c..398e369 100644 --- a/lib/errors/protocol.js +++ b/lib/errors/protocol.js @@ -1,3 +1,5 @@ +"use strict"; + var OrientDBError = require('./base'); module.exports = OrientDBError.inherit(function ProtocolError (message, data) { diff --git a/lib/errors/record.js b/lib/errors/record.js index d64eb91..9182976 100644 --- a/lib/errors/record.js +++ b/lib/errors/record.js @@ -1,3 +1,5 @@ +"use strict"; + var OrientDBError = require('./base'); module.exports = OrientDBError.inherit(function RecordError (code, message, data) { diff --git a/lib/errors/request.js b/lib/errors/request.js index a408eeb..3bc0090 100644 --- a/lib/errors/request.js +++ b/lib/errors/request.js @@ -1,3 +1,5 @@ +"use strict"; + var OperationError = require('./operation'); module.exports = OperationError.inherit(function RequestError (message, data) { diff --git a/lib/index.js b/lib/index.js index 7a5d73a..5f6b31d 100755 --- a/lib/index.js +++ b/lib/index.js @@ -5,13 +5,17 @@ function Oriento (config) { } Oriento.RecordID = Oriento.RecordId = Oriento.RID = require('./recordid'); - +Oriento.RIDBag = Oriento.Bag = require('./bag'); Oriento.Server = require('./server'); Oriento.Db = require('./db'); -Oriento.protocol = require('./protocol'); +Oriento.Statement = Oriento.Db.Statement; +Oriento.Query = Oriento.Db.Query; +Oriento.transport = require('./transport'); Oriento.errors = require('./errors'); Oriento.Migration = require('./migration'); Oriento.CLI = require('./cli'); +Oriento.utils = require('./utils'); +Oriento.jsonify = Oriento.utils.jsonify; /** * A list of orientdb data types, indexed by their type id. diff --git a/lib/protocol/long.js b/lib/long.js similarity index 99% rename from lib/protocol/long.js rename to lib/long.js index a87d057..cac5e75 100755 --- a/lib/protocol/long.js +++ b/lib/long.js @@ -1,3 +1,5 @@ +"use strict"; + // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -41,7 +43,7 @@ */ function Long(low, high) { if(!(this instanceof Long)) return new Long(low, high); - + this._bsontype = 'Long'; /** * @type {number} diff --git a/lib/migration/index.js b/lib/migration/index.js index 1846fa1..01d9dfe 100644 --- a/lib/migration/index.js +++ b/lib/migration/index.js @@ -1,3 +1,5 @@ +"use strict"; + var Promise = require('bluebird'), errors = require('../errors'), utils = require('../utils'), @@ -13,7 +15,9 @@ function Migration (config) { this.name = ''; this.server = null; this.db = null; - if (config) this.configure(config); + if (config) { + this.configure(config); + } } Migration.extend = utils.extend; diff --git a/lib/migration/manager.js b/lib/migration/manager.js index 3e9f3ec..a50b823 100644 --- a/lib/migration/manager.js +++ b/lib/migration/manager.js @@ -1,3 +1,5 @@ +"use strict"; + var Promise = require('bluebird'), errors = require('../errors'), utils = require('../utils'), @@ -14,7 +16,9 @@ function MigrationManager (config) { this.db = null; this.dir = process.cwd(); this.className = 'Migration'; - if (config) this.configure(config); + if (config) { + this.configure(config); + } } MigrationManager.extend = utils.extend; @@ -77,8 +81,7 @@ MigrationManager.prototype.create = function (config) { * @return {String} The generated JavaScript source code. */ MigrationManager.prototype.generateMigration = function (config) { - var content = 'exports.name = ' + JSON.stringify(config.name) + ';\n\n'; - content += 'exports.db = ' + JSON.stringify(config.db) + ';\n\n'; + var content = '"use strict";\nexports.name = ' + JSON.stringify(config.name) + ';\n\n'; content += 'exports.up = function (db) {\n // @todo implementation\n};\n\n'; content += 'exports.down = function (db) {\n // @todo implementation\n};\n\n'; return content; @@ -99,10 +102,8 @@ MigrationManager.prototype.list = function () { this.listApplied() ]); }) - .then(function (args) { - var available = args[0], - applied = args[1], - pending = [], + .spread(function (available, applied) { + var pending = [], totalAvailable = available.length, totalApplied = applied.length, item, other, i, j, found; @@ -117,10 +118,15 @@ MigrationManager.prototype.list = function () { break; } } - if (!found) + if (!found) { pending.push(item); + } } return pending; + }) + .then(function (migrations) { + migrations.sort(); + return migrations; }); }; @@ -191,10 +197,12 @@ MigrationManager.prototype.up = function (limit) { return this.list() .bind(this) .filter(function (item, index) { - if (limit && index >= limit) + if (limit && index >= limit) { return false; - else + } + else { return true; + } }) .reduce(function (accumulator, name) { return this.applyMigration(name) @@ -202,7 +210,7 @@ MigrationManager.prototype.up = function (limit) { accumulator.push(name); return accumulator; }); - }, []) + }, []); }; @@ -219,13 +227,16 @@ MigrationManager.prototype.down = function (limit) { return item.name; }) .then(function (items) { + items.sort(); return items.reverse(); }) .filter(function (item, index) { - if (limit && index >= limit) + if (limit && index >= limit) { return false; - else + } + else { return true; + } }) .reduce(function (accumulator, name) { return this.revertMigration(name) @@ -233,7 +244,7 @@ MigrationManager.prototype.down = function (limit) { accumulator.push(name); return accumulator; }); - }, []) + }, []); }; @@ -293,4 +304,5 @@ MigrationManager.prototype.revertMigration = function (name) { }) .return(result); }); -}; \ No newline at end of file +}; + diff --git a/lib/protocol/deserializer.js b/lib/protocol/deserializer.js deleted file mode 100644 index 59f863c..0000000 --- a/lib/protocol/deserializer.js +++ /dev/null @@ -1,224 +0,0 @@ -var RecordID = require('../recordid'), - Long = require('./long').Long - - -/** - * A map of start delimiters to their end delimiters. - * @type {Object} - */ -var START_DELIMITERS = { - '[': ']', - '{': '}', - '(': ')', - '<': '>' -}; - -/** - * A map of end delimiters to their start delimiters. - * @type {Object} - */ -var END_DELIMITERS = { - ']': '[', - '}': '{', - ')': '(', - '>': '<' -} - -/** - * Deserialize a given serialized document. - * - * @param {String} serialized The serialized document. - * @param {Object} document The optional document to apply the unserialized values to. - * @param {Boolean} isMap If this is a map of values - * @return {Object} The deserialized document - */ -function deserializeDocument (serialized, document, isMap) { - if (serialized == null) return serialized; - serialized = serialized.trim(); - document = document || {}; - - var classIndex = serialized.indexOf("@"), - indexOfColon = serialized.indexOf(":"), - fieldIndex, field, commaIndex, value; - - if (~classIndex && (indexOfColon > classIndex || !~indexOfColon)) { - document['@class'] = serialized.substr(0, classIndex); - serialized = serialized.substr(classIndex + 1); - } - - if (!isMap) { - document['@type'] = 'd'; - } - - var fieldIndex; - - while (~(fieldIndex = serialized.indexOf(':'))) { - field = serialized.substr(0, fieldIndex); - serialized = serialized.substr(fieldIndex + 1); - if (field.charAt(0) === "\"" && field[field.length - 1] === "\"") { - field = field.substring(1, field.length - 1); - } - commaIndex = findCommaIndex(serialized); - value = serialized.substr(0, commaIndex); - serialized = serialized.substr(commaIndex + 1); - value = deserializeValue(value); - document[field] = value; - } - - return document; -}; - -/** - * Deserialize a bag of values. - * - * @param {String} value The serialized bag. - * @return {RecordID[]} The unserialized bag. - */ -function deserializeBag (value) { - var buffer = new Buffer(value.slice(1,-1), 'base64'), - offset = 0, - status = buffer.readUInt8(offset++), - total = buffer.readUInt32BE(offset), - content = [], - i, cluster, position; - - offset += 4; - for (i = 0; i < total; i++) { - cluster = buffer.readInt16BE(offset); - offset += 2; - position = Long.fromBits(buffer.readUInt32BE(offset + 4), buffer.readInt32BE(offset)).toNumber(); - offset += 8; - content.push(new RecordID({ - cluster: cluster, - position: position - })); - } - return content; -} - -/** - * Deserialize a given value into the right type. - * - * @param {String} value The serialized value to unserialize. - * @return {Mixed} The deserialized value. - */ -function deserializeValue (value) { - value = (''+value).trim(); - if(value === '') { - return null; - } - - if (value === 'true' || value === 'false') { - return value === 'true'; - } - - var firstChar = value.charAt(0), - lastChar = value[value.length - 1], - values; - - if (firstChar === "\"") { - // string - return value.substr(1, value.length - 2).replace(/\\"/g, "\"").replace(/\\\\/, "\\"); - } - else if (firstChar === '%' && lastChar === ';') { - return deserializeBag(value); - } - else if (lastChar === 't' || lastChar === 'a') { - // date - return new Date(parseInt(value.substr(0, value.length - 1))); - } - else if (firstChar === '(') { - // object / document - return deserializeDocument(value.substr(1, value.length - 2)); - } - if (firstChar === '{') { - // map - return deserializeDocument(value.substr(1, value.length - 2), {}, true); - } - else if (firstChar === '[' || firstChar === '<') { - //process Set <...> like List [...] - values = splitList(value.substr(1, value.length - 2)); - for (var i = 0, length = values.length; i < length; i++) { - values[i] = deserializeValue(values[i]); - } - return values; - } - else if (lastChar === 'b') { - // byte - return String.fromCharCode(parseInt(value.substr(0, value.length - 1))); - } - else if (lastChar === 'l' || lastChar === 's' || lastChar === 'c') { - // integer - return parseInt(value.substr(0, value.length - 1), 10); - } - else if (lastChar === 'f' || lastChar === 'd') { - // float / decimal - return parseFloat(value.substr(0, value.length - 1)); - } - else if (+value == value) { - // integer - return +value; - } - else if (value && (rid = RecordID.parse(value))) { - return rid; - } - else { - return value; - } -}; - - -/** - * Find the index of the first comma in the given serialized input, - * disregarding values that appear within delimiters. - * - * @param {String} serialized The serialized value to inspect. - * @return {Integer} The index of the first comma. - */ -function findCommaIndex (serialized) { - var delimiters = [], - current, prev, i, length; - for (i = 0, length = serialized.length; i < length; i++) { - current = serialized[i]; - prev = delimiters[delimiters.length - 1]; - if (current === "," && delimiters.length === 0) { - return i; - } - else if (START_DELIMITERS[current] && prev !== "\"") { - delimiters.push(current); - } - else if (END_DELIMITERS[current] && prev !== "\"" && (current === START_DELIMITERS[prev] || current === '"')) { - delimiters.pop(); - } - else if (current === "\"" && prev === "\"" && i > 0 && serialized[i - 1] !== "\\") { - delimiters.pop(); - } - else if (current === "\"" && prev !== "\"") { - delimiters.push(current); - } - } - return serialized.length; -} - -/** - * Split a serialized list into items that can then be parsed individually. - * @param {String} value The list value to split. - * @return {String[]} The split items, ready to be parsed - */ -function splitList (value) { - var result = [], - commaAt; - while (value.length) { - commaAt = findCommaIndex(value); - result.push(value.substr(0, commaAt)); - value = value.substr(commaAt + 1); - } - return result; -} - - -// export the public methods - - -exports.deserializeValue = deserializeValue; -exports.deserializeDocument = deserializeDocument; \ No newline at end of file diff --git a/lib/recordid.js b/lib/recordid.js index a7fb1b5..10242fd 100644 --- a/lib/recordid.js +++ b/lib/recordid.js @@ -1,3 +1,5 @@ +"use strict"; + /** * # Record ID * @@ -26,8 +28,9 @@ * */ function RecordID (input) { - if (!(this instanceof RecordID)) + if (!(this instanceof RecordID)) { return new RecordID(input); + } this.cluster = null; this.position = null; @@ -35,8 +38,9 @@ function RecordID (input) { if (input) { if (typeof input === 'string') { var parsed = RecordID.parse(input); - if (parsed) + if (parsed) { return parsed; + } } else if (Array.isArray(input)) { return input.map(function (item) { @@ -44,8 +48,12 @@ function RecordID (input) { }); } else if (typeof input === 'object') { - if (input.cluster == +input.cluster) this.cluster = +input.cluster; - if (input.position == +input.position) this.position = +input.position; + if (input.cluster == +input.cluster) { + this.cluster = +input.cluster; + } + if (input.position == +input.position) { + this.position = +input.position; + } } } } @@ -66,33 +74,65 @@ RecordID.prototype.isValid = function () { return this.cluster == +this.cluster && this.position == +this.position; }; +/** + * Determine whether the record id is equal to another. + * + * @param {String|RID} rid The RID to compare with. + * @return {Boolean} If the RID matches, then true. + */ +RecordID.prototype.equals = function (rid) { + if (rid === this) { + return true; + } + else if (typeof rid === 'string') { + return this.toString() === rid; + } + else if (rid && rid['@rid']) { + return this.equals(rid['@rid']); + } + else if (rid instanceof RecordID) { + return rid.cluster === this.cluster && rid.position === this.position; + } + else if ((rid = RecordID.parse(rid))) { + return rid.cluster === this.cluster && rid.position === this.position; + } + else { + return false; + } +}; /** * Parse a record id into a RecordID object. * * @param {String|Array|Object} input The input to parse. - * @return {RecordID|RecordID[]|Boolean} The parsed RecordID instance(s) or false if the record id could not be parsed + * @return {RecordID|RecordID[]|Boolean} The parsed RecordID instance(s) + * or false if the record id could not be parsed */ RecordID.parse = function (input) { if (input && typeof input === 'object') { - if (Array.isArray(input)) + if (Array.isArray(input)) { return input.map(RecordID.parse) .filter(function (item) { return item; }); - else if (input.cluster != null && input.position != null) + } + else if (input.cluster != null && input.position != null) { return new RecordID(input); - else + } + else { return false; + } } var matches = /^#(-?\d+):(-?\d+)$/.exec(input); - if (!matches) + if (!matches) { return false; - else + } + else { return new RecordID({ cluster: +matches[1], position: +matches[2] }); + } }; /** @@ -109,9 +149,11 @@ RecordID.isValid = function (input) { return /^#(-?\d+):(-?\d+)$/.test(input); } else if (input && Array.isArray(input)) { + total = input.length; for (i = 0; i < total; i++) { - if (!RecordID.isValid(input[i])) + if (!RecordID.isValid(input[i])) { return false; + } } return i ? true : false; } @@ -121,7 +163,7 @@ RecordID.isValid = function (input) { else { return false; } -} +}; /** * Return a record id for a given cluster and position. diff --git a/lib/server/config.js b/lib/server/config.js index 739af44..5b9916b 100644 --- a/lib/server/config.js +++ b/lib/server/config.js @@ -1,3 +1,5 @@ +"use strict"; + /** * Get a configuration value from the server. * diff --git a/lib/server/index.js b/lib/server/index.js index 329740a..41fa3d4 100644 --- a/lib/server/index.js +++ b/lib/server/index.js @@ -1,5 +1,9 @@ -var ConnectionPool = require('./connection-pool'), - Connection = require('./connection'), +"use strict"; + +var ConnectionPool = require('../transport/binary/connection-pool'), + Connection = require('../transport/binary/connection'), + BinaryTransport = require('../transport/binary'), + RestTransport = require('../transport/rest'), utils = require('../utils'), errors = require('../errors'), Db = require('../db/index'), @@ -15,9 +19,8 @@ var ConnectionPool = require('./connection-pool'), * @param {String|Object} options The server URL, or configuration object */ function Server (options) { - this.applySettings(options || {}); + this.configure(options || {}); this.init(); - this.augment('config', require('./config')); EventEmitter.call(this); this.setMaxListeners(Infinity); @@ -31,7 +34,17 @@ Server.prototype.augment = utils.augment; module.exports = Server; - +Object.defineProperty(Server.prototype, 'token', { + get: function () { + return this.transport.token; + }, + set: function (token) { + if (typeof token === 'string') { + token = new Buffer(token, 'base64'); + } + this.transport.token = token; + } +}); /** * Configure the server instance. @@ -39,24 +52,30 @@ module.exports = Server; * @param {Object} config The configuration for the server. * @return {Server} The configured server object. */ -Server.prototype.applySettings = function (options) { - - this.connecting = false; - this.closing = false; +Server.prototype.configure = function (config) { + this.useToken = config.useToken || false; + this.configureLogger(config.logger || {}); + this.configureTransport(config); +}; - this.host = options.host || options.hostname || 'localhost'; - this.port = options.port || 2424; - this.username = options.username || 'root'; - this.password = options.password || ''; - this.sessionId = -1; - this.configureLogger(options.logger || {}); - if (options.pool) { - this.configurePool(options.pool); +/** + * Configure the transport for the server. + * + * @param {Object} config The server config. + * @return {Server} The configured server object. + */ +Server.prototype.configureTransport = function (config) { + if (config.transport === 'rest') { + this.transport = new RestTransport(config); } else { - this.configureConnection(); + this.transport = new BinaryTransport(config); } + this.transport.on('reset', function () { + this.emit('reset'); + }.bind(this)); + return this; }; /** @@ -75,45 +94,6 @@ Server.prototype.configureLogger = function (config) { }; -/** - * Configure a connection for the server. - * - * @return {Server} The server instance with the configured connection. - */ -Server.prototype.configureConnection = function () { - this.connection = new Connection({ - host: this.host, - port: this.port, - logger: this.logger - }); - this.connection.on('update-config', function (config) { - this.logger.debug('updating config...'); - this.serverCluster = config; - }.bind(this)); - return this; -}; - -/** - * Configure a connection pool for the server. - * - * @param {Object} config The connection pool config - * @return {Server} The server instance with the configured connection pool. - */ -Server.prototype.configurePool = function (config) { - this.pool = new ConnectionPool({ - host: this.host, - port: this.port, - logger: this.logger, - max: config.max - }); - this.pool.on('update-config', function (config) { - this.logger.debug('updating config...'); - this.serverCluster = config; - }.bind(this)); - return this; -}; - - /** * Initialize the server instance. */ @@ -121,33 +101,6 @@ Server.prototype.init = function () { }; -/** - * Connect to the server. - * - * @promise {Server} The connected server instance. - */ -Server.prototype.connect = function () { - if (this.sessionId !== -1) - return Promise.resolve(this); - - if (this.connecting) { - return this.connecting; - } - - this.connecting = (this.pool || this.connection).send('connect', { - username: this.username, - password: this.password - }) - .bind(this) - .then(function (response) { - this.logger.debug('got session id: ' + response.sessionId); - this.sessionId = response.sessionId; - return this; - }); - - return this.connecting; -}; - /** * Send an operation to the server, * @@ -156,18 +109,7 @@ Server.prototype.connect = function () { * @promise {Mixed} The result of the operation. */ Server.prototype.send = function (operation, options) { - options = options || {}; - if (~this.sessionId) { - options.sessionId = options.sessionId != null ? options.sessionId : this.sessionId; - return (this.pool || this.connection).send(operation, options); - } - else { - return this.connect() - .then(function (server) { - options.sessionId = options.sessionId != null ? options.sessionId : this.sessionId; - return (server.pool || server.connection).send(operation, options); - }); - } + return this.transport.send(operation, options); }; /** @@ -176,12 +118,9 @@ Server.prototype.send = function (operation, options) { * @return {Server} the disconnected server instance */ Server.prototype.close = function () { - if (!this.closing && this.socket) { - this.closing = false; - this.sessionId = -1; - } + this.transport.close(); return this; -} +}; // # Database Related Methods @@ -192,7 +131,9 @@ Server.prototype.close = function () { * @return {Db} The database instance. */ Server.prototype.use = function (config) { - if (!config) throw new errors.Config('Cannot use a database without a name.'); + if (!config) { + throw new errors.Config('Cannot use a database without a name.'); + } if (typeof config === 'string') { config = { @@ -202,9 +143,23 @@ Server.prototype.use = function (config) { } else { config.server = this; + if (config.username) { + this.transport.username = config.username; + this.transport.skipServerConnect = true; + } + if (config.password) { + this.transport.password = config.password; + this.transport.skipServerConnect = true; + } } - if (!config.name) throw new errors.Config('Cannot use a database without a name.'); + if (!config.name) { + throw new errors.Config('Cannot use a database without a name.'); + } + + if (config.useToken == null) { + config.useToken = this.useToken; + } return new Db(config); }; @@ -227,7 +182,7 @@ Server.prototype.create = function (config) { } else { config = { - name: config.name || config.name, + name: config.name, type: (config.type || config.type), storage: config.storage || config.storage || 'plocal' }; @@ -237,8 +192,9 @@ Server.prototype.create = function (config) { return Promise.reject(new errors.Config('Cannot create database, no name specified.')); } - if (config.type !== 'document' && config.type !== 'graph') + if (config.type !== 'document' && config.type !== 'graph') { config.type = 'graph'; + } if (config.storage !== 'local' && config.storage !== 'plocal' && config.storage !== 'memory') { config.storage = 'plocal'; @@ -259,7 +215,7 @@ Server.prototype.create = function (config) { * @param {String|Object} config The database name or configuration object. * @promise {Mixed} The server response. */ -Server.prototype.delete = function (config) { +Server.prototype.drop = function (config) { config = config || ''; if (typeof config === 'string' || typeof config === 'number') { @@ -270,7 +226,7 @@ Server.prototype.delete = function (config) { } else { config = { - name: config.name || config.name, + name: config.name, storage: config.storage || config.storage || 'plocal' }; } @@ -283,9 +239,6 @@ Server.prototype.delete = function (config) { .return(true); }; -// deprecated name -Server.prototype.drop = Server.prototype.delete; - /** * List all the databases on the server. * @@ -293,6 +246,7 @@ Server.prototype.drop = Server.prototype.delete; */ Server.prototype.list = function () { return this.send('db-list') + .bind(this) .then(function (results) { var names = Object.keys(results.databases), total = names.length, @@ -317,7 +271,7 @@ Server.prototype.list = function () { * Determine whether a database exists with the given name. * * @param {String} name The database name. - * @param {String} storageType The storage type, defaults to `local`. + * @param {String} storageType The storage type, defaults to `plocal`. * @promise {Boolean} true if the database exists. */ Server.prototype.exists = function (name, storageType) { @@ -329,7 +283,7 @@ Server.prototype.exists = function (name, storageType) { } storageType = storageType || 'plocal'; return this.send('db-exists', { - name: (''+name).toLowerCase(), + name: ''+name, storage: storageType.toLowerCase() }) .then(function (response) { @@ -343,21 +297,51 @@ Server.prototype.exist = Server.prototype.exists; /** * Freeze the database with the given name. * - * @param {String} databaseName The name of the database to freeze. + * @param {String} name The database name. + * @param {String} storageType The storage type, defaults to `plocal`. * @return {Object} The response from the server. */ -Server.prototype.freeze = function (databaseName) { - // @todo implementation - throw new Error('Not yet implemented!'); +Server.prototype.freeze = function (name, storageType) { + var config; + + storageType = storageType || 'plocal'; + config = { + name: name, + storage: storageType.toLowerCase() + }; + + if (!config.name) { + return Promise.reject(new errors.Config('Cannot freeze, no database specified.')); + } + + this.logger.debug('Freeze database ' + config.name); + return this.send('db-freeze', config) + .return(true); }; /** * Release the database with the given name. * - * @param {String} databaseName The name of the database to release. + * @param {String} name The database name. + * @param {String} storageType The storage type, defaults to `plocal`. * @return {Object} The response from the server. */ -Server.prototype.release = function (databaseName) { - // @todo implementation - throw new Error('Not yet implemented!'); + +Server.prototype.release = function (name, storageType) { + var config; + + storageType = storageType || 'plocal'; + config = { + name: name, + storage: storageType.toLowerCase() + }; + + if (!config.name) { + return Promise.reject(new errors.Config('Cannot release, no database specified.')); + } + + this.logger.debug('Release database ' + config.name); + return this.send('db-release', config) + .return(true); + }; diff --git a/lib/server/connection-pool.js b/lib/transport/binary/connection-pool.js similarity index 76% rename from lib/server/connection-pool.js rename to lib/transport/binary/connection-pool.js index fc8716e..9cf50be 100644 --- a/lib/server/connection-pool.js +++ b/lib/transport/binary/connection-pool.js @@ -1,6 +1,9 @@ +"use strict"; + var Connection = require('./connection'), util = require('util'), - EventEmitter = require('events').EventEmitter; + EventEmitter = require('events').EventEmitter, + Promise = require('bluebird'); function ConnectionPool (config) { EventEmitter.call(this); @@ -21,7 +24,10 @@ module.exports = ConnectionPool; * @return {Connection} A connection instance. */ ConnectionPool.prototype.dip = function () { - if (++this.index >= this.max) this.index = 0; + if (++this.index >= this.max) { + this.index = 0; + } + if (this.index > this.connections.length - 1) { this.connections.push(this.createConnection()); } @@ -58,4 +64,19 @@ ConnectionPool.prototype.send = function (operation, options) { .then(function (connection) { return connection.send(operation, options); }); +}; + + +/** + * Close all the connections in the pool. + * + * @return {ConnectionPool} The connection pool with sockets closed. + */ +ConnectionPool.prototype.close = function () { + this.connections.forEach(function (connection) { + connection.close(); + }); + this.connections = []; + this.index = -1; + return this; }; \ No newline at end of file diff --git a/lib/server/connection.js b/lib/transport/binary/connection.js similarity index 72% rename from lib/server/connection.js rename to lib/transport/binary/connection.js index 70ff9fb..86ea949 100644 --- a/lib/server/connection.js +++ b/lib/transport/binary/connection.js @@ -1,11 +1,14 @@ -var net = require('net') +"use strict"; + +var net = require('net'), util = require('util'), - utils = require('../utils'), - errors = require('../errors'), - Operation = require('../protocol/operation'), - operations = require('../protocol/operations'), + utils = require('../../utils'), + errors = require('../../errors'), + OperationStatus = require('./operation-status'), EventEmitter = require('events').EventEmitter, - Promise = require('bluebird'); + Promise = require('bluebird'), + Operation = require('./protocol28/operation'); //TODO refactor this!!! + function Connection (config) { EventEmitter.call(this); @@ -16,6 +19,11 @@ function Connection (config) { this.logger = config.logger || {debug: function () {}}; this.setMaxListeners(Infinity); + + this.enableRIDBags = config.enableRIDBags == null ? true : config.enableRIDBags; + this.closing = false; + this.reconnectNow = false; + this.protocol = null; this.queue = []; this.writes = []; this.remaining = null; @@ -39,7 +47,7 @@ Connection.prototype.connect = function () { } this.socket = this.createSocket(); - return this.negotiateConnection(); + return (this.connecting = this.negotiateConnection()); }; /** @@ -69,9 +77,10 @@ Connection.prototype.send = function (op, params) { * @promise {Object} The result of the operation. */ Connection.prototype._sendOp = function (op, params) { + var self = this; return new Promise(function (resolve, reject) { if (typeof op === 'string') { - op = new operations[op](params || {}); + op = new self.protocol.operations[op](params || {}); } // define the write operations op.writer(); @@ -79,21 +88,20 @@ Connection.prototype._sendOp = function (op, params) { op.reader(); var buffer = op.buffer(); - - if (this.socket) { - this.socket.write(buffer); - } - else { - this.writes.push(buffer); - } - if (op.id === 'REQUEST_DB_CLOSE') { - resolve({}); + self.reconnectNow = true; + self.socket.write(buffer, resolve); + return; + } + else if (self.socket) { + self.socket.write(buffer); } else { - this.queue.push([op, {resolve: resolve, reject: reject}]); + self.writes.push(buffer); } - }.bind(this)); + + self.queue.push([op, {resolve: resolve, reject: reject}]); + }); }; /** @@ -104,7 +112,7 @@ Connection.prototype._sendOp = function (op, params) { */ Connection.prototype.cancel = function (err) { var item, op; - while (item = this.queue.shift()) { + while ((item = this.queue.shift())) { op = item[0]; item[1].reject(err); } @@ -124,6 +132,21 @@ Connection.prototype.createSocket = function () { return socket; }; +/** + * Close the socket. + * + * @return {Connection} The now closed connection. + */ +Connection.prototype.close = function () { + if (this.socket) { + // to avoid hangup of process we need to destroy socket completely. + this.socket.removeAllListeners(); + this.socket.destroy(); + this.socket = null; + } + return this; +}; + /** * Negotiate a connection to the server. * @@ -149,10 +172,12 @@ Connection.prototype.negotiateConnection = function () { this.logger.debug('connection closed during negotiation: ' + err); this.socket.removeAllListeners(); this.connecting = false; - if (err) + if (err) { reject(new errors.Connection(err.code, err.message)); - else + } + else { reject(new errors.Connection(0, 'Socket Closed')); + } }.bind(this)); }.bind(this)); @@ -169,6 +194,16 @@ Connection.prototype.negotiateProtocol = function () { this.socket.removeAllListeners('error'); this.protocolVersion = data.readUInt16BE(0); this.logger.debug('server protocol: ' + this.protocolVersion); + if (this.protocolVersion >= 28) { + this.protocol = require('./protocol28'); + } + else if (this.protocolVersion === 26) { + this.protocol = require('./protocol26'); + } + else { + this.protocol = require('./protocol19'); + } + this.protocol.deserializer.enableRIDBags = this.enableRIDBags; resolve(this); this.connecting = false; this.bindToSocket(); @@ -200,6 +235,19 @@ Connection.prototype.negotiateProtocol = function () { this.socket.removeAllListeners(); reject(new errors.Connection(err.code, err.message)); }.bind(this)); + this.socket.once('close', function (err) { + this.logger.debug('connection closed during protocol negotiation: ' + err); + if (this.socket) { + this.socket.removeAllListeners(); // close earlier could have destroyed & set socket to null. + } + this.connecting = false; + if (err) { + reject(new errors.Connection(err.code, err.message)); + } + else { + reject(new errors.Connection(0, 'Socket Closed')); + } + }.bind(this)); }.bind(this)); }; @@ -210,7 +258,7 @@ Connection.prototype.negotiateProtocol = function () { Connection.prototype.bindToSocket = function () { this.socket.on('data', this.handleSocketData.bind(this)); this.socket.on('error', this.handleSocketError.bind(this)); - this.socket.on('close', this.handleSocketClose.bind(this)) + this.socket.on('close', this.handleSocketClose.bind(this)); this.socket.on('end', this.handleSocketEnd.bind(this)); }; @@ -257,9 +305,16 @@ Connection.prototype.handleSocketError = function (err) { * @param {Error} err The error object, if any. */ Connection.prototype.handleSocketEnd = function (err) { + if (this.reconnectNow) { + this.reconnectNow = false; + this.destroySocket(); + this.emit('reconnectNow'); + return; + } if (this.closing) { this.closing = false; this.destroySocket(); + this.emit('close'); return; } err = new errors.Connection(1, err || 'Remote server closed the connection.'); @@ -299,32 +354,50 @@ Connection.prototype.destroySocket = function () { * @return {Integer} The offset that was successfully read up to. */ Connection.prototype.process = function (buffer, offset) { - var code, parsed, result, status, item, op, deferred, err; + var code, parsed, result, status, item, op, deferred, err, token, operation; offset = offset || 0; - while (item = this.queue.shift()) { + if(this.queue.length === 0){ + op = new Operation();//TODO refactor this! + parsed = op.consume(buffer, offset); + status = parsed[0]; + if (status === OperationStatus.PUSH_DATA) { + offset = parsed[1]; + result = parsed[2]; + this.emit('update-config', result); + return offset; + }else if(status === OperationStatus.LIVE_RESULT){ + token = parsed[1]; + operation = parsed[2]; + result = parsed[3]; + offset = parsed[4]; + this.emit('live-query-result', token, operation, result); + return offset; + } + } + while ((item = this.queue.shift())) { op = item[0]; deferred = item[1]; parsed = op.consume(buffer, offset); status = parsed[0]; offset = parsed[1]; result = parsed[2]; - if (status === Operation.READING) { + if (status === OperationStatus.READING) { // operation is incomplete, buffer does not contain enough data this.queue.unshift(item); return offset; } - else if (status === Operation.PUSH_DATA) { + else if (status === OperationStatus.PUSH_DATA) { this.emit('update-config', result); this.queue.unshift(item); return offset; } - else if (status === Operation.COMPLETE) { + else if (status === OperationStatus.COMPLETE) { deferred.resolve(result); } - else if (status === Operation.ERROR) { + else if (status === OperationStatus.ERROR) { if (result.status.error) { // this is likely a recoverable error - deferred.reject(result.status.error) + deferred.reject(result.status.error); } else { // cannot recover, reject everything and let the application decide what to do @@ -334,8 +407,9 @@ Connection.prototype.process = function (buffer, offset) { this.emit('error', err); } } - else + else { deferred.reject(new errors.Protocol('Unsupported operation status: ' + status)); + } } return offset; -}; \ No newline at end of file +}; diff --git a/lib/transport/binary/index.js b/lib/transport/binary/index.js new file mode 100644 index 0000000..03d2be6 --- /dev/null +++ b/lib/transport/binary/index.js @@ -0,0 +1,225 @@ +"use strict"; + +var ConnectionPool = require('./connection-pool'), + Connection = require('./connection'), + utils = require('../../utils'), + errors = require('../../errors'), + Db = require('../../db/index'), + Promise = require('bluebird'), + net = require('net'), + util = require('util'), + EventEmitter = require('events').EventEmitter; + + +/** + * # Binary Transport + * + * @param {Object} config The configuration for the transport. + */ +function BinaryTransport (config) { + EventEmitter.call(this); + this.setMaxListeners(Infinity); + this.configure(config || {}); + this.closing = false; +} + +util.inherits(BinaryTransport, EventEmitter); + +BinaryTransport.extend = utils.extend; +BinaryTransport.prototype.augment = utils.augment; + +module.exports = BinaryTransport; + + +/** + * Configure the transport. + * + * @param {Object} config The transport configuration. + */ +BinaryTransport.prototype.configure = function (config) { + this.connecting = false; + this.closing = false; + this.retries = 0; + this.maxRetries = config.maxRetries || 5; + + this.host = config.host || config.hostname || 'localhost'; + this.port = config.port || 2424; + this.username = config.username || 'root'; + this.password = config.password || ''; + + this.enableRIDBags = config.enableRIDBags == null ? true : config.enableRIDBags; + this.useToken = config.useToken || false; + this.token = config.token || null; + + this.sessionId = -1; + this.configureLogger(config.logger || {}); + if (config.pool) { + this.configurePool(config.pool); + } + else { + this.configureConnection(); + } +}; + +/** + * Configure the logger for the transport. + * + * @param {Object} config The logger config + * @return {BinaryTransport} The transport instance with the configured logger. + */ +BinaryTransport.prototype.configureLogger = function (config) { + this.logger = { + error: config.error || console.error.bind(console), + log: config.log || console.log.bind(console), + debug: config.debug || function () {} // do not log debug by default + }; + return this; +}; + + +/** + * Configure a connection for the transport. + * + * @return {BinaryTransport} The transport instance with the configured connection. + */ +BinaryTransport.prototype.configureConnection = function () { + this.connection = new Connection({ + host: this.host, + port: this.port, + enableRIDBags: this.enableRIDBags, + logger: this.logger, + useToken: this.useToken + }); + this.connection.on('update-config', function (config) { + this.logger.debug('updating config...'); + this.transportCluster = config; + }.bind(this)); + this.connection.on('reconnectNow', function () { + reconnectTransport(this); + }.bind(this)); + this.connection.on('error', function (err) { + if (this.retries++ > this.maxRetries) { + return this.emit('error', err); + } + reconnectTransport(this, err); + }.bind(this)); + return this; +}; + +/** + * Configure a connection pool for the transport. + * + * @param {Object} config The connection pool config + * @return {BinaryTransport} The transport instance with the configured connection pool. + */ +BinaryTransport.prototype.configurePool = function (config) { + this.pool = new ConnectionPool({ + host: this.host, + port: this.port, + enableRIDBags: this.enableRIDBags, + logger: this.logger, + max: config.max + }); + this.pool.on('update-config', function (config) { + this.logger.debug('updating config...'); + this.serverCluster = config; + }.bind(this)); + return this; +}; + + + +/** + * Connect to the server. + * + * @promise {BinaryTransport} The connected transport instance. + */ +BinaryTransport.prototype.connect = function () { + if (this.sessionId !== -1) { + return Promise.resolve(this); + } + + if (this.skipServerConnect) { + return Promise.resolve(this).bind(this); + } + + if (this.connecting) { + if (this.connecting.isRejected()) { + return new Promise(function (resolve, reject) { + this.once('reset', function () { + this.connect().then(resolve, reject); + }.bind(this)); + reconnectTransport(this); + }.bind(this)); + } + else { + return this.connecting; + } + } + + this.connecting = (this.pool || this.connection).send('connect', { + username: this.username, + password: this.password, + useToken: this.useToken + }) + .bind(this) + .then(function (response) { + this.logger.debug('got session id: ' + response.sessionId); + this.sessionId = response.sessionId; + this.token = response.token; + this.retries = 0; + return this; + }); + + return this.connecting; +}; + +/** + * Send an operation to the server, + * + * @param {Integer} operation The id of the operation to send. + * @param {Object} options The options for the operation. + * @promise {Mixed} The result of the operation. + */ +BinaryTransport.prototype.send = function (operation, options) { + options = options || {}; + if (!options.token && this.useToken && this.token) { + options.token = this.token; + } + if (~this.sessionId || options.sessionId != null) { + options.sessionId = options.sessionId != null ? options.sessionId : this.sessionId; + return (this.pool || this.connection).send(operation, options); + } + else { + return this.connect() + .then(function (server) { + options.token = this.token; + options.sessionId = options.sessionId != null ? options.sessionId : this.sessionId; + return (server.pool || server.connection).send(operation, options); + }); + } +}; + + +/** + * Close the connection to the server. + * + * @return {BinaryTransport} the disconnected transport instance + */ +BinaryTransport.prototype.close = function () { + this.closing = true; + (this.pool || this.connection).close(); + return this; +}; + + +function reconnectTransport (transport, cancellationError) { + cancellationError = cancellationError || new Error('Connection closed.'); + transport.sessionId = -1; + transport.connecting = false; + transport.connection.removeAllListeners(); + transport.connection.cancel(cancellationError); + transport.connection = false; + transport.configureConnection(); + transport.emit('reset'); +} diff --git a/lib/transport/binary/operation-status.js b/lib/transport/binary/operation-status.js new file mode 100644 index 0000000..c701e67 --- /dev/null +++ b/lib/transport/binary/operation-status.js @@ -0,0 +1,9 @@ +'use strict'; + +exports.PENDING = 0; +exports.WRITTEN = 1; +exports.READING = 2; +exports.COMPLETE = 3; +exports.ERROR = 4; +exports.PUSH_DATA = 5; +exports.LIVE_RESULT = 6; \ No newline at end of file diff --git a/lib/protocol/constants.js b/lib/transport/binary/protocol19/constants.js similarity index 94% rename from lib/protocol/constants.js rename to lib/transport/binary/protocol19/constants.js index d88077e..bde444d 100644 --- a/lib/protocol/constants.js +++ b/lib/transport/binary/protocol19/constants.js @@ -1,3 +1,5 @@ +"use strict"; + exports.PROTOCOL_VERSION = 19; exports.BYTES_LONG = 8; diff --git a/lib/transport/binary/protocol19/deserializer.js b/lib/transport/binary/protocol19/deserializer.js new file mode 100644 index 0000000..2d00933 --- /dev/null +++ b/lib/transport/binary/protocol19/deserializer.js @@ -0,0 +1,554 @@ +"use strict"; + +var RID = require('../../../recordid'), + Bag = require('../../../bag'); + +/** + * Deserialize the given record and return an object containing the values. + * + * @param {String} input The serialized record. + * @param {Object} classes The optional map of class names to transformers. + * @return {Object} The deserialized record. + */ +function deserialize (input, classes) { + var record = {'@type': 'd'}, + chunk, key, value; + if (!input) { + return null; + } + chunk = eatFirstKey(input); + if (chunk[2]) { + // this is actually a class name + record['@class'] = chunk[0]; + input = chunk[1]; + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + } + else { + key = chunk[0]; + input = chunk[1]; + } + // read the first value. + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + + while (input.length) { + if (input.charAt(0) === ',') { + input = input.slice(1); + } + else { + break; + } + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + if (input.length) { + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + } + else { + record[key] = null; + } + } + + if (classes && record['@class'] && classes[record['@class']]) { + return classes[record['@class']](record); + } + else { + return record; + } +} + +/** + * Consume the first field key, which could be a class name. + * + * @param {String} input The input to parse. + * @return {[String, String]} The collected key, and any remaining input. + */ +function eatFirstKey (input) { + var length = input.length, + collected = '', + isClassName = false, + result, c, i; + + if (input.charAt(0) === '"') { + result = eatString(input.slice(1)); + return [result[0], result[1].slice(1)]; + } + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === '@') { + isClassName = true; + break; + } + else if (c === ':') { + break; + } + else { + collected += c; + } + } + return [collected, input.slice(i + 1), isClassName]; +} + + +/** + * Consume a field key, which may or may not be quoted. + * + * @param {String} input The input to parse. + * @return {[String, String]} The collected key, and any remaining input. + */ +function eatKey (input) { + var length = input.length, + collected = '', + result, c, i; + + if (input.charAt(0) === '"') { + result = eatString(input.slice(1)); + return [result[0], result[1].slice(1)]; + } + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === ':') { + break; + } + else { + collected += c; + } + } + + return [collected, input.slice(i + 1)]; +} + + + +/** + * Consume a field value. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Mixed, String]} The collected value, and any remaining input. + */ +function eatValue (input, classes) { + var c, n; + c = input.charAt(0); + while (c === ' ' && input.length) { + input = input.slice(1); + c = input.charAt(0); + } + + if (!input.length || c === ',') { + // this is a null field. + return [null, input]; + } + else if (c === '"') { + return eatString(input.slice(1)); + } + else if (c === '#') { + return eatRID(input.slice(1)); + } + else if (c === '[') { + return eatArray(input.slice(1), classes); + } + else if (c === '<') { + return eatSet(input.slice(1), classes); + } + else if (c === '{') { + return eatMap(input.slice(1), classes); + } + else if (c === '(') { + return eatRecord(input.slice(1), classes); + } + else if (c === '%') { + return eatBag(input.slice(1)); + } + else if (c === '_') { + return eatBinary(input.slice(1)); + } + else if (c === '-' || c === '0' || +c) { + return eatNumber(input); + } + else if (c === 'n' && input.slice(0, 4) === 'null') { + return [null, input.slice(4)]; + } + else if (c === 't' && input.slice(0, 4) === 'true') { + return [true, input.slice(4)]; + } + else if (c === 'f' && input.slice(0, 5) === 'false') { + return [false, input.slice(5)]; + } + else { + return [null, input]; + } +} + +/** + * Consume a string + * + * @param {String} input The input to parse. + * @return {[String, String]} The collected string, and any remaining input. + */ +function eatString (input) { + var length = input.length, + collected = '', + c, i; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === '\\') { + // escape, skip to the next character + i++; + collected += input.charAt(i); + continue; + } + else if (c === '"') { + break; + } + else { + collected += c; + } + } + + return [collected, input.slice(i + 1)]; +} + +/** + * Consume a number. + * + * If the number has a suffix, consume it also and instantiate the right type, e.g. for dates + * + * @param {String} input The input to parse. + * @return {[Mixed, String]} The collected number, and any remaining input. + */ +function eatNumber (input) { + var length = input.length, + collected = '', + pattern = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/, + num, c, i; + + num = input.match(pattern); + if (num) { + collected = num[0]; + i = collected.length; + } + + collected = +collected; + input = input.slice(i); + + c = input.charAt(0); + + if (c === 'a' || c === 't') { + collected = new Date(collected); + input = input.slice(1); + } + else if (c === 'b' || c === 's' || c === 'l' || c === 'f' || c == 'd' || c === 'c') { + input = input.slice(1); + } + + return [collected, input]; +} + +/** + * Consume a Record ID. + * + * @param {String} input The input to parse. + * @return {[RID, String]} The collected record id, and any remaining input. + */ +function eatRID (input) { + var length = input.length, + collected = '', + cluster, c, i; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (cluster === undefined && c === ':') { + cluster = +collected; + collected = ''; + } + else if (c === '-' || c === '0' || +c) { + collected += c; + } + else { + break; + } + } + + return [new RID({cluster: cluster, position: +collected}), input.slice(i)]; +} + + +/** + * Consume an array. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Array, String]} The collected array, and any remaining input. + */ +function eatArray (input, classes) { + var length = input.length, + array = [], + chunk, c; + + while (input.length) { + c = input.charAt(0); + if (c === ',') { + input = input.slice(1); + } + else if (c === ']') { + input = input.slice(1); + break; + } + chunk = eatValue(input, classes); + array.push(chunk[0]); + input = chunk[1]; + } + return [array, input]; +} + + +/** + * Consume a set. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Array, String]} The collected set, and any remaining input. + */ +function eatSet (input, classes) { + var length = input.length, + set = [], + chunk, c; + + while (input.length) { + c = input.charAt(0); + if (c === ',') { + input = input.slice(1); + } + else if (c === '>') { + input = input.slice(1); + break; + } + chunk = eatValue(input, classes); + set.push(chunk[0]); + input = chunk[1]; + } + + return [set, input]; +} + +/** + * Consume a map (object). + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Object, String]} The collected map, and any remaining input. + */ +function eatMap (input, classes) { + var length = input.length, + map = {}, + key, value, chunk, c; + + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + if (c === ',') { + input = input.slice(1); + } + else if (c === '}') { + input = input.slice(1); + break; + } + + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + if (input.length) { + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + map[key] = value; + } + else { + map[key] = null; + } + } + + return [map, input]; +} + +/** + * Consume an embedded record. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Object, String]} The collected record, and any remaining input. + */ +function eatRecord (input, classes) { + var record = {'@type': 'd'}, + chunk, c, key, value; + + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + else if (c === ')') { + // empty record. + input = input.slice(1); + return [record, input]; + } + else { + break; + } + } + + chunk = eatFirstKey(input); + + if (chunk[2]) { + // this is actually a class name + record['@class'] = chunk[0]; + input = chunk[1]; + chunk = eatKey(input); + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + else if (c === ')') { + // empty record. + input = input.slice(1); + return [record, input]; + } + else { + break; + } + } + key = chunk[0]; + input = chunk[1]; + } + else { + key = chunk[0]; + input = chunk[1]; + } + + // read the first value. + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + if (c === ',') { + input = input.slice(1); + } + else if (c === ')') { + input = input.slice(1); + break; + } + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + if (input.length) { + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + } + else { + record[key] = null; + } + } + + if (classes && record['@class'] && classes[record['@class']]) { + record = classes[record['@class']](record); + } + + return [record, input]; +} + +/** + * Consume a RID Bag. + * + * @param {String} input The input to parse. + * @return {[Object, String]} The collected bag, and any remaining input. + */ +function eatBag (input) { + var length = input.length, + collected = '', + i, bag, chunk, c; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === ';') { + break; + } + else { + collected += c; + } + } + input = input.slice(i + 1); + + if (exports.enableRIDBags) { + return [new Bag(collected), input]; + } + else { + return [new Bag(collected).all(), input]; + } +} + + +/** + * Consume a binary buffer. + * + * @param {String} input The input to parse. + * @return {[Object, String]} The collected bag, and any remaining input. + */ +function eatBinary (input) { + var length = input.length, + collected = '', + i, bag, chunk, c; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === '_' || c === ',' || c === ')' || c === '>' || c === '}' || c === ']') { + break; + } + else { + collected += c; + } + } + input = input.slice(i + 1); + + return [new Buffer(collected, 'base64'), input]; +} + + +exports.enableRIDBags = true; +exports.deserialize = deserialize; +exports.eatKey = eatKey; +exports.eatValue = eatValue; +exports.eatString = eatString; +exports.eatNumber = eatNumber; +exports.eatRID = eatRID; +exports.eatArray = eatArray; +exports.eatSet = eatSet; +exports.eatMap = eatMap; +exports.eatRecord = eatRecord; +exports.eatBag = eatBag; +exports.eatBinary = eatBinary; diff --git a/lib/protocol/index.js b/lib/transport/binary/protocol19/index.js similarity index 95% rename from lib/protocol/index.js rename to lib/transport/binary/protocol19/index.js index 753164d..f022070 100644 --- a/lib/protocol/index.js +++ b/lib/transport/binary/protocol19/index.js @@ -1,3 +1,4 @@ +"use strict"; exports.Operation = require('./operation'); exports.OperationQueue = require('./operation-queue'); exports.constants = require('./constants'); diff --git a/lib/protocol/operation-queue.js b/lib/transport/binary/protocol19/operation-queue.js similarity index 93% rename from lib/protocol/operation-queue.js rename to lib/transport/binary/protocol19/operation-queue.js index 94e6757..d29b353 100644 --- a/lib/protocol/operation-queue.js +++ b/lib/transport/binary/protocol19/operation-queue.js @@ -1,8 +1,10 @@ +"use strict"; + var Promise = require('bluebird'), Operation = require('./operation'), operations = require('./operations'), Emitter = require('events').EventEmitter, - errors = require('../errors'), + errors = require('../../../errors'), util = require('util'); function OperationQueue (socket) { @@ -10,7 +12,9 @@ function OperationQueue (socket) { this.items = []; this.writes = []; this.remaining = null; - if (socket) this.bindToSocket(); + if (socket) { + this.bindToSocket(); + } Emitter.call(this); } @@ -59,7 +63,7 @@ OperationQueue.prototype.add = function (op, params) { */ OperationQueue.prototype.cancel = function (err) { var item, op, deferred; - while (item = this.items.shift()) { + while ((item = this.items.shift())) { op = item[0]; deferred = item[1]; deferred.reject(err); @@ -72,10 +76,11 @@ OperationQueue.prototype.cancel = function (err) { */ OperationQueue.prototype.bindToSocket = function (socket) { var total, i; - if (socket) + if (socket) { this.socket = socket; + } this.socket.on('data', this.handleChunk.bind(this)); - if (total = this.writes.length) { + if ((total = this.writes.length)) { if (this.socket.connected) { for (i = 0; i < total; i++) { this.socket.write(this.writes[i]); @@ -138,7 +143,7 @@ OperationQueue.prototype.handleChunk = function (data) { OperationQueue.prototype.process = function (buffer, offset) { var code, parsed, result, status, item, op, deferred, err; offset = offset || 0; - while (item = this.items.shift()) { + while ((item = this.items.shift())) { op = item[0]; deferred = item[1]; parsed = op.consume(buffer, offset); @@ -161,7 +166,7 @@ OperationQueue.prototype.process = function (buffer, offset) { else if (status === Operation.ERROR) { if (result.status.error) { // this is likely a recoverable error - deferred.reject(result.status.error) + deferred.reject(result.status.error); } else { // cannot recover, reject everything and let the application decide what to do @@ -171,8 +176,9 @@ OperationQueue.prototype.process = function (buffer, offset) { this.emit('error', err); } } - else + else { deferred.reject(new errors.Protocol('Unsupported operation status: ' + status)); + } } return offset; }; \ No newline at end of file diff --git a/lib/protocol/operation.js b/lib/transport/binary/protocol19/operation.js similarity index 90% rename from lib/protocol/operation.js rename to lib/transport/binary/protocol19/operation.js index c34b2f9..c109040 100644 --- a/lib/protocol/operation.js +++ b/lib/transport/binary/protocol19/operation.js @@ -1,7 +1,9 @@ +"use strict"; + var constants = require('./constants'), - utils = require('../utils'), - Long = require('./long').Long, - errors = require('../errors'), + utils = require('../../../utils'), + Long = require('../../../long').Long, + errors = require('../../../errors'), deserializer = require('./deserializer'); @@ -28,13 +30,14 @@ module.exports = Operation; // operation statuses +var statuses = require('../operation-status'); -Operation.PENDING = 0; -Operation.WRITTEN = 1; -Operation.READING = 2; -Operation.COMPLETE = 3; -Operation.ERROR = 4; -Operation.PUSH_DATA = 5; +Operation.PENDING = statuses.PENDING; +Operation.WRITTEN = statuses.WRITTEN; +Operation.READING = statuses.READING; +Operation.COMPLETE = statuses.COMPLETE; +Operation.ERROR = statuses.ERROR; +Operation.PUSH_DATA = statuses.PUSH_DATA; // make it easy to inherit from the base class Operation.extend = utils.extend; @@ -62,7 +65,9 @@ Operation.prototype.reader = function () { */ Operation.prototype.buffer = function () { - if (!this.writeOps.length) this.writer(); + if (!this.writeOps.length) { + this.writer(); + } var total = this.writeOps.length, size = 0, @@ -151,8 +156,7 @@ Operation.prototype.writeInt = function (data) { Operation.prototype.writeLong = function (data) { this.writeOps.push([constants.BYTES_LONG, function (buffer, offset) { data = Long.fromNumber(data); - - buffer.fill(0, offset, constants.BYTES_LONG); + buffer.fill(0, offset, offset + constants.BYTES_LONG); buffer.writeInt32BE(data.high_, offset); buffer.writeInt32BE(data.low_, offset + constants.BYTES_INT); @@ -197,15 +201,42 @@ Operation.prototype.writeString = function (data) { if (data == null) { return this.writeInt(-1); } - var length = Buffer.byteLength(data); + var encoded = encodeString(data), + length = encoded.length; this.writeOps.push([constants.BYTES_INT + length, function (buffer, offset) { - buffer.fill(0, offset, offset + constants.BYTES_INT + length); buffer.writeInt32BE(length, offset); - buffer.write(data, offset + constants.BYTES_INT); + encoded.copy(buffer, offset + constants.BYTES_INT); }]); return this; }; +function encodeString (data) { + var length = data.length, + output = new Buffer(length * 3), // worst case, all chars could require 3-byte encodings. + j = 0, // index output + i, c; + + for (i = 0; i < length; i++) { + c = data.charCodeAt(i); + if (c < 0x80) { + // 7-bits done in one byte. + output[j++] = c; + } + else if (c < 0x800) { + // 8-11 bits done in 2 bytes + output[j++] = (0xC0 | c >> 6); + output[j++] = (0x80 | c & 0x3F); + } + else { + // 12-16 bits done in 3 bytes + output[j++] = (0xE0 | c >> 12); + output[j++] = (0x80 | c >> 6 & 0x3F); + output[j++] = (0x80 | c & 0x3F); + } + } + return output.slice(0, j); +} + // # Read Operations @@ -272,7 +303,7 @@ Operation.prototype.readError = function (fieldName, reader) { */ Operation.prototype.readObject = function (fieldName, reader) { this.readOps.push(['String', [fieldName, function (data, fieldName) { - data[fieldName] = deserializer.deserializeValue('{'+data[fieldName]+'}'); + data[fieldName] = deserializer.deserialize(data[fieldName], this.data.transformerFunctions); if (reader) { reader.call(this, data, fieldName); } @@ -349,7 +380,7 @@ Operation.prototype.consume = function (buffer, offset) { if (this.readOps.length === 0) { return [Operation.COMPLETE, offset, this.stack[0]]; } - while (item = this.readOps.shift()) { + while ((item = this.readOps.shift())) { context = this.stack[this.stack.length - 1]; if (typeof item === 'function') { // this is a nop, just execute it. @@ -384,7 +415,9 @@ Operation.prototype.consume = function (buffer, offset) { */ Operation.prototype.canRead = function (type, buffer, offset) { var length = buffer.length; - if (offset > length) return false; + if (offset > length) { + return false; + } switch (type) { case 'Array': case 'Error': @@ -403,14 +436,17 @@ Operation.prototype.canRead = function (type, buffer, offset) { return length >= offset + constants.BYTES_INT; case 'Bytes': case 'String': - if (length <= offset + constants.BYTES_INT) + if (length <= offset + constants.BYTES_INT) { return false; - else + } + else { return length >= offset + constants.BYTES_INT + buffer.readInt32BE(offset); + } + break; default: return false; } -} +}; /** @@ -426,8 +462,9 @@ Operation.prototype.canRead = function (type, buffer, offset) { */ Operation.prototype.parseByte = function (buffer, offset, context, fieldName, reader) { context[fieldName] = buffer.readUInt8(offset); - if (reader) + if (reader) { reader.call(this, context, fieldName); + } return 1; }; @@ -444,8 +481,9 @@ Operation.prototype.parseByte = function (buffer, offset, context, fieldName, re */ Operation.prototype.parseChar = function (buffer, offset, context, fieldName, reader) { context[fieldName] = String.fromCharCode(buffer.readUInt8(offset)); - if (reader) + if (reader) { reader.call(this, context, fieldName); + } return 1; }; @@ -462,8 +500,9 @@ Operation.prototype.parseChar = function (buffer, offset, context, fieldName, re */ Operation.prototype.parseBoolean = function (buffer, offset, context, fieldName, reader) { context[fieldName] = Boolean(buffer.readUInt8(offset)); - if (reader) + if (reader) { reader.call(this, context, fieldName); + } return 1; }; @@ -482,8 +521,9 @@ Operation.prototype.parseBoolean = function (buffer, offset, context, fieldName, Operation.prototype.parseShort = function (buffer, offset, context, fieldName, reader) { context[fieldName] = buffer.readInt16BE(offset); - if (reader) + if (reader) { reader.call(this, context, fieldName); + } return constants.BYTES_SHORT; }; @@ -500,8 +540,9 @@ Operation.prototype.parseShort = function (buffer, offset, context, fieldName, r */ Operation.prototype.parseInt = function (buffer, offset, context, fieldName, reader) { context[fieldName] = buffer.readInt32BE(offset); - if (reader) + if (reader) { reader.call(this, context, fieldName); + } return constants.BYTES_INT; }; @@ -524,8 +565,9 @@ Operation.prototype.parseLong = function (buffer, offset, context, fieldName, re ) .toNumber(); - if (reader) + if (reader) { reader.call(this, context, fieldName); + } return constants.BYTES_LONG; }; @@ -543,12 +585,15 @@ Operation.prototype.parseLong = function (buffer, offset, context, fieldName, re Operation.prototype.parseBytes = function (buffer, offset, context, fieldName, reader) { var length = buffer.readInt32BE(offset); offset += constants.BYTES_INT; - if (length < 0) + if (length < 0) { context[fieldName] = null; - else + } + else { context[fieldName] = buffer.slice(offset, offset + length); - if (reader) + } + if (reader) { reader.call(this, context, fieldName); + } return length > 0 ? length + constants.BYTES_INT : constants.BYTES_INT; }; @@ -567,12 +612,15 @@ Operation.prototype.parseBytes = function (buffer, offset, context, fieldName, r Operation.prototype.parseString = function (buffer, offset, context, fieldName, reader) { var length = buffer.readInt32BE(offset); offset += constants.BYTES_INT; - if (length < 0) + if (length < 0) { context[fieldName] = null; - else + } + else { context[fieldName] = buffer.toString('utf8', offset, offset + length); - if (reader) + } + if (reader) { reader.call(this, context, fieldName); + } return length > 0 ? length + constants.BYTES_INT : constants.BYTES_INT; }; @@ -592,17 +640,20 @@ Operation.prototype.parseRecord = function (buffer, offset, context, fieldName, record = {}; this.readOps = []; this.stack.push(record); - if (Array.isArray(context[fieldName])) + if (Array.isArray(context[fieldName])) { context[fieldName].push(record); - else + } + else { context[fieldName] = record; + } this.readShort('classId', function (record, fieldName) { if (record[fieldName] === -1) { record.value = new errors.Protocol('No class for record, cannot proceed.'); this.stack.pop(); this.readOps.push(function () { - if (reader) + if (reader) { reader.call(this, context, fieldName); + } }); this.readOps.push.apply(this.readOps, remainingOps); return; @@ -611,8 +662,9 @@ Operation.prototype.parseRecord = function (buffer, offset, context, fieldName, record.value = null; this.stack.pop(); this.readOps.push(function () { - if (reader) + if (reader) { reader.call(this, context, fieldName); + } }); this.readOps.push.apply(this.readOps, remainingOps); return; @@ -625,8 +677,9 @@ Operation.prototype.parseRecord = function (buffer, offset, context, fieldName, .readOps.push(function () { this.stack.pop(); this.readOps.push(function () { - if (reader) + if (reader) { reader.call(this, context, fieldName); + } }); this.readOps.push.apply(this.readOps, remainingOps); }); @@ -638,11 +691,12 @@ Operation.prototype.parseRecord = function (buffer, offset, context, fieldName, .readLong('position') .readInt('version') .readString('value', function (data, key) { - data[key] = deserializer.deserializeDocument(data[key]); + data[key] = deserializer.deserialize(data[key], this.data.transformerFunctions); this.stack.pop(); this.readOps.push(function () { - if (reader) + if (reader) { reader.call(this, context, fieldName); + } }); this.readOps.push.apply(this.readOps, remainingOps); }); @@ -684,7 +738,7 @@ Operation.prototype.parseCollection = function (buffer, offset, context, fieldNa } this.readOps.push.apply(this.readOps, remainingOps); return 4; -} +}; /** @@ -752,7 +806,6 @@ Operation.prototype.parseArray = function (buffer, offset, context, fieldName, r Operation.prototype.parseError = function (buffer, offset, context, fieldName, reader) { var err = new errors.Request(); err.previous = []; - // remove any ops we were expecting to run. this.readOps = []; @@ -768,25 +821,28 @@ Operation.prototype.parseError = function (buffer, offset, context, fieldName, r var prev; if (data.hasMore) { prev = new errors.Request(); + prev.type = data.type; + prev.message = data.message; err.previous.push(prev); + this.stack.pop(); this.stack.push(prev); readItem.call(this); - this.readOps.push(function () { - this.stack.pop(); - }); } else { + err.type = data.type; + err.message = data.message; this.readBytes('javaStackTrace', function (data) { this.readOps.push(function (data) { this.stack.pop(); }); }); } - }) + }); } readItem.call(this); - if (reader) + if (reader) { reader.call(this, context, fieldName); + } return 0; }; @@ -810,13 +866,14 @@ Operation.prototype.parsePushedData = function (buffer, offset, context, fieldNa asString = buffer.toString('utf8', offset, offset + length); switch (asString.charAt(0)) { case 'R': - context[fieldName] = deserializer.deserializeValue('{' + asString.slice(1) + '}'); + context[fieldName] = deserializer.deserialize(asString.slice(1), this.data.transformerFunctions); break; default: console.log('unsupported pushed data format: ' + asString); } - if (reader) + if (reader) { reader.call(this, context, fieldName); + } return length + constants.BYTES_INT; }; diff --git a/lib/transport/binary/protocol19/operations/command.js b/lib/transport/binary/protocol19/operations/command.js new file mode 100644 index 0000000..624c8a9 --- /dev/null +++ b/lib/transport/binary/protocol19/operations/command.js @@ -0,0 +1,166 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + serializer = require('../serializer'), + writer = require('../writer'), + RID = require('../../../../recordid'), + utils = require('../../../../utils'); + +module.exports = Operation.extend({ + id: 'REQUEST_COMMAND', + opCode: 41, + writer: function () { + if (this.data.mode === 'a' && !this.data.class) { + this.data.class = 'com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery'; + } + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1) + .writeChar(this.data.mode || 's') + .writeBytes(this.serializeQuery()); + + }, + serializeQuery: function () { + var buffers = [writer.writeString(this.data.class)]; + + var text = this.data.query; + // if there are bound parameters, force prepare them, OrientDB's support is limited in this version. + if (this.data.params && this.data.params.params) { + text = utils.prepare(text, this.data.params.params); + } + + if (this.data.class === 'q' || + this.data.class === 'com.orientechnologies.orient.core.sql.query.OSQLSynchQuery' || + this.data.class === 'com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery') { + buffers.push( + writer.writeString(text), + writer.writeInt(this.data.limit), + writer.writeString(this.data.fetchPlan || '') + ); + + buffers.push(writer.writeInt(0)); + } + else if ( + this.data.class === 's' || + this.data.class === 'com.orientechnologies.orient.core.command.script.OCommandScript') { + buffers.push( + writer.writeString(this.data.language || 'sql'), + writer.writeString(text) + ); + buffers.push(writer.writeBoolean(false)); + buffers.push(writer.writeByte(0)); + } + else { + buffers.push(writer.writeString(text)); + buffers.push(writer.writeBoolean(false)); + buffers.push(writer.writeBoolean(false)); + } + return Buffer.concat(buffers); + }, + reader: function () { + this + .readStatus('status') + .readCommandResult('results'); + }, + readCommandResult: function (fieldName, reader) { + this.payloads = []; + this.readOps.push(function (data) { + data[fieldName] = this.payloads; + this.stack.push(data[fieldName]); + this.readPayload('payloadStatus', function () { + this.stack.pop(); + }); + }); + return this; + }, + readPayload: function (payloadFieldName, reader) { + + return this.readByte(payloadFieldName, function (data, fieldName) { + var record = {}; + switch (data[fieldName]) { + case 0: + if (reader) { + reader.call(this); + } + break; + case 110: // null + record.type = 'r'; + record.content = null; + this.payloads.push(record); + this.readPayload(payloadFieldName, reader); + break; + case 1: + case 114: + // a record + record.type = 'r'; + this.payloads.push(record); + this.stack.push(record); + this.readRecord('content', function () { + this.stack.pop(); + this.readPayload(payloadFieldName, reader); + }); + break; + case 2: + // prefeteched record + record.type = 'p'; + this.payloads.push(record); + this.stack.push(record); + this.readRecord('content', function (data) { + this.stack.pop(); + this.readPayload(payloadFieldName, reader); + }); + break; + case 97: + // serialized result + record.type = 'f'; + this.payloads.push(record); + this.stack.push(record); + this.readString('content', function () { + this.stack.pop(); + this.readPayload(payloadFieldName, reader); + }); + break; + case 108: + // collection of records + record.type = 'l'; + this.payloads.push(record); + this.stack.push(record); + this.readCollection('content', function (data) { + this.stack.pop(); + this.readPayload(payloadFieldName, reader); + }); + break; + default: + reader.call(this); + } + }); + } +}); + +/** + * Serialize the parameters for a query. + * + * > Note: There is a bug in OrientDB where special kinds of string values + * > need to be twice quoted *in parameters*. Hence the need for this specialist function. + * + * @param {Object} data The data to serialize. + * @return {String} The serialized data. + */ +function serializeParams (data) { + var keys = Object.keys(data.params || {}), + total = keys.length, + c, i, key, value; + + for (i = 0; i < total; i++) { + key = keys[i]; + value = data.params[key]; + if (typeof value === 'string') { + c = value.charAt(0); + if (c === '.' || c === '#' || c === '<' || c === '[' || c === '(' || c === '{' || c === '0' || +c) { + data.params[key] = '"' + value + '"'; + } + } + } + return serializer.serializeDocument(data); +} \ No newline at end of file diff --git a/lib/protocol/operations/config-get.js b/lib/transport/binary/protocol19/operations/config-get.js similarity index 96% rename from lib/protocol/operations/config-get.js rename to lib/transport/binary/protocol19/operations/config-get.js index e99fb33..a5e3b74 100644 --- a/lib/protocol/operations/config-get.js +++ b/lib/transport/binary/protocol19/operations/config-get.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/config-list.js b/lib/transport/binary/protocol19/operations/config-list.js similarity index 97% rename from lib/protocol/operations/config-list.js rename to lib/transport/binary/protocol19/operations/config-list.js index c898aab..e4cab31 100644 --- a/lib/protocol/operations/config-list.js +++ b/lib/transport/binary/protocol19/operations/config-list.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/config-set.js b/lib/transport/binary/protocol19/operations/config-set.js similarity index 96% rename from lib/protocol/operations/config-set.js rename to lib/transport/binary/protocol19/operations/config-set.js index 5ad4ec4..686dcca 100644 --- a/lib/protocol/operations/config-set.js +++ b/lib/transport/binary/protocol19/operations/config-set.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/connect.js b/lib/transport/binary/protocol19/operations/connect.js similarity index 88% rename from lib/protocol/operations/connect.js rename to lib/transport/binary/protocol19/operations/connect.js index 6e9edf0..73f98a0 100644 --- a/lib/protocol/operations/connect.js +++ b/lib/transport/binary/protocol19/operations/connect.js @@ -1,6 +1,8 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'), - npmPackage = require('../../../package.json'); + npmPackage = require('../../../../../package.json'); module.exports = Operation.extend({ id: 'REQUEST_CONNECT', diff --git a/lib/protocol/operations/datacluster-add.js b/lib/transport/binary/protocol19/operations/datacluster-add.js similarity index 97% rename from lib/protocol/operations/datacluster-add.js rename to lib/transport/binary/protocol19/operations/datacluster-add.js index 0cb7d74..bbff494 100644 --- a/lib/protocol/operations/datacluster-add.js +++ b/lib/transport/binary/protocol19/operations/datacluster-add.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/datacluster-count.js b/lib/transport/binary/protocol19/operations/datacluster-count.js similarity index 97% rename from lib/protocol/operations/datacluster-count.js rename to lib/transport/binary/protocol19/operations/datacluster-count.js index 6aa7a87..2859a63 100644 --- a/lib/protocol/operations/datacluster-count.js +++ b/lib/transport/binary/protocol19/operations/datacluster-count.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/datacluster-datarange.js b/lib/transport/binary/protocol19/operations/datacluster-datarange.js similarity index 96% rename from lib/protocol/operations/datacluster-datarange.js rename to lib/transport/binary/protocol19/operations/datacluster-datarange.js index 59daf65..9caae51 100644 --- a/lib/protocol/operations/datacluster-datarange.js +++ b/lib/transport/binary/protocol19/operations/datacluster-datarange.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/datacluster-drop.js b/lib/transport/binary/protocol19/operations/datacluster-drop.js similarity index 96% rename from lib/protocol/operations/datacluster-drop.js rename to lib/transport/binary/protocol19/operations/datacluster-drop.js index aaea905..89b3ea5 100644 --- a/lib/protocol/operations/datacluster-drop.js +++ b/lib/transport/binary/protocol19/operations/datacluster-drop.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/datasegment-add.js b/lib/transport/binary/protocol19/operations/datasegment-add.js similarity index 92% rename from lib/protocol/operations/datasegment-add.js rename to lib/transport/binary/protocol19/operations/datasegment-add.js index 7e2819e..371f158 100644 --- a/lib/protocol/operations/datasegment-add.js +++ b/lib/transport/binary/protocol19/operations/datasegment-add.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); @@ -16,6 +18,6 @@ module.exports = Operation.extend({ reader: function () { this .readStatus('status') - .readInt('id') + .readInt('id'); } }); \ No newline at end of file diff --git a/lib/protocol/operations/datasegment-drop.js b/lib/transport/binary/protocol19/operations/datasegment-drop.js similarity index 96% rename from lib/protocol/operations/datasegment-drop.js rename to lib/transport/binary/protocol19/operations/datasegment-drop.js index 4037c51..396eba7 100644 --- a/lib/protocol/operations/datasegment-drop.js +++ b/lib/transport/binary/protocol19/operations/datasegment-drop.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/db-close.js b/lib/transport/binary/protocol19/operations/db-close.js similarity index 73% rename from lib/protocol/operations/db-close.js rename to lib/transport/binary/protocol19/operations/db-close.js index e874f5d..ba291e8 100644 --- a/lib/protocol/operations/db-close.js +++ b/lib/transport/binary/protocol19/operations/db-close.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); @@ -7,8 +9,7 @@ module.exports = Operation.extend({ writer: function () { this .writeByte(this.opCode) - .writeInt(this.data.sessionId || -1) + .writeInt(this.data.sessionId || -1); }, - reader: function () { - } + reader: function () {} }); \ No newline at end of file diff --git a/lib/protocol/operations/db-countrecords.js b/lib/transport/binary/protocol19/operations/db-countrecords.js similarity index 86% rename from lib/protocol/operations/db-countrecords.js rename to lib/transport/binary/protocol19/operations/db-countrecords.js index dfbd726..a290a89 100644 --- a/lib/protocol/operations/db-countrecords.js +++ b/lib/transport/binary/protocol19/operations/db-countrecords.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); @@ -7,7 +9,7 @@ module.exports = Operation.extend({ writer: function () { this .writeByte(this.opCode) - .writeInt(this.data.sessionId) + .writeInt(this.data.sessionId); }, reader: function () { this diff --git a/lib/protocol/operations/db-create.js b/lib/transport/binary/protocol19/operations/db-create.js similarity index 96% rename from lib/protocol/operations/db-create.js rename to lib/transport/binary/protocol19/operations/db-create.js index 7ac02b9..e99b764 100644 --- a/lib/protocol/operations/db-create.js +++ b/lib/transport/binary/protocol19/operations/db-create.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/db-delete.js b/lib/transport/binary/protocol19/operations/db-delete.js similarity index 96% rename from lib/protocol/operations/db-delete.js rename to lib/transport/binary/protocol19/operations/db-delete.js index c53e69b..04e5b58 100644 --- a/lib/protocol/operations/db-delete.js +++ b/lib/transport/binary/protocol19/operations/db-delete.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/protocol/operations/db-exists.js b/lib/transport/binary/protocol19/operations/db-exists.js similarity index 97% rename from lib/protocol/operations/db-exists.js rename to lib/transport/binary/protocol19/operations/db-exists.js index cba9d8d..29c1e40 100644 --- a/lib/protocol/operations/db-exists.js +++ b/lib/transport/binary/protocol19/operations/db-exists.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); diff --git a/lib/transport/binary/protocol19/operations/db-freeze.js b/lib/transport/binary/protocol19/operations/db-freeze.js new file mode 100644 index 0000000..6c9cba4 --- /dev/null +++ b/lib/transport/binary/protocol19/operations/db-freeze.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_FREEZE', + opCode: 94, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeString(this.data.name) + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); diff --git a/lib/protocol/operations/db-list.js b/lib/transport/binary/protocol19/operations/db-list.js similarity index 85% rename from lib/protocol/operations/db-list.js rename to lib/transport/binary/protocol19/operations/db-list.js index 833d58a..8262b5e 100644 --- a/lib/protocol/operations/db-list.js +++ b/lib/transport/binary/protocol19/operations/db-list.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); @@ -7,13 +9,13 @@ module.exports = Operation.extend({ writer: function () { this .writeByte(this.opCode) - .writeInt(this.data.sessionId || -1) + .writeInt(this.data.sessionId || -1); }, reader: function () { this .readStatus('status') .readObject('databases', function (data, fieldName) { data[fieldName] = data[fieldName].databases; - }) + }); } }); \ No newline at end of file diff --git a/lib/protocol/operations/db-open.js b/lib/transport/binary/protocol19/operations/db-open.js similarity index 94% rename from lib/protocol/operations/db-open.js rename to lib/transport/binary/protocol19/operations/db-open.js index 7c144ae..e595de3 100644 --- a/lib/protocol/operations/db-open.js +++ b/lib/transport/binary/protocol19/operations/db-open.js @@ -1,6 +1,7 @@ +"use strict"; var Operation = require('../operation'), constants = require('../constants'), - npmPackage = require('../../../package.json'); + npmPackage = require('../../../../../package.json'); module.exports = Operation.extend({ id: 'REQUEST_DB_OPEN', diff --git a/lib/transport/binary/protocol19/operations/db-release.js b/lib/transport/binary/protocol19/operations/db-release.js new file mode 100644 index 0000000..daeada5 --- /dev/null +++ b/lib/transport/binary/protocol19/operations/db-release.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_RELEASE', + opCode: 95, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeString(this.data.name) + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); diff --git a/lib/protocol/operations/db-reload.js b/lib/transport/binary/protocol19/operations/db-reload.js similarity index 92% rename from lib/protocol/operations/db-reload.js rename to lib/transport/binary/protocol19/operations/db-reload.js index 1aed6d5..64561f2 100644 --- a/lib/protocol/operations/db-reload.js +++ b/lib/transport/binary/protocol19/operations/db-reload.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); @@ -7,7 +9,7 @@ module.exports = Operation.extend({ writer: function () { this .writeByte(this.opCode) - .writeInt(this.data.sessionId || -1) + .writeInt(this.data.sessionId || -1); }, reader: function () { this diff --git a/lib/protocol/operations/db-size.js b/lib/transport/binary/protocol19/operations/db-size.js similarity index 85% rename from lib/protocol/operations/db-size.js rename to lib/transport/binary/protocol19/operations/db-size.js index d064b04..e83b6b3 100644 --- a/lib/protocol/operations/db-size.js +++ b/lib/transport/binary/protocol19/operations/db-size.js @@ -1,3 +1,5 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'); @@ -7,7 +9,7 @@ module.exports = Operation.extend({ writer: function () { this .writeByte(this.opCode) - .writeInt(this.data.sessionId) + .writeInt(this.data.sessionId); }, reader: function () { this diff --git a/lib/protocol/operations/index.js b/lib/transport/binary/protocol19/operations/index.js similarity index 85% rename from lib/protocol/operations/index.js rename to lib/transport/binary/protocol19/operations/index.js index 0166eb4..a980948 100644 --- a/lib/protocol/operations/index.js +++ b/lib/transport/binary/protocol19/operations/index.js @@ -1,3 +1,5 @@ +"use strict"; /*jshint sub:true*/ + exports['connect'] = require('./connect'); exports['db-open'] = require('./db-open'); exports['db-create'] = require('./db-create'); @@ -7,6 +9,8 @@ exports['db-size'] = require('./db-size'); exports['db-countrecords'] = require('./db-countrecords'); exports['db-reload'] = require('./db-reload'); exports['db-list'] = require('./db-list'); +exports['db-freeze'] = require('./db-freeze'); +exports['db-release'] = require('./db-release'); exports['db-close'] = require('./db-close'); @@ -27,7 +31,8 @@ exports['record-delete'] = require('./record-delete'); exports['record-clean-out'] = require('./record-clean-out'); exports['command'] = require('./command'); +exports['tx-commit'] = require('./tx-commit'); exports['config-list'] = require('./config-list'); exports['config-get'] = require('./config-get'); -exports['config-set'] = require('./config-set'); \ No newline at end of file +exports['config-set'] = require('./config-set'); diff --git a/lib/protocol/operations/record-clean-out.js b/lib/transport/binary/protocol19/operations/record-clean-out.js similarity index 90% rename from lib/protocol/operations/record-clean-out.js rename to lib/transport/binary/protocol19/operations/record-clean-out.js index f4871a2..a140348 100644 --- a/lib/protocol/operations/record-clean-out.js +++ b/lib/transport/binary/protocol19/operations/record-clean-out.js @@ -1,6 +1,8 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'), - RID = require('../../recordid'), + RID = require('../../../../recordid'), serializer = require('../serializer'); module.exports = Operation.extend({ @@ -28,6 +30,6 @@ module.exports = Operation.extend({ reader: function () { this .readStatus('status') - .readBoolean('success') + .readBoolean('success'); } }); \ No newline at end of file diff --git a/lib/protocol/operations/record-create.js b/lib/transport/binary/protocol19/operations/record-create.js similarity index 93% rename from lib/protocol/operations/record-create.js rename to lib/transport/binary/protocol19/operations/record-create.js index 4f73123..ea83566 100644 --- a/lib/protocol/operations/record-create.js +++ b/lib/transport/binary/protocol19/operations/record-create.js @@ -1,6 +1,8 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'), - RID = require('../../recordid'), + RID = require('../../../../recordid'), serializer = require('../serializer'); module.exports = Operation.extend({ diff --git a/lib/protocol/operations/record-delete.js b/lib/transport/binary/protocol19/operations/record-delete.js similarity index 95% rename from lib/protocol/operations/record-delete.js rename to lib/transport/binary/protocol19/operations/record-delete.js index 168c5d5..a246628 100644 --- a/lib/protocol/operations/record-delete.js +++ b/lib/transport/binary/protocol19/operations/record-delete.js @@ -1,6 +1,8 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'), - RID = require('../../recordid'), + RID = require('../../../../recordid'), serializer = require('../serializer'); module.exports = Operation.extend({ diff --git a/lib/protocol/operations/record-load.js b/lib/transport/binary/protocol19/operations/record-load.js similarity index 90% rename from lib/protocol/operations/record-load.js rename to lib/transport/binary/protocol19/operations/record-load.js index 6e9241f..6c2134f 100644 --- a/lib/protocol/operations/record-load.js +++ b/lib/transport/binary/protocol19/operations/record-load.js @@ -1,9 +1,11 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'), - RID = require('../../recordid'), + RID = require('../../../../recordid'), serializer = require('../serializer'), deserializer = require('../deserializer'), - errors = require('../../errors'); + errors = require('../../../../errors'); module.exports = Operation.extend({ id: 'REQUEST_RECORD_LOAD', @@ -76,11 +78,11 @@ module.exports = Operation.extend({ data.cluster = this.data.cluster; data.position = this.data.position; if (data[fieldName] === 'd') { - data.content = deserializer.deserializeValue('{' + data.content + '}'); + data.content = deserializer.deserialize(data.content, this.data.transformerFunctions); } this.stack.pop(); this.readPayload(records, ender); - }) + }); break; case 2: // a sub record @@ -94,7 +96,6 @@ module.exports = Operation.extend({ break; case -3: throw new errors.Protocol('ClassID ' + data[fieldName] + ' is not supported.'); - break; default: this .readChar('type') @@ -103,13 +104,13 @@ module.exports = Operation.extend({ .readInt('version') .readString('content', function (data, fieldName) { if (data.type === 'd') { - data.content = deserializer.deserializeValue('{' + data.content + '}'); + data.content = deserializer.deserialize(data.content, this.data.transformerFunctions); } this.stack.pop(); this.readPayload(records, ender); }); } - }) + }); break; default: this.readPayload(records, ender); diff --git a/lib/protocol/operations/record-metadata.js b/lib/transport/binary/protocol19/operations/record-metadata.js similarity index 93% rename from lib/protocol/operations/record-metadata.js rename to lib/transport/binary/protocol19/operations/record-metadata.js index f111b8a..2d0ccb3 100644 --- a/lib/protocol/operations/record-metadata.js +++ b/lib/transport/binary/protocol19/operations/record-metadata.js @@ -1,6 +1,8 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'), - RID = require('../../recordid'), + RID = require('../../../../recordid'), serializer = require('../serializer'); module.exports = Operation.extend({ diff --git a/lib/protocol/operations/record-update.js b/lib/transport/binary/protocol19/operations/record-update.js similarity index 95% rename from lib/protocol/operations/record-update.js rename to lib/transport/binary/protocol19/operations/record-update.js index 493efe9..c128025 100644 --- a/lib/protocol/operations/record-update.js +++ b/lib/transport/binary/protocol19/operations/record-update.js @@ -1,6 +1,8 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'), - RID = require('../../recordid'), + RID = require('../../../../recordid'), serializer = require('../serializer'); module.exports = Operation.extend({ diff --git a/lib/transport/binary/protocol19/operations/tx-commit.js b/lib/transport/binary/protocol19/operations/tx-commit.js new file mode 100644 index 0000000..63fda65 --- /dev/null +++ b/lib/transport/binary/protocol19/operations/tx-commit.js @@ -0,0 +1,114 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_TX_COMMIT', + opCode: 60, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1) + .writeInt(this.data.txId) + .writeByte(this.data.txLog); // use transaction log + + + // creates + var total = this.data.creates.length, + item, i; + + for (i = 0; i < total; i++) { + item = this.data.creates[i]; + this.writeByte(1); // mark the start of an entry. + this.writeByte(3); // create. + this.writeShort(item['@rid'].cluster); + this.writeLong(item['@rid'].position); + this.writeByte(constants.RECORD_TYPES[item['@type'] || 'd'] || 100); // document by default + this.writeBytes(serializer.encodeRecordData(item)); + } + + // updates + total = this.data.updates.length; + + for (i = 0; i < total; i++) { + item = this.data.updates[i]; + this.writeByte(1); // mark the start of an entry. + this.writeByte(1); // update. + this.writeShort(item['@rid'].cluster); + this.writeLong(item['@rid'].position); + this.writeByte(constants.RECORD_TYPES[item['@type'] || 'd'] || 100); // document by default + this.writeInt(item['@version'] || 0); + this.writeBytes(serializer.encodeRecordData(item)); + } + + // deletes + total = this.data.deletes.length; + + for (i = 0; i < total; i++) { + item = this.data.deletes[i]; + this.writeByte(1); // mark the start of an entry. + this.writeByte(2); // delete + this.writeShort(item['@rid'].cluster); + this.writeLong(item['@rid'].position); + this.writeByte(constants.RECORD_TYPES[item['@type'] || 'd'] || 100); // document by default + this.writeInt(item['@version'] || 0); + } + this.writeByte(0); // no more documents + this.writeString(''); + }, + reader: function () { + this + .readStatus('status') + .readInt('totalCreated') + .readArray('created', function (data) { + var items = [], + i; + for (i = 0; i < data.totalCreated; i++) { + items.push(function () { + this + .readShort('tmpCluster') + .readLong('tmpPosition') + .readShort('cluster') + .readLong('position'); + }); + } + return items; + }) + .readInt('totalUpdated') + .readArray('updated', function (data) { + var items = [], + i; + for (i = 0; i < data.totalUpdated; i++) { + items.push(function () { + this + .readShort('cluster') + .readLong('position') + .readInt('version'); + }); + } + return items; + }); + + if (this.data.storageType !== 'memory') { + this.readInt('totalChanges') + .readArray('changes', function (data) { + var items = [], + i; + for (i = 0; i < data.totalChanges; i++) { + items.push(function () { + this + .readLong('uuidHigh') + .readLong('uuidLow') + .readLong('fileId') + .readLong('pageIndex') + .readInt('pageOffset'); + }); + } + return items; + }); + } + } +}); \ No newline at end of file diff --git a/lib/protocol/serializer.js b/lib/transport/binary/protocol19/serializer.js similarity index 80% rename from lib/protocol/serializer.js rename to lib/transport/binary/protocol19/serializer.js index 131136f..1bb1d3e 100644 --- a/lib/protocol/serializer.js +++ b/lib/transport/binary/protocol19/serializer.js @@ -1,4 +1,6 @@ -var RecordID = require('../recordid'); +"use strict"; + +var RecordID = require('../../../recordid'); /** * Serialize a record and return it as a buffer. @@ -53,7 +55,7 @@ function serializeDocument (document, isMap) { } return result; -}; +} /** * Serialize a given value according to its type. @@ -61,16 +63,14 @@ function serializeDocument (document, isMap) { * @return {String} The serialized value. */ function serializeValue (value) { - if (isMD5(value)) { - return '\"\"' + value.replace(/\\/, "\\\\").replace(/"/g, "\\\"") + "\"\""; + var type = typeof value; + if (type === 'string') { + return '"' + value.replace(/\\/, "\\\\").replace(/"/g, '\\"') + '"'; } - else if (''+value === value) { - return '\"' + value.replace(/\\/, "\\\\").replace(/"/g, "\\\"") + "\""; - } - else if (+value === value) { + else if (type === 'number') { return ~value.toString().indexOf('.') ? value + 'f' : value; } - else if (value === true || value === false) { + else if (type === 'boolean') { return value ? true : false; } else if (Object.prototype.toString.call(value) === '[object Date]') { @@ -82,9 +82,10 @@ function serializeValue (value) { else if (value === Object(value)) { return serializeObject(value); } - else + else { return ''; -}; + } +} /** @@ -115,27 +116,20 @@ function serializeArray (value) { * @return {String} The serialized value. */ function serializeObject (value) { - if (value instanceof RecordID) + if (value instanceof RecordID) { return value.toString(); - else if (value['@type'] === 'd') + } + else if (value['@type'] === 'd') { return '(' + serializeDocument(value, false) + ')'; - else + } + else { return '{' + serializeDocument(value, true) + '}'; -}; - - - -/** - * Determine whether the given value is a valid MD5 hash. - * @param {String} value The value to check - * @return {Boolean} true if the value is a valid md5, otherwise false. - */ -function isMD5 (value) { - return /^[0-9a-f]{32}$/i.test(value); + } } + // export the public methods exports.serializeDocument = serializeDocument; diff --git a/lib/protocol/writer.js b/lib/transport/binary/protocol19/writer.js similarity index 74% rename from lib/protocol/writer.js rename to lib/transport/binary/protocol19/writer.js index 40c57ef..5c21cd2 100644 --- a/lib/protocol/writer.js +++ b/lib/transport/binary/protocol19/writer.js @@ -1,4 +1,6 @@ -var Long = require('./long').Long, +"use strict"; + +var Long = require('../../../long').Long, constants = require('./constants'); /** @@ -77,7 +79,7 @@ function writeString (data) { if (data === null) { return writeInt(-1); } - var stringBuf = new Buffer(data), + var stringBuf = encodeString(data), length = stringBuf.length, buf = new Buffer(constants.BYTES_INT + length); buf.writeInt32BE(length, 0); @@ -85,7 +87,32 @@ function writeString (data) { return buf; } +function encodeString (data) { + var length = data.length, + output = new Buffer(length * 3), // worst case, all chars could require 3-byte encodings. + j = 0, // index output + i, c; + for (i = 0; i < length; i++) { + c = data.charCodeAt(i); + if (c < 0x80) { + // 7-bits done in one byte. + output[j++] = c; + } + else if (c < 0x800) { + // 8-11 bits done in 2 bytes + output[j++] = (0xC0 | c >> 6); + output[j++] = (0x80 | c & 0x3F); + } + else { + // 12-16 bits done in 3 bytes + output[j++] = (0xE0 | c >> 12); + output[j++] = (0x80 | c >> 6 & 0x3F); + output[j++] = (0x80 | c & 0x3F); + } + } + return output.slice(0, j); +} exports.writeByte = writeByte; exports.writeBoolean = writeByte; diff --git a/lib/transport/binary/protocol26/constants.js b/lib/transport/binary/protocol26/constants.js new file mode 100644 index 0000000..00440c5 --- /dev/null +++ b/lib/transport/binary/protocol26/constants.js @@ -0,0 +1,20 @@ +"use strict"; + +exports.PROTOCOL_VERSION = 26; + +exports.BYTES_LONG = 8; +exports.BYTES_INT = 4; +exports.BYTES_SHORT = 2; +exports.BYTES_BYTE = 1; + +exports.RECORD_TYPES = { + 'd': 100, + 'b': 98, + 'f': 102, + + // duplicated as upper case for fast lookup + + 'D': 100, + 'B': 98, + 'F': 102, +}; \ No newline at end of file diff --git a/lib/transport/binary/protocol26/deserializer.js b/lib/transport/binary/protocol26/deserializer.js new file mode 100644 index 0000000..2d00933 --- /dev/null +++ b/lib/transport/binary/protocol26/deserializer.js @@ -0,0 +1,554 @@ +"use strict"; + +var RID = require('../../../recordid'), + Bag = require('../../../bag'); + +/** + * Deserialize the given record and return an object containing the values. + * + * @param {String} input The serialized record. + * @param {Object} classes The optional map of class names to transformers. + * @return {Object} The deserialized record. + */ +function deserialize (input, classes) { + var record = {'@type': 'd'}, + chunk, key, value; + if (!input) { + return null; + } + chunk = eatFirstKey(input); + if (chunk[2]) { + // this is actually a class name + record['@class'] = chunk[0]; + input = chunk[1]; + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + } + else { + key = chunk[0]; + input = chunk[1]; + } + // read the first value. + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + + while (input.length) { + if (input.charAt(0) === ',') { + input = input.slice(1); + } + else { + break; + } + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + if (input.length) { + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + } + else { + record[key] = null; + } + } + + if (classes && record['@class'] && classes[record['@class']]) { + return classes[record['@class']](record); + } + else { + return record; + } +} + +/** + * Consume the first field key, which could be a class name. + * + * @param {String} input The input to parse. + * @return {[String, String]} The collected key, and any remaining input. + */ +function eatFirstKey (input) { + var length = input.length, + collected = '', + isClassName = false, + result, c, i; + + if (input.charAt(0) === '"') { + result = eatString(input.slice(1)); + return [result[0], result[1].slice(1)]; + } + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === '@') { + isClassName = true; + break; + } + else if (c === ':') { + break; + } + else { + collected += c; + } + } + return [collected, input.slice(i + 1), isClassName]; +} + + +/** + * Consume a field key, which may or may not be quoted. + * + * @param {String} input The input to parse. + * @return {[String, String]} The collected key, and any remaining input. + */ +function eatKey (input) { + var length = input.length, + collected = '', + result, c, i; + + if (input.charAt(0) === '"') { + result = eatString(input.slice(1)); + return [result[0], result[1].slice(1)]; + } + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === ':') { + break; + } + else { + collected += c; + } + } + + return [collected, input.slice(i + 1)]; +} + + + +/** + * Consume a field value. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Mixed, String]} The collected value, and any remaining input. + */ +function eatValue (input, classes) { + var c, n; + c = input.charAt(0); + while (c === ' ' && input.length) { + input = input.slice(1); + c = input.charAt(0); + } + + if (!input.length || c === ',') { + // this is a null field. + return [null, input]; + } + else if (c === '"') { + return eatString(input.slice(1)); + } + else if (c === '#') { + return eatRID(input.slice(1)); + } + else if (c === '[') { + return eatArray(input.slice(1), classes); + } + else if (c === '<') { + return eatSet(input.slice(1), classes); + } + else if (c === '{') { + return eatMap(input.slice(1), classes); + } + else if (c === '(') { + return eatRecord(input.slice(1), classes); + } + else if (c === '%') { + return eatBag(input.slice(1)); + } + else if (c === '_') { + return eatBinary(input.slice(1)); + } + else if (c === '-' || c === '0' || +c) { + return eatNumber(input); + } + else if (c === 'n' && input.slice(0, 4) === 'null') { + return [null, input.slice(4)]; + } + else if (c === 't' && input.slice(0, 4) === 'true') { + return [true, input.slice(4)]; + } + else if (c === 'f' && input.slice(0, 5) === 'false') { + return [false, input.slice(5)]; + } + else { + return [null, input]; + } +} + +/** + * Consume a string + * + * @param {String} input The input to parse. + * @return {[String, String]} The collected string, and any remaining input. + */ +function eatString (input) { + var length = input.length, + collected = '', + c, i; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === '\\') { + // escape, skip to the next character + i++; + collected += input.charAt(i); + continue; + } + else if (c === '"') { + break; + } + else { + collected += c; + } + } + + return [collected, input.slice(i + 1)]; +} + +/** + * Consume a number. + * + * If the number has a suffix, consume it also and instantiate the right type, e.g. for dates + * + * @param {String} input The input to parse. + * @return {[Mixed, String]} The collected number, and any remaining input. + */ +function eatNumber (input) { + var length = input.length, + collected = '', + pattern = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/, + num, c, i; + + num = input.match(pattern); + if (num) { + collected = num[0]; + i = collected.length; + } + + collected = +collected; + input = input.slice(i); + + c = input.charAt(0); + + if (c === 'a' || c === 't') { + collected = new Date(collected); + input = input.slice(1); + } + else if (c === 'b' || c === 's' || c === 'l' || c === 'f' || c == 'd' || c === 'c') { + input = input.slice(1); + } + + return [collected, input]; +} + +/** + * Consume a Record ID. + * + * @param {String} input The input to parse. + * @return {[RID, String]} The collected record id, and any remaining input. + */ +function eatRID (input) { + var length = input.length, + collected = '', + cluster, c, i; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (cluster === undefined && c === ':') { + cluster = +collected; + collected = ''; + } + else if (c === '-' || c === '0' || +c) { + collected += c; + } + else { + break; + } + } + + return [new RID({cluster: cluster, position: +collected}), input.slice(i)]; +} + + +/** + * Consume an array. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Array, String]} The collected array, and any remaining input. + */ +function eatArray (input, classes) { + var length = input.length, + array = [], + chunk, c; + + while (input.length) { + c = input.charAt(0); + if (c === ',') { + input = input.slice(1); + } + else if (c === ']') { + input = input.slice(1); + break; + } + chunk = eatValue(input, classes); + array.push(chunk[0]); + input = chunk[1]; + } + return [array, input]; +} + + +/** + * Consume a set. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Array, String]} The collected set, and any remaining input. + */ +function eatSet (input, classes) { + var length = input.length, + set = [], + chunk, c; + + while (input.length) { + c = input.charAt(0); + if (c === ',') { + input = input.slice(1); + } + else if (c === '>') { + input = input.slice(1); + break; + } + chunk = eatValue(input, classes); + set.push(chunk[0]); + input = chunk[1]; + } + + return [set, input]; +} + +/** + * Consume a map (object). + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Object, String]} The collected map, and any remaining input. + */ +function eatMap (input, classes) { + var length = input.length, + map = {}, + key, value, chunk, c; + + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + if (c === ',') { + input = input.slice(1); + } + else if (c === '}') { + input = input.slice(1); + break; + } + + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + if (input.length) { + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + map[key] = value; + } + else { + map[key] = null; + } + } + + return [map, input]; +} + +/** + * Consume an embedded record. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Object, String]} The collected record, and any remaining input. + */ +function eatRecord (input, classes) { + var record = {'@type': 'd'}, + chunk, c, key, value; + + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + else if (c === ')') { + // empty record. + input = input.slice(1); + return [record, input]; + } + else { + break; + } + } + + chunk = eatFirstKey(input); + + if (chunk[2]) { + // this is actually a class name + record['@class'] = chunk[0]; + input = chunk[1]; + chunk = eatKey(input); + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + else if (c === ')') { + // empty record. + input = input.slice(1); + return [record, input]; + } + else { + break; + } + } + key = chunk[0]; + input = chunk[1]; + } + else { + key = chunk[0]; + input = chunk[1]; + } + + // read the first value. + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + if (c === ',') { + input = input.slice(1); + } + else if (c === ')') { + input = input.slice(1); + break; + } + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + if (input.length) { + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + } + else { + record[key] = null; + } + } + + if (classes && record['@class'] && classes[record['@class']]) { + record = classes[record['@class']](record); + } + + return [record, input]; +} + +/** + * Consume a RID Bag. + * + * @param {String} input The input to parse. + * @return {[Object, String]} The collected bag, and any remaining input. + */ +function eatBag (input) { + var length = input.length, + collected = '', + i, bag, chunk, c; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === ';') { + break; + } + else { + collected += c; + } + } + input = input.slice(i + 1); + + if (exports.enableRIDBags) { + return [new Bag(collected), input]; + } + else { + return [new Bag(collected).all(), input]; + } +} + + +/** + * Consume a binary buffer. + * + * @param {String} input The input to parse. + * @return {[Object, String]} The collected bag, and any remaining input. + */ +function eatBinary (input) { + var length = input.length, + collected = '', + i, bag, chunk, c; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === '_' || c === ',' || c === ')' || c === '>' || c === '}' || c === ']') { + break; + } + else { + collected += c; + } + } + input = input.slice(i + 1); + + return [new Buffer(collected, 'base64'), input]; +} + + +exports.enableRIDBags = true; +exports.deserialize = deserialize; +exports.eatKey = eatKey; +exports.eatValue = eatValue; +exports.eatString = eatString; +exports.eatNumber = eatNumber; +exports.eatRID = eatRID; +exports.eatArray = eatArray; +exports.eatSet = eatSet; +exports.eatMap = eatMap; +exports.eatRecord = eatRecord; +exports.eatBag = eatBag; +exports.eatBinary = eatBinary; diff --git a/lib/transport/binary/protocol26/index.js b/lib/transport/binary/protocol26/index.js new file mode 100644 index 0000000..f022070 --- /dev/null +++ b/lib/transport/binary/protocol26/index.js @@ -0,0 +1,7 @@ +"use strict"; +exports.Operation = require('./operation'); +exports.OperationQueue = require('./operation-queue'); +exports.constants = require('./constants'); +exports.serializer = require('./serializer'); +exports.deserializer = require('./deserializer'); +exports.operations = require('./operations'); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operation-queue.js b/lib/transport/binary/protocol26/operation-queue.js new file mode 100644 index 0000000..d29b353 --- /dev/null +++ b/lib/transport/binary/protocol26/operation-queue.js @@ -0,0 +1,184 @@ +"use strict"; + +var Promise = require('bluebird'), + Operation = require('./operation'), + operations = require('./operations'), + Emitter = require('events').EventEmitter, + errors = require('../../../errors'), + util = require('util'); + +function OperationQueue (socket) { + this.socket = socket || null; + this.items = []; + this.writes = []; + this.remaining = null; + if (socket) { + this.bindToSocket(); + } + Emitter.call(this); +} + +util.inherits(OperationQueue, Emitter); + +module.exports = OperationQueue; + +/** + * Add an operation to the queue. + * + * @param {String|Operation} op The operation name or instance. + * @param {Object} params The parameters for the operation, if op is a string. + * @promise {Object} The result of the operation. + */ +OperationQueue.prototype.add = function (op, params) { + if (typeof op === 'string') { + op = new operations[op](params || {}); + } + var deferred = Promise.defer(), + buffer; + // define the write operations + op.writer(); + // define the read operations + op.reader(); + buffer = op.buffer(); + if (this.socket) { + this.socket.write(buffer); + } + else { + this.writes.push(buffer); + } + if (op.id === 'REQUEST_DB_CLOSE') { + deferred.resolve({}); + } + else { + this.items.push([op, deferred]); + } + return deferred.promise; +}; + +/** + * Cancel all the operations in the queue. + * + * @param {Error} err The error object, if any. + * @return {OperationQueue} The now empty queue. + */ +OperationQueue.prototype.cancel = function (err) { + var item, op, deferred; + while ((item = this.items.shift())) { + op = item[0]; + deferred = item[1]; + deferred.reject(err); + } + return this; +}; + +/** + * Bind to events on the socket. + */ +OperationQueue.prototype.bindToSocket = function (socket) { + var total, i; + if (socket) { + this.socket = socket; + } + this.socket.on('data', this.handleChunk.bind(this)); + if ((total = this.writes.length)) { + if (this.socket.connected) { + for (i = 0; i < total; i++) { + this.socket.write(this.writes[i]); + } + this.writes = []; + } + else { + this.socket.once('connect', function () { + var total = this.writes.length, + i; + for (i = 0; i < total; i++) { + this.socket.write(this.writes[i]); + } + this.writes = []; + }.bind(this)); + } + } +}; + +/** + * Unbind from socket events. + */ +OperationQueue.prototype.unbindFromSocket = function () { + this.socket.removeAllListeners('data'); + delete this.socket; +}; + +/** + * Handle a chunk of data from the socket and attempt to process it. + * + * @param {Buffer} data The data received from the server. + */ +OperationQueue.prototype.handleChunk = function (data) { + var buffer, result, offset; + if (this.remaining) { + buffer = new Buffer(this.remaining.length + data.length); + this.remaining.copy(buffer); + data.copy(buffer, this.remaining.length); + } + else { + buffer = data; + } + offset = this.process(buffer); + if (buffer.length - offset === 0) { + this.remaining = null; + } + else { + this.remaining = buffer.slice(offset); + } +}; + +/** + * Process the operations in the queue against the given buffer. + * + * + * @param {Buffer} buffer The buffer to process. + * @param {Integer} offset The offset to start processing from, defaults to 0. + * @return {Integer} The offset that was successfully read up to. + */ +OperationQueue.prototype.process = function (buffer, offset) { + var code, parsed, result, status, item, op, deferred, err; + offset = offset || 0; + while ((item = this.items.shift())) { + op = item[0]; + deferred = item[1]; + parsed = op.consume(buffer, offset); + status = parsed[0]; + offset = parsed[1]; + result = parsed[2]; + if (status === Operation.READING) { + // operation is incomplete, buffer does not contain enough data + this.items.unshift(item); + return offset; + } + else if (status === Operation.PUSH_DATA) { + this.emit('update-config', result); + this.items.unshift(item); + return offset; + } + else if (status === Operation.COMPLETE) { + deferred.resolve(result); + } + else if (status === Operation.ERROR) { + if (result.status.error) { + // this is likely a recoverable error + deferred.reject(result.status.error); + } + else { + // cannot recover, reject everything and let the application decide what to do + err = new errors.Protocol('Unknown Error on operation id ' + op.id, result); + deferred.reject(err); + this.cancel(err); + this.emit('error', err); + } + } + else { + deferred.reject(new errors.Protocol('Unsupported operation status: ' + status)); + } + } + return offset; +}; \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operation.js b/lib/transport/binary/protocol26/operation.js new file mode 100644 index 0000000..c109040 --- /dev/null +++ b/lib/transport/binary/protocol26/operation.js @@ -0,0 +1,879 @@ +"use strict"; + +var constants = require('./constants'), + utils = require('../../../utils'), + Long = require('../../../long').Long, + errors = require('../../../errors'), + deserializer = require('./deserializer'); + + +/** + * # Operations + * + * The base class for operations, provides a simple DSL for defining + * the steps required to send a command to the server, and then read + * the response. + * + * Each operation should implement the `writer()` and `reader()` methods. + * + * @param {Object} data The data for the operation. + */ +function Operation (data) { + this.status = Operation.PENDING; + this.writeOps = []; + this.readOps = []; + this.stack = [{}]; + this.data = data || {}; +} + +module.exports = Operation; + +// operation statuses + +var statuses = require('../operation-status'); + +Operation.PENDING = statuses.PENDING; +Operation.WRITTEN = statuses.WRITTEN; +Operation.READING = statuses.READING; +Operation.COMPLETE = statuses.COMPLETE; +Operation.ERROR = statuses.ERROR; +Operation.PUSH_DATA = statuses.PUSH_DATA; + +// make it easy to inherit from the base class +Operation.extend = utils.extend; + +/** + * Declares the commands to send to the server. + * Child classes should implement this function. + */ +Operation.prototype.writer = function () { + +}; + +/** + * Declares the steps required to recieve data for the operation. + * Child classes should implement this function. + */ +Operation.prototype.reader = function () { + +}; + +/** + * Prepare the buffer for the operation. + * + * @return {Buffer} The buffer containing the commands to send to the server. + */ +Operation.prototype.buffer = function () { + + if (!this.writeOps.length) { + this.writer(); + } + + var total = this.writeOps.length, + size = 0, + commands = [], + item, i, fn, offset, data, buffer; + + for (i = 0; i < total; i++) { + item = this.writeOps[i]; + offset = size; + commands.push([item[1], offset]); + size += item[0]; + } + + buffer = new Buffer(size); + + for (i = 0; i < total; i++) { + item = commands[i]; + fn = item[0]; + offset = item[1]; + fn(buffer, offset); + } + + return buffer; +}; + +/** + * Write a byte. + * + * @param {Mixed} data The data. + * @return {Operation} The operation instance. + */ +Operation.prototype.writeByte = function (data) { + this.writeOps.push([1, function (buffer, offset) { + buffer[offset] = data; + }]); + return this; +}; + +/** + * Write a boolean. + * + * @param {Mixed} data The data. + * @return {Operation} The operation instance. + */ +Operation.prototype.writeBoolean = function (data) { + this.writeOps.push([1, function (buffer, offset) { + buffer[offset] = data ? 1 : 0; + }]); + return this; +}; + + +/** + * Write a single character. + * + * @param {Mixed} data The data. + * @return {Operation} The operation instance. + */ +Operation.prototype.writeChar = function (data) { + this.writeOps.push([1, function (buffer, offset) { + buffer[offset] = (''+data).charCodeAt(0); + }]); + return this; +}; + +/** + * Parse data to 4 bytes which represents integer value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeInt = function (data) { + this.writeOps.push([constants.BYTES_INT, function (buffer, offset) { + buffer.fill(0, offset, offset + constants.BYTES_INT); + buffer.writeInt32BE(data, offset); + }]); + return this; +}; + +/** + * Parse data to 8 bytes which represents a long value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeLong = function (data) { + this.writeOps.push([constants.BYTES_LONG, function (buffer, offset) { + data = Long.fromNumber(data); + buffer.fill(0, offset, offset + constants.BYTES_LONG); + buffer.writeInt32BE(data.high_, offset); + buffer.writeInt32BE(data.low_, offset + constants.BYTES_INT); + + }]); + return this; +}; + +/** + * Parse data to 2 bytes which represents short value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeShort = function (data) { + this.writeOps.push([constants.BYTES_SHORT, function (buffer, offset) { + buffer.fill(0, offset, offset + constants.BYTES_SHORT); + buffer.writeInt16BE(data, offset); + }]); + return this; +}; + +/** + * Write bytes to a buffer + * @param {Buffer} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeBytes = function (data) { + this.writeOps.push([constants.BYTES_INT + data.length, function (buffer, offset) { + buffer.writeInt32BE(data.length, offset); + data.copy(buffer, offset + constants.BYTES_INT); + }]); + return this; +}; + +/** + * Parse string data to buffer with UTF-8 encoding. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeString = function (data) { + if (data == null) { + return this.writeInt(-1); + } + var encoded = encodeString(data), + length = encoded.length; + this.writeOps.push([constants.BYTES_INT + length, function (buffer, offset) { + buffer.writeInt32BE(length, offset); + encoded.copy(buffer, offset + constants.BYTES_INT); + }]); + return this; +}; + +function encodeString (data) { + var length = data.length, + output = new Buffer(length * 3), // worst case, all chars could require 3-byte encodings. + j = 0, // index output + i, c; + + for (i = 0; i < length; i++) { + c = data.charCodeAt(i); + if (c < 0x80) { + // 7-bits done in one byte. + output[j++] = c; + } + else if (c < 0x800) { + // 8-11 bits done in 2 bytes + output[j++] = (0xC0 | c >> 6); + output[j++] = (0x80 | c & 0x3F); + } + else { + // 12-16 bits done in 3 bytes + output[j++] = (0xE0 | c >> 12); + output[j++] = (0x80 | c >> 6 & 0x3F); + output[j++] = (0x80 | c & 0x3F); + } + } + return output.slice(0, j); +} + +// # Read Operations + + +/** + * Read a status from the server response. + * If the status contains an error, that error + * will be read instead of any subsequently queued commands. + * + * @param {String} fieldName The name of the data field to populate. + * @param {Function} reader The function that should be invoked after this value is read. if any. + * @return {Operation} The operation instance. + */ +Operation.prototype.readStatus = function (fieldName, reader) { + var value = {}; + fieldName = fieldName || 'status'; + + this.readOps.push(function (data) { + data[fieldName] = value; + this.stack.push(data[fieldName]); + }); + this.readByte('code'); + this.readInt('sessionId', function (data) { + if (data.code === 1) { + this.readError('error', function () { + if (reader) { + reader.call(this, value, fieldName); + } + this.readOps.push(function () { + this.stack.pop(); + }); + }); + } + else { + if (reader) { + reader.call(this, value, fieldName); + } + this.stack.pop(); + } + }); + return this; +}; + +/** + * Read an error from the server response. + * Any subsequently queued commands will not run. + * + * @param {String} fieldName The name of the data field to populate. + * @param {Function} reader The function that should be invoked after this value is read. if any. + * @return {Operation} The operation instance. + */ +Operation.prototype.readError = function (fieldName, reader) { + this.readOps = [['Error', [fieldName, reader]]]; + return this; +}; + +/** + * Read an object from the server response. + * This is the same as `readString` but deserializes the returned string + * into an object. + * + * @param {String} fieldName The name of the data field to populate. + * @param {Function} reader The function that should be invoked after this value is read. if any. + * @return {Operation} The operation instance. + */ +Operation.prototype.readObject = function (fieldName, reader) { + this.readOps.push(['String', [fieldName, function (data, fieldName) { + data[fieldName] = deserializer.deserialize(data[fieldName], this.data.transformerFunctions); + if (reader) { + reader.call(this, data, fieldName); + } + }]]); + return this; +}; + + +// Add the `readByte`, `readInt` etc methods. +// these are just shortcuts + +[ + 'Byte', + 'Bytes', + 'Int', + 'Short', + 'Long', + 'String', + 'Array', + 'Record', + 'Char', + 'Boolean', + 'Collection' +] +.forEach(function (name) { + this['read' + name] = function (fieldName, reader) { + this.readOps.push([name, arguments]); + return this; + }; +}, Operation.prototype); + + +/** + * Consume the buffer starting from the given offset. + * Returns an array containing the operation status, the + * new offset and any collected result. + * + * If the buffer doesn't contain enough data for the operation + * to complete, it will process as much as possible and return + * a partial result with a status code of `Operation.READING` meaning + * that the operation is still in the reading state. + * + * If the operation completes successfully, the status code will be + * `Operation.COMPLETE`. + * + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset + * @return {Array} The array containing the status, new offset and result. + */ +Operation.prototype.consume = function (buffer, offset) { + var obj, code, context, item, type, args; + offset = offset || 0; + if (this.status === Operation.PENDING) { + // this is the first time consume has been called for this + // operation. We need to determine whether the response + // we're reading is really for us or whether it's a + // PUSH_DATA command. + if (buffer.length < offset + 1) { + // not enough bytes in the buffer to check. + return [Operation.READING, offset, {}]; + } + + code = buffer.readUInt8(offset); + if (code === 3) { + offset += 5; // ignore the next integer + obj = {}; + offset += this.parsePushedData(buffer, offset, obj, 'data'); + return [Operation.PUSH_DATA, offset, obj.data]; + } + + this.status = Operation.READING; + } + if (this.readOps.length === 0) { + return [Operation.COMPLETE, offset, this.stack[0]]; + } + while ((item = this.readOps.shift())) { + context = this.stack[this.stack.length - 1]; + if (typeof item === 'function') { + // this is a nop, just execute it. + item.call(this, context, buffer, offset); + continue; + } + type = item[0]; + args = item[1]; + if (!this.canRead(type, buffer, offset)) { + // not enough bytes in the buffer, operation is still reading. + this.readOps.unshift(item); + return [Operation.READING, offset, this.stack[0]]; + } + offset += this['parse' + type](buffer, offset, context, args[0], args[1]); + } + if (this.stack[0] && this.stack[0].status && this.stack[0].status.code) { + return [Operation.ERROR, offset, this.stack[0]]; + } + else { + return [Operation.COMPLETE, offset, this.stack[0]]; + } +}; + +/** + * Defetermine whether the operation can read a value of the given + * type from the buffer at the given offset. + * + * @param {String} type The value type. + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading at. + * @return {Boolean} true if the value can be read. + */ +Operation.prototype.canRead = function (type, buffer, offset) { + var length = buffer.length; + if (offset > length) { + return false; + } + switch (type) { + case 'Array': + case 'Error': + return true; + case 'Byte': + case 'Char': + case 'Boolean': + return length >= offset + 1; + case 'Short': + case 'Record': + return length >= offset + constants.BYTES_SHORT; + case 'Long': + return length >= offset + constants.BYTES_LONG; + case 'Int': + case 'Collection': + return length >= offset + constants.BYTES_INT; + case 'Bytes': + case 'String': + if (length <= offset + constants.BYTES_INT) { + return false; + } + else { + return length >= offset + constants.BYTES_INT + buffer.readInt32BE(offset); + } + break; + default: + return false; + } +}; + + +/** + * Parse a byte from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseByte = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = buffer.readUInt8(offset); + if (reader) { + reader.call(this, context, fieldName); + } + return 1; +}; + +/** + * Parse a character from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseChar = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = String.fromCharCode(buffer.readUInt8(offset)); + if (reader) { + reader.call(this, context, fieldName); + } + return 1; +}; + +/** + * Parse a boolean from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseBoolean = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = Boolean(buffer.readUInt8(offset)); + if (reader) { + reader.call(this, context, fieldName); + } + return 1; +}; + + +/** + * Parse a short from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseShort = function (buffer, offset, context, fieldName, reader) { + + context[fieldName] = buffer.readInt16BE(offset); + if (reader) { + reader.call(this, context, fieldName); + } + return constants.BYTES_SHORT; +}; + +/** + * Parse an integer from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseInt = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = buffer.readInt32BE(offset); + if (reader) { + reader.call(this, context, fieldName); + } + return constants.BYTES_INT; +}; + +/** + * Parse a long from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseLong = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = Long + .fromBits( + buffer.readUInt32BE(offset + constants.BYTES_INT), + buffer.readInt32BE(offset) + ) + .toNumber(); + + if (reader) { + reader.call(this, context, fieldName); + } + return constants.BYTES_LONG; +}; + +/** + * Parse some bytes from the given buffer at the given offset and + * insert them into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseBytes = function (buffer, offset, context, fieldName, reader) { + var length = buffer.readInt32BE(offset); + offset += constants.BYTES_INT; + if (length < 0) { + context[fieldName] = null; + } + else { + context[fieldName] = buffer.slice(offset, offset + length); + } + if (reader) { + reader.call(this, context, fieldName); + } + return length > 0 ? length + constants.BYTES_INT : constants.BYTES_INT; +}; + + +/** + * Parse a string from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseString = function (buffer, offset, context, fieldName, reader) { + var length = buffer.readInt32BE(offset); + offset += constants.BYTES_INT; + if (length < 0) { + context[fieldName] = null; + } + else { + context[fieldName] = buffer.toString('utf8', offset, offset + length); + } + if (reader) { + reader.call(this, context, fieldName); + } + return length > 0 ? length + constants.BYTES_INT : constants.BYTES_INT; +}; + +/** + * Parse a record from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseRecord = function (buffer, offset, context, fieldName, reader) { + var remainingOps = this.readOps, + record = {}; + this.readOps = []; + this.stack.push(record); + if (Array.isArray(context[fieldName])) { + context[fieldName].push(record); + } + else { + context[fieldName] = record; + } + this.readShort('classId', function (record, fieldName) { + if (record[fieldName] === -1) { + record.value = new errors.Protocol('No class for record, cannot proceed.'); + this.stack.pop(); + this.readOps.push(function () { + if (reader) { + reader.call(this, context, fieldName); + } + }); + this.readOps.push.apply(this.readOps, remainingOps); + return; + } + else if (record[fieldName] === -2) { + record.value = null; + this.stack.pop(); + this.readOps.push(function () { + if (reader) { + reader.call(this, context, fieldName); + } + }); + this.readOps.push.apply(this.readOps, remainingOps); + return; + } + else if (record[fieldName] === -3) { + record.type = 'd'; + this + .readShort('cluster') + .readLong('position') + .readOps.push(function () { + this.stack.pop(); + this.readOps.push(function () { + if (reader) { + reader.call(this, context, fieldName); + } + }); + this.readOps.push.apply(this.readOps, remainingOps); + }); + } + else if (record[fieldName] > -1) { + this + .readChar('type') + .readShort('cluster') + .readLong('position') + .readInt('version') + .readString('value', function (data, key) { + data[key] = deserializer.deserialize(data[key], this.data.transformerFunctions); + this.stack.pop(); + this.readOps.push(function () { + if (reader) { + reader.call(this, context, fieldName); + } + }); + this.readOps.push.apply(this.readOps, remainingOps); + }); + } + }); + + + + return 0; +}; + + +/** + * Parse a collection from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseCollection = function (buffer, offset, context, fieldName, reader) { + var remainingOps = this.readOps, + records = [], + total = buffer.readInt32BE(offset), + i; + offset += 4; + this.readOps = []; + context[fieldName] = records; + for (i = 0; i < total; i++) { + this.readRecord(fieldName); + } + if (reader) { + this.readOps.push(function () { + reader.call(this, records); + }); + } + this.readOps.push.apply(this.readOps, remainingOps); + return 4; +}; + + +/** + * Parse an array from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * > Note. this differs from the other `parseXYZ` methods in that `reader` + * is required, and MUST return an array of functions. Each function in the + * array represents a 'scope' for an item in the array and will be invoked in order. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseArray = function (buffer, offset, context, fieldName, reader) { + var items = reader.call(this, context), + remainingOps = this.readOps; + + this.readOps = []; + + context[fieldName] = []; + this.stack.push(context[fieldName]); + + items.map(function (item) { + var childContext = {}; + context[fieldName].push(childContext); + this.readOps.push(function () { + this.stack.push(childContext); + }); + + item.call(this, childContext); + + this.readOps.push(function () { + this.stack.pop(); + }); + }, this); + + + this.readOps.push(function () { + this.stack.pop(); + }); + + this.readOps.push.apply(this.readOps, remainingOps); + return 0; +}; + +/** + * Parse an error from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * > Note: this implementation differs from the others in that + * when an error is encountered, any subsequent `readXYZ()` commands + * that were due to be run will be skipped. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseError = function (buffer, offset, context, fieldName, reader) { + var err = new errors.Request(); + err.previous = []; + // remove any ops we were expecting to run. + this.readOps = []; + + context[fieldName] = err; + this.stack.push(err); + this.readByte('id'); + + + function readItem () { + this.readString('type'); + this.readString('message'); + this.readByte('hasMore', function (data) { + var prev; + if (data.hasMore) { + prev = new errors.Request(); + prev.type = data.type; + prev.message = data.message; + err.previous.push(prev); + this.stack.pop(); + this.stack.push(prev); + readItem.call(this); + } + else { + err.type = data.type; + err.message = data.message; + this.readBytes('javaStackTrace', function (data) { + this.readOps.push(function (data) { + this.stack.pop(); + }); + }); + } + }); + } + readItem.call(this); + if (reader) { + reader.call(this, context, fieldName); + } + return 0; +}; + + +/** + * Parse any pushed data from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parsePushedData = function (buffer, offset, context, fieldName, reader) { + var length = buffer.readInt32BE(offset), + asString; + offset += constants.BYTES_INT; + asString = buffer.toString('utf8', offset, offset + length); + switch (asString.charAt(0)) { + case 'R': + context[fieldName] = deserializer.deserialize(asString.slice(1), this.data.transformerFunctions); + break; + default: + console.log('unsupported pushed data format: ' + asString); + } + if (reader) { + reader.call(this, context, fieldName); + } + return length + constants.BYTES_INT; +}; + diff --git a/lib/protocol/operations/command.js b/lib/transport/binary/protocol26/operations/command.js similarity index 60% rename from lib/protocol/operations/command.js rename to lib/transport/binary/protocol26/operations/command.js index 778ab5e..343400a 100644 --- a/lib/protocol/operations/command.js +++ b/lib/transport/binary/protocol26/operations/command.js @@ -1,14 +1,16 @@ +"use strict"; + var Operation = require('../operation'), constants = require('../constants'), serializer = require('../serializer'), - deserializer = require('../deserializer'), - writer = require('../writer'); + writer = require('../writer'), + RID = require('../../../../recordid'); module.exports = Operation.extend({ id: 'REQUEST_COMMAND', opCode: 41, writer: function () { - if (this.data.mode === 'a') { + if (this.data.mode === 'a' && !this.data.class) { this.data.class = 'com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery'; } this @@ -19,28 +21,48 @@ module.exports = Operation.extend({ }, serializeQuery: function () { - var buffers = [ - writer.writeString(this.data.class), - writer.writeString(this.data.query) - ]; - if (this.data.class === 'com.orientechnologies.orient.core.sql.query.OSQLSynchQuery' || this.data.class === 'com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery') { + var buffers = [writer.writeString(this.data.class)]; + + if (this.data.class === 'q' || + this.data.class === 'com.orientechnologies.orient.core.sql.query.OSQLSynchQuery' || + this.data.class === 'com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery') { buffers.push( + writer.writeString(this.data.query), writer.writeInt(this.data.limit), writer.writeString(this.data.fetchPlan || '') ); if (this.data.params) { - buffers.push(writer.writeString(serializer.serializeDocument(this.data.params))); + buffers.push(writer.writeString(serializeParams(this.data.params))); } else { buffers.push(writer.writeInt(0)); } } + else if ( + this.data.class === 's' || + this.data.class === 'com.orientechnologies.orient.core.command.script.OCommandScript') { + buffers.push( + writer.writeString(this.data.language || 'sql'), + writer.writeString(this.data.query) + ); + if (this.data.params && this.data.params.params && Object.keys(this.data.params.params).length) { + buffers.push( + writer.writeBoolean(true), + writer.writeString(serializeParams(this.data.params)) + ); + } + else { + buffers.push(writer.writeBoolean(false)); + } + buffers.push(writer.writeByte(0)); + } else { + buffers.push(writer.writeString(this.data.query)); if (this.data.params) { buffers.push( writer.writeBoolean(true), - writer.writeString(serializer.serializeDocument(this.data.params)) + writer.writeString(serializeParams(this.data.params)) ); } else { @@ -130,3 +152,29 @@ module.exports = Operation.extend({ } }); +/** + * Serialize the parameters for a query. + * + * > Note: There is a bug in OrientDB where special kinds of string values + * > need to be twice quoted *in parameters*. Hence the need for this specialist function. + * + * @param {Object} data The data to serialize. + * @return {String} The serialized data. + */ +function serializeParams (data) { + var keys = Object.keys(data.params || {}), + total = keys.length, + c, i, key, value; + + for (i = 0; i < total; i++) { + key = keys[i]; + value = data.params[key]; + if (typeof value === 'string') { + c = value.charAt(0); + if (c === '.' || c === '#' || c === '<' || c === '[' || c === '(' || c === '{' || c === '0' || +c) { + data.params[key] = '"' + value + '"'; + } + } + } + return serializer.serializeDocument(data); +} \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/config-get.js b/lib/transport/binary/protocol26/operations/config-get.js new file mode 100644 index 0000000..a5e3b74 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/config-get.js @@ -0,0 +1,20 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_CONFIG_GET', + opCode: 70, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1) + .writeString(this.data.key); + }, + reader: function () { + this + .readStatus('status') + .readString('value'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/config-list.js b/lib/transport/binary/protocol26/operations/config-list.js new file mode 100644 index 0000000..e4cab31 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/config-list.js @@ -0,0 +1,31 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_CONFIG_LIST', + opCode: 72, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1); + }, + reader: function () { + this + .readStatus('status') + .readShort('total') + .readArray('items', function (data) { + var items = [], + i; + for (i = 0; i < data.total; i++) { + items.push(function () { + this + .readString('key') + .readString('value'); + }); + } + return items; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/config-set.js b/lib/transport/binary/protocol26/operations/config-set.js new file mode 100644 index 0000000..686dcca --- /dev/null +++ b/lib/transport/binary/protocol26/operations/config-set.js @@ -0,0 +1,23 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_CONFIG_SET', + opCode: 71, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1) + .writeString(this.data.key) + .writeString(this.data.value); + }, + reader: function () { + this + .readStatus('status') + .readOps.push(function (data) { + data.success = true; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/connect.js b/lib/transport/binary/protocol26/operations/connect.js new file mode 100644 index 0000000..d9b8916 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/connect.js @@ -0,0 +1,27 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + npmPackage = require('../../../../../package.json'); + +module.exports = Operation.extend({ + id: 'REQUEST_CONNECT', + opCode: 2, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1) + .writeString(npmPackage.name) + .writeString(npmPackage.version) + .writeShort(+constants.PROTOCOL_VERSION) + .writeString('') // client id + .writeString('ORecordDocument2csv') // serialization format + .writeString(this.data.username) + .writeString(this.data.password); + }, + reader: function () { + this + .readStatus('status') + .readInt('sessionId'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/datacluster-add.js b/lib/transport/binary/protocol26/operations/datacluster-add.js new file mode 100644 index 0000000..5846cc3 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/datacluster-add.js @@ -0,0 +1,21 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DATACLUSTER_ADD', + opCode: 10, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeString(this.data.name) + .writeShort(this.data.id || -1); + }, + reader: function () { + this + .readStatus('status') + .readShort('id'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/datacluster-count.js b/lib/transport/binary/protocol26/operations/datacluster-count.js new file mode 100644 index 0000000..2859a63 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/datacluster-count.js @@ -0,0 +1,35 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DATACLUSTER_COUNT', + opCode: 12, + writer: function () { + var total, item, i; + + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId); + + if (Array.isArray(this.data.id)) { + total = this.data.id.length; + this.writeShort(total); + for (i = 0; i < total; i++) { + this.writeShort(this.data.id[i]); + } + } + else { + this + .writeShort(1) + .writeShort(this.data.id); + } + this.writeByte(this.data.tombstones || false); + }, + reader: function () { + this + .readStatus('status') + .readLong('count'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/datacluster-datarange.js b/lib/transport/binary/protocol26/operations/datacluster-datarange.js new file mode 100644 index 0000000..9caae51 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/datacluster-datarange.js @@ -0,0 +1,23 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DATACLUSTER_DATARANGE', + opCode: 13, + writer: function () { + var total, i; + + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeShort(this.data.id); + }, + reader: function () { + this + .readStatus('status') + .readLong('begin') + .readLong('end'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/datacluster-drop.js b/lib/transport/binary/protocol26/operations/datacluster-drop.js new file mode 100644 index 0000000..89b3ea5 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/datacluster-drop.js @@ -0,0 +1,22 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DATACLUSTER_DROP', + opCode: 11, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeShort(this.data.id); + }, + reader: function () { + this + .readStatus('status') + .readByte('success', function (data, fieldName) { + data[fieldName] = Boolean(data[fieldName]); + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/db-close.js b/lib/transport/binary/protocol26/operations/db-close.js new file mode 100644 index 0000000..ba291e8 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-close.js @@ -0,0 +1,15 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_CLOSE', + opCode: 5, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1); + }, + reader: function () {} +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/db-countrecords.js b/lib/transport/binary/protocol26/operations/db-countrecords.js new file mode 100644 index 0000000..a290a89 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-countrecords.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_COUNTRECORDS', + opCode: 9, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId); + }, + reader: function () { + this + .readStatus('status') + .readLong('count'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/db-create.js b/lib/transport/binary/protocol26/operations/db-create.js new file mode 100644 index 0000000..3381106 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-create.js @@ -0,0 +1,20 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_CREATE', + opCode: 4, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeString(this.data.name) + .writeString(this.data.type || 'graph') + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/db-delete.js b/lib/transport/binary/protocol26/operations/db-delete.js new file mode 100644 index 0000000..6772b10 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-delete.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_DROP', + opCode: 7, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeString(this.data.name) + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/db-exists.js b/lib/transport/binary/protocol26/operations/db-exists.js new file mode 100644 index 0000000..29c1e40 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-exists.js @@ -0,0 +1,23 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_EXIST', + opCode: 6, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeString(this.data.name) + .writeString(this.data.storage || 'local'); + }, + reader: function () { + this + .readStatus('status') + .readByte('exists', function (data) { + data.exists = Boolean(data.exists); + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/db-freeze.js b/lib/transport/binary/protocol26/operations/db-freeze.js new file mode 100644 index 0000000..6c9cba4 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-freeze.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_FREEZE', + opCode: 94, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeString(this.data.name) + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); diff --git a/lib/transport/binary/protocol26/operations/db-list.js b/lib/transport/binary/protocol26/operations/db-list.js new file mode 100644 index 0000000..8262b5e --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-list.js @@ -0,0 +1,21 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_LIST', + opCode: 74, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1); + }, + reader: function () { + this + .readStatus('status') + .readObject('databases', function (data, fieldName) { + data[fieldName] = data[fieldName].databases; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/db-open.js b/lib/transport/binary/protocol26/operations/db-open.js new file mode 100644 index 0000000..8c3eb8f --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-open.js @@ -0,0 +1,44 @@ +"use strict"; +var Operation = require('../operation'), + constants = require('../constants'), + npmPackage = require('../../../../../package.json'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_OPEN', + opCode: 3, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1) + .writeString(npmPackage.name) + .writeString(npmPackage.version) + .writeShort(+constants.PROTOCOL_VERSION) + .writeString('') // client id + .writeString('ORecordDocument2csv') // serialization format + .writeString(this.data.name) + .writeString(this.data.type) + .writeString(this.data.username) + .writeString(this.data.password); + }, + reader: function () { + this + .readStatus('status') + .readInt('sessionId') + .readShort('totalClusters') + .readArray('clusters', function (data) { + var clusters = [], + total = data.totalClusters, + i; + + for (i = 0; i < total; i++) { + clusters.push(function (data) { + this.readString('name') + .readShort('id'); + }); + } + return clusters; + }) + .readObject('serverCluster') + .readString('release'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/db-release.js b/lib/transport/binary/protocol26/operations/db-release.js new file mode 100644 index 0000000..daeada5 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-release.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_RELEASE', + opCode: 95, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeString(this.data.name) + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); diff --git a/lib/transport/binary/protocol26/operations/db-reload.js b/lib/transport/binary/protocol26/operations/db-reload.js new file mode 100644 index 0000000..0f01c18 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-reload.js @@ -0,0 +1,32 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_RELOAD', + opCode: 73, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1); + }, + reader: function () { + this + .readStatus('status') + .readShort('totalClusters') + .readArray('clusters', function (data) { + var clusters = [], + total = data.totalClusters, + i; + + for (i = 0; i < total; i++) { + clusters.push(function (data) { + this.readString('name') + .readShort('id'); + }); + } + return clusters; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/db-size.js b/lib/transport/binary/protocol26/operations/db-size.js new file mode 100644 index 0000000..e83b6b3 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/db-size.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_SIZE', + opCode: 8, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId); + }, + reader: function () { + this + .readStatus('status') + .readLong('size'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/index.js b/lib/transport/binary/protocol26/operations/index.js new file mode 100644 index 0000000..be581ca --- /dev/null +++ b/lib/transport/binary/protocol26/operations/index.js @@ -0,0 +1,34 @@ +"use strict"; /*jshint sub:true*/ + +exports['connect'] = require('./connect'); +exports['db-open'] = require('./db-open'); +exports['db-create'] = require('./db-create'); +exports['db-exists'] = require('./db-exists'); +exports['db-delete'] = require('./db-delete'); +exports['db-size'] = require('./db-size'); +exports['db-countrecords'] = require('./db-countrecords'); +exports['db-reload'] = require('./db-reload'); +exports['db-list'] = require('./db-list'); +exports['db-freeze'] = require('./db-freeze'); +exports['db-release'] = require('./db-release'); +exports['db-close'] = require('./db-close'); + + +exports['datacluster-add'] = require('./datacluster-add'); +exports['datacluster-count'] = require('./datacluster-count'); +exports['datacluster-datarange'] = require('./datacluster-datarange'); +exports['datacluster-drop'] = require('./datacluster-drop'); + +exports['record-create'] = require('./record-create'); +exports['record-load'] = require('./record-load'); +exports['record-metadata'] = require('./record-metadata'); +exports['record-update'] = require('./record-update'); +exports['record-delete'] = require('./record-delete'); +exports['record-clean-out'] = require('./record-clean-out'); + +exports['command'] = require('./command'); +exports['tx-commit'] = require('./tx-commit'); + +exports['config-list'] = require('./config-list'); +exports['config-get'] = require('./config-get'); +exports['config-set'] = require('./config-set'); diff --git a/lib/transport/binary/protocol26/operations/record-clean-out.js b/lib/transport/binary/protocol26/operations/record-clean-out.js new file mode 100644 index 0000000..a140348 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/record-clean-out.js @@ -0,0 +1,35 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_CLEAN_OUT', + opCode: 38, + writer: function () { + var rid, cluster, position; + if (this.data.record && this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + position = this.data.position || rid.position; + } + else { + cluster = this.data.cluster; + position = this.data.position; + } + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeShort(cluster) + .writeLong(position) + .writeInt(this.data.version || -1) + .writeBoolean(this.data.mode); + }, + reader: function () { + this + .readStatus('status') + .readBoolean('success'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/record-create.js b/lib/transport/binary/protocol26/operations/record-create.js new file mode 100644 index 0000000..b4fc227 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/record-create.js @@ -0,0 +1,51 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_CREATE', + opCode: 31, + writer: function () { + var rid, cluster; + if (this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + } + else { + cluster = this.data.cluster; + } + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeShort(cluster) + .writeBytes(serializer.encodeRecordData(this.data.record)) + .writeByte(constants.RECORD_TYPES[this.data.type || 'd']) + .writeByte(this.data.mode || 0); + }, + reader: function () { + this + .readStatus('status') + .readShort('cluster') + .readLong('position') + .readInt('version') + .readInt('totalChanges') + .readArray('changes', function (data) { + var items = [], + i; + for (i = 0; i < data.totalChanges; i++) { + items.push(function () { + this + .readLong('uuidHigh') + .readLong('uuidLow') + .readLong('fileId') + .readLong('pageIndex') + .readInt('pageOffset'); + }); + } + return items; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/record-delete.js b/lib/transport/binary/protocol26/operations/record-delete.js new file mode 100644 index 0000000..a246628 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/record-delete.js @@ -0,0 +1,46 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_DELETE', + opCode: 33, + writer: function () { + var rid, cluster, position, version; + if (this.data.record && this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + position = this.data.position || rid.position; + } + else { + cluster = this.data.cluster; + position = this.data.position; + } + if (this.data.version != null) { + version = this.data.version; + } + else if (this.data.record && this.data.record['@version'] != null) { + version = this.data.record['@version']; + } + else { + version = -1; + } + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeShort(cluster) + .writeLong(position) + .writeInt(version) + .writeByte(this.data.mode || 0); + }, + reader: function () { + this + .readStatus('status') + .readByte('success', function (data, fieldName) { + data[fieldName] = Boolean(data[fieldName]); + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/record-load.js b/lib/transport/binary/protocol26/operations/record-load.js new file mode 100644 index 0000000..6c2134f --- /dev/null +++ b/lib/transport/binary/protocol26/operations/record-load.js @@ -0,0 +1,120 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'), + deserializer = require('../deserializer'), + errors = require('../../../../errors'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_LOAD', + opCode: 30, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeShort(this.data.cluster) + .writeLong(this.data.position) + .writeString(this.data.fetchPlan || '') + .writeByte(this.data.ignoreCache || 0) + .writeByte(this.data.tombstones || 0); + }, + reader: function () { + var records = []; + this.readStatus('status'); + this.readOps.push(function (data) { + data.records = records; + this.stack.push(data.records); + this.readPayload(records, function () { + this.stack.pop(); + data.records = data.records.map(function (record) { + var r; + if (record.type === 'd') { + r = record.content || {}; + r['@rid'] = r['@rid'] ||new RID({ + cluster: record.cluster, + position: record.position + }); + r['@version'] = record.version; + r['@type'] = record.type; + } + else { + r = { + '@rid': new RID({ + cluster: record.cluster, + position: record.position + }), + '@version': record.version, + '@type': record.type, + value: record.content + }; + + } + return r; + }, this); + }); + }); + }, + readPayload: function (records, ender) { + + return this.readByte('payloadStatus', function (data, fieldName) { + var record = {}; + switch (data[fieldName]) { + case 0: + // nothing to do. + if (ender) { + ender.call(this); + } + break; + case 1: + // a record + records.push(record); + this.stack.push(record); + this + .readString('content') + .readInt('version') + .readChar('type', function (data, fieldName) { + data.cluster = this.data.cluster; + data.position = this.data.position; + if (data[fieldName] === 'd') { + data.content = deserializer.deserialize(data.content, this.data.transformerFunctions); + } + this.stack.pop(); + this.readPayload(records, ender); + }); + break; + case 2: + // a sub record + records.push(record); + this.stack.push(record); + this.readShort('classId', function (data, fieldName) { + switch (data[fieldName]) { + case -2: + this.stack.pop(); + this.readPayload(records, ender); + break; + case -3: + throw new errors.Protocol('ClassID ' + data[fieldName] + ' is not supported.'); + default: + this + .readChar('type') + .readShort('cluster') + .readLong('position') + .readInt('version') + .readString('content', function (data, fieldName) { + if (data.type === 'd') { + data.content = deserializer.deserialize(data.content, this.data.transformerFunctions); + } + this.stack.pop(); + this.readPayload(records, ender); + }); + } + }); + break; + default: + this.readPayload(records, ender); + } + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/record-metadata.js b/lib/transport/binary/protocol26/operations/record-metadata.js new file mode 100644 index 0000000..2d0ccb3 --- /dev/null +++ b/lib/transport/binary/protocol26/operations/record-metadata.js @@ -0,0 +1,35 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_METADATA', + opCode: 29, + writer: function () { + var rid, cluster, position; + if (this.data.record && this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + position = this.data.position || rid.position; + } + else { + cluster = this.data.cluster; + position = this.data.position; + } + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeShort(cluster) + .writeLong(position); + }, + reader: function () { + this + .readStatus('status') + .readShort('cluster') + .readLong('position') + .readInt('version'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/record-update.js b/lib/transport/binary/protocol26/operations/record-update.js new file mode 100644 index 0000000..091792e --- /dev/null +++ b/lib/transport/binary/protocol26/operations/record-update.js @@ -0,0 +1,63 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_UPDATE', + opCode: 32, + writer: function () { + var rid, cluster, position, version; + if (this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + position = this.data.position || rid.position; + } + else { + cluster = this.data.cluster; + position = this.data.position; + } + if (this.data.version != null) { + version = this.data.version; + } + else if (this.data.record['@version'] != null) { + version = this.data.record['@version']; + } + else { + version = -1; + } + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId) + .writeShort(cluster) + .writeLong(position) + .writeBoolean(true) + .writeBytes(serializer.encodeRecordData(this.data.record)) + .writeInt(version) + .writeByte(constants.RECORD_TYPES[this.data.type || 'd']) + .writeByte(this.data.mode || 0); + }, + reader: function () { + this + .readStatus('status') + .readInt('version') + .readInt('totalChanges') + .readArray('changes', function (data) { + var items = [], + i; + for (i = 0; i < data.totalChanges; i++) { + items.push(function () { + this + .readLong('uuidHigh') + .readLong('uuidLow') + .readLong('fileId') + .readLong('pageIndex') + .readInt('pageOffset'); + }); + } + return items; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/operations/tx-commit.js b/lib/transport/binary/protocol26/operations/tx-commit.js new file mode 100644 index 0000000..b80f39f --- /dev/null +++ b/lib/transport/binary/protocol26/operations/tx-commit.js @@ -0,0 +1,113 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_TX_COMMIT', + opCode: 60, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1) + .writeInt(this.data.txId) + .writeByte(this.data.txLog); // use transaction log + + + // creates + var total = this.data.creates.length, + item, i; + + for (i = 0; i < total; i++) { + item = this.data.creates[i]; + this.writeByte(1); // mark the start of an entry. + this.writeByte(3); // create. + this.writeShort(item['@rid'].cluster); + this.writeLong(item['@rid'].position); + this.writeByte(constants.RECORD_TYPES[item['@type'] || 'd'] || 100); // document by default + this.writeBytes(serializer.encodeRecordData(item)); + } + + // updates + total = this.data.updates.length; + + for (i = 0; i < total; i++) { + item = this.data.updates[i]; + this.writeByte(1); // mark the start of an entry. + this.writeByte(1); // update. + this.writeShort(item['@rid'].cluster); + this.writeLong(item['@rid'].position); + this.writeByte(constants.RECORD_TYPES[item['@type'] || 'd'] || 100); // document by default + this.writeInt(item['@version'] || 0); + this.writeBytes(serializer.encodeRecordData(item)); + this.writeBoolean(true); + } + + // deletes + total = this.data.deletes.length; + + for (i = 0; i < total; i++) { + item = this.data.deletes[i]; + this.writeByte(1); // mark the start of an entry. + this.writeByte(2); // delete + this.writeShort(item['@rid'].cluster); + this.writeLong(item['@rid'].position); + this.writeByte(constants.RECORD_TYPES[item['@type'] || 'd'] || 100); // document by default + this.writeInt(item['@version'] || 0); + } + this.writeByte(0); // no more documents + this.writeString(''); + }, + reader: function () { + this + .readStatus('status') + .readInt('totalCreated') + .readArray('created', function (data) { + var items = [], + i; + for (i = 0; i < data.totalCreated; i++) { + items.push(function () { + this + .readShort('tmpCluster') + .readLong('tmpPosition') + .readShort('cluster') + .readLong('position'); + }); + } + return items; + }) + .readInt('totalUpdated') + .readArray('updated', function (data) { + var items = [], + i; + for (i = 0; i < data.totalUpdated; i++) { + items.push(function () { + this + .readShort('cluster') + .readLong('position') + .readInt('version'); + }); + } + return items; + }); + + this.readInt('totalChanges') + .readArray('changes', function (data) { + var items = [], + i; + for (i = 0; i < data.totalChanges; i++) { + items.push(function () { + this + .readLong('uuidHigh') + .readLong('uuidLow') + .readLong('fileId') + .readLong('pageIndex') + .readInt('pageOffset'); + }); + } + return items; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol26/serializer.js b/lib/transport/binary/protocol26/serializer.js new file mode 100644 index 0000000..1bb1d3e --- /dev/null +++ b/lib/transport/binary/protocol26/serializer.js @@ -0,0 +1,137 @@ +"use strict"; + +var RecordID = require('../../../recordid'); + +/** + * Serialize a record and return it as a buffer. + * + * @param {Object} content The record to serialize. + * @return {Buffer} The buffer containing the content. + */ +function encodeRecordData (content) { + return new Buffer(serializeDocument(content), 'utf8'); +} + +/** + * Serialize a document. + * + * @param {Object} document The document to serialize. + * @param {Boolean} isMap Whether to serialize the document as a map. + * @return {String} The serialized document. + */ +function serializeDocument (document, isMap) { + var result = '', + className = '', + fieldNames = Object.keys(document), + totalFields = fieldNames.length, + fieldWrap, value, field, i; + + for (i = 0; i < totalFields; i++) { + field = fieldNames[i]; + value = document[field]; + if (field === '@class') { + className = value; + } + else if (field.charAt(0) === '@') { + continue; + } + else { + if (isMap) { + fieldWrap = '"'; + } + else { + fieldWrap = ''; + } + result += fieldWrap + field + fieldWrap + ':' + serializeValue(value) + ','; + } + } + + if (className !== '') { + result = className + '@' + result; + } + + if (result[result.length - 1] === ',') { + result = result.slice(0, -1); + } + + return result; +} + +/** + * Serialize a given value according to its type. + * @param {Object} value The value to serialize. + * @return {String} The serialized value. + */ +function serializeValue (value) { + var type = typeof value; + if (type === 'string') { + return '"' + value.replace(/\\/, "\\\\").replace(/"/g, '\\"') + '"'; + } + else if (type === 'number') { + return ~value.toString().indexOf('.') ? value + 'f' : value; + } + else if (type === 'boolean') { + return value ? true : false; + } + else if (Object.prototype.toString.call(value) === '[object Date]') { + return value.getTime() + 't'; + } + else if (Array.isArray(value)) { + return serializeArray(value); + } + else if (value === Object(value)) { + return serializeObject(value); + } + else { + return ''; + } +} + + +/** + * Serialize an array of values. + * @param {Array} value The value to serialize. + * @return {String} The serialized value. + */ +function serializeArray (value) { + var result = '[', i, limit; + for (i = 0, limit = value.length; i < limit; i++) { + if (value[i] === Object(value[i])) { + result += serializeObject(value[i]); + } + else { + result += serializeValue(value[i]); + } + if (i < limit - 1) { + result += ','; + } + } + result += ']'; + return result; +} + +/** + * Serialize an object. + * @param {Object} value The value to serialize. + * @return {String} The serialized value. + */ +function serializeObject (value) { + if (value instanceof RecordID) { + return value.toString(); + } + else if (value['@type'] === 'd') { + return '(' + serializeDocument(value, false) + ')'; + } + else { + return '{' + serializeDocument(value, true) + '}'; + } +} + + + + +// export the public methods + +exports.serializeDocument = serializeDocument; +exports.serializeValue = serializeValue; +exports.encodeRecordData = encodeRecordData; \ No newline at end of file diff --git a/lib/transport/binary/protocol26/writer.js b/lib/transport/binary/protocol26/writer.js new file mode 100644 index 0000000..5c21cd2 --- /dev/null +++ b/lib/transport/binary/protocol26/writer.js @@ -0,0 +1,123 @@ +"use strict"; + +var Long = require('../../../long').Long, + constants = require('./constants'); + +/** + * Parse data to 4 bytes which represents integer value. + * + * @fixme this is a super misleading function name and comment! + * + * @param {Mixed} data The data. + * @return {Buffer} The buffer containing the data. + */ +function writeByte (data) { + return new Buffer([data]); +} + +/** + * Parse data to 4 bytes which represents integer value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeInt (data) { + var buf = new Buffer(constants.BYTES_INT); + buf.writeInt32BE(data, 0); + return buf; +} + +/** + * Parse data to 8 bytes which represents a long value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeLong (data) { + var buf = new Buffer(constants.BYTES_LONG), + value = Long.fromNumber(data); + + buf.fill(0); + buf.writeInt32BE(value.high_, 0); + buf.writeInt32BE(value.low_, constants.BYTES_INT); + + return buf; +} + +/** + * Parse data to 2 bytes which represents short value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeShort (data) { + var buf = new Buffer(constants.BYTES_SHORT); + buf.writeInt16BE(data, 0); + return buf; +} + +/** + * Write bytes to a buffer + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeBytes (data) { + var length = data.length, + buf = new Buffer(constants.BYTES_INT + length); + buf.writeInt32BE(length, 0); + data.copy(buf, constants.BYTES_INT); + return buf; +} + +/** + * Parse string data to buffer with UTF-8 encoding. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeString (data) { + if (data === null) { + return writeInt(-1); + } + var stringBuf = encodeString(data), + length = stringBuf.length, + buf = new Buffer(constants.BYTES_INT + length); + buf.writeInt32BE(length, 0); + stringBuf.copy(buf, constants.BYTES_INT, 0, stringBuf.length); + return buf; +} + +function encodeString (data) { + var length = data.length, + output = new Buffer(length * 3), // worst case, all chars could require 3-byte encodings. + j = 0, // index output + i, c; + + for (i = 0; i < length; i++) { + c = data.charCodeAt(i); + if (c < 0x80) { + // 7-bits done in one byte. + output[j++] = c; + } + else if (c < 0x800) { + // 8-11 bits done in 2 bytes + output[j++] = (0xC0 | c >> 6); + output[j++] = (0x80 | c & 0x3F); + } + else { + // 12-16 bits done in 3 bytes + output[j++] = (0xE0 | c >> 12); + output[j++] = (0x80 | c >> 6 & 0x3F); + output[j++] = (0x80 | c & 0x3F); + } + } + return output.slice(0, j); +} + +exports.writeByte = writeByte; +exports.writeBoolean = writeByte; +exports.writeBytes = writeBytes; +exports.writeShort = writeShort; +exports.writeInt = writeInt; +exports.writeLong = writeLong; +exports.writeString = writeString; \ No newline at end of file diff --git a/lib/transport/binary/protocol28/constants.js b/lib/transport/binary/protocol28/constants.js new file mode 100644 index 0000000..01d0314 --- /dev/null +++ b/lib/transport/binary/protocol28/constants.js @@ -0,0 +1,20 @@ +"use strict"; + +exports.PROTOCOL_VERSION = 28; + +exports.BYTES_LONG = 8; +exports.BYTES_INT = 4; +exports.BYTES_SHORT = 2; +exports.BYTES_BYTE = 1; + +exports.RECORD_TYPES = { + 'd': 100, + 'b': 98, + 'f': 102, + + // duplicated as upper case for fast lookup + + 'D': 100, + 'B': 98, + 'F': 102, +}; \ No newline at end of file diff --git a/lib/transport/binary/protocol28/deserializer.js b/lib/transport/binary/protocol28/deserializer.js new file mode 100644 index 0000000..2d00933 --- /dev/null +++ b/lib/transport/binary/protocol28/deserializer.js @@ -0,0 +1,554 @@ +"use strict"; + +var RID = require('../../../recordid'), + Bag = require('../../../bag'); + +/** + * Deserialize the given record and return an object containing the values. + * + * @param {String} input The serialized record. + * @param {Object} classes The optional map of class names to transformers. + * @return {Object} The deserialized record. + */ +function deserialize (input, classes) { + var record = {'@type': 'd'}, + chunk, key, value; + if (!input) { + return null; + } + chunk = eatFirstKey(input); + if (chunk[2]) { + // this is actually a class name + record['@class'] = chunk[0]; + input = chunk[1]; + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + } + else { + key = chunk[0]; + input = chunk[1]; + } + // read the first value. + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + + while (input.length) { + if (input.charAt(0) === ',') { + input = input.slice(1); + } + else { + break; + } + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + if (input.length) { + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + } + else { + record[key] = null; + } + } + + if (classes && record['@class'] && classes[record['@class']]) { + return classes[record['@class']](record); + } + else { + return record; + } +} + +/** + * Consume the first field key, which could be a class name. + * + * @param {String} input The input to parse. + * @return {[String, String]} The collected key, and any remaining input. + */ +function eatFirstKey (input) { + var length = input.length, + collected = '', + isClassName = false, + result, c, i; + + if (input.charAt(0) === '"') { + result = eatString(input.slice(1)); + return [result[0], result[1].slice(1)]; + } + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === '@') { + isClassName = true; + break; + } + else if (c === ':') { + break; + } + else { + collected += c; + } + } + return [collected, input.slice(i + 1), isClassName]; +} + + +/** + * Consume a field key, which may or may not be quoted. + * + * @param {String} input The input to parse. + * @return {[String, String]} The collected key, and any remaining input. + */ +function eatKey (input) { + var length = input.length, + collected = '', + result, c, i; + + if (input.charAt(0) === '"') { + result = eatString(input.slice(1)); + return [result[0], result[1].slice(1)]; + } + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === ':') { + break; + } + else { + collected += c; + } + } + + return [collected, input.slice(i + 1)]; +} + + + +/** + * Consume a field value. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Mixed, String]} The collected value, and any remaining input. + */ +function eatValue (input, classes) { + var c, n; + c = input.charAt(0); + while (c === ' ' && input.length) { + input = input.slice(1); + c = input.charAt(0); + } + + if (!input.length || c === ',') { + // this is a null field. + return [null, input]; + } + else if (c === '"') { + return eatString(input.slice(1)); + } + else if (c === '#') { + return eatRID(input.slice(1)); + } + else if (c === '[') { + return eatArray(input.slice(1), classes); + } + else if (c === '<') { + return eatSet(input.slice(1), classes); + } + else if (c === '{') { + return eatMap(input.slice(1), classes); + } + else if (c === '(') { + return eatRecord(input.slice(1), classes); + } + else if (c === '%') { + return eatBag(input.slice(1)); + } + else if (c === '_') { + return eatBinary(input.slice(1)); + } + else if (c === '-' || c === '0' || +c) { + return eatNumber(input); + } + else if (c === 'n' && input.slice(0, 4) === 'null') { + return [null, input.slice(4)]; + } + else if (c === 't' && input.slice(0, 4) === 'true') { + return [true, input.slice(4)]; + } + else if (c === 'f' && input.slice(0, 5) === 'false') { + return [false, input.slice(5)]; + } + else { + return [null, input]; + } +} + +/** + * Consume a string + * + * @param {String} input The input to parse. + * @return {[String, String]} The collected string, and any remaining input. + */ +function eatString (input) { + var length = input.length, + collected = '', + c, i; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === '\\') { + // escape, skip to the next character + i++; + collected += input.charAt(i); + continue; + } + else if (c === '"') { + break; + } + else { + collected += c; + } + } + + return [collected, input.slice(i + 1)]; +} + +/** + * Consume a number. + * + * If the number has a suffix, consume it also and instantiate the right type, e.g. for dates + * + * @param {String} input The input to parse. + * @return {[Mixed, String]} The collected number, and any remaining input. + */ +function eatNumber (input) { + var length = input.length, + collected = '', + pattern = /^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/, + num, c, i; + + num = input.match(pattern); + if (num) { + collected = num[0]; + i = collected.length; + } + + collected = +collected; + input = input.slice(i); + + c = input.charAt(0); + + if (c === 'a' || c === 't') { + collected = new Date(collected); + input = input.slice(1); + } + else if (c === 'b' || c === 's' || c === 'l' || c === 'f' || c == 'd' || c === 'c') { + input = input.slice(1); + } + + return [collected, input]; +} + +/** + * Consume a Record ID. + * + * @param {String} input The input to parse. + * @return {[RID, String]} The collected record id, and any remaining input. + */ +function eatRID (input) { + var length = input.length, + collected = '', + cluster, c, i; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (cluster === undefined && c === ':') { + cluster = +collected; + collected = ''; + } + else if (c === '-' || c === '0' || +c) { + collected += c; + } + else { + break; + } + } + + return [new RID({cluster: cluster, position: +collected}), input.slice(i)]; +} + + +/** + * Consume an array. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Array, String]} The collected array, and any remaining input. + */ +function eatArray (input, classes) { + var length = input.length, + array = [], + chunk, c; + + while (input.length) { + c = input.charAt(0); + if (c === ',') { + input = input.slice(1); + } + else if (c === ']') { + input = input.slice(1); + break; + } + chunk = eatValue(input, classes); + array.push(chunk[0]); + input = chunk[1]; + } + return [array, input]; +} + + +/** + * Consume a set. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Array, String]} The collected set, and any remaining input. + */ +function eatSet (input, classes) { + var length = input.length, + set = [], + chunk, c; + + while (input.length) { + c = input.charAt(0); + if (c === ',') { + input = input.slice(1); + } + else if (c === '>') { + input = input.slice(1); + break; + } + chunk = eatValue(input, classes); + set.push(chunk[0]); + input = chunk[1]; + } + + return [set, input]; +} + +/** + * Consume a map (object). + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Object, String]} The collected map, and any remaining input. + */ +function eatMap (input, classes) { + var length = input.length, + map = {}, + key, value, chunk, c; + + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + if (c === ',') { + input = input.slice(1); + } + else if (c === '}') { + input = input.slice(1); + break; + } + + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + if (input.length) { + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + map[key] = value; + } + else { + map[key] = null; + } + } + + return [map, input]; +} + +/** + * Consume an embedded record. + * + * @param {String} input The input to parse. + * @param {Object} classes The optional map of class names to transformers. + * @return {[Object, String]} The collected record, and any remaining input. + */ +function eatRecord (input, classes) { + var record = {'@type': 'd'}, + chunk, c, key, value; + + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + else if (c === ')') { + // empty record. + input = input.slice(1); + return [record, input]; + } + else { + break; + } + } + + chunk = eatFirstKey(input); + + if (chunk[2]) { + // this is actually a class name + record['@class'] = chunk[0]; + input = chunk[1]; + chunk = eatKey(input); + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + else if (c === ')') { + // empty record. + input = input.slice(1); + return [record, input]; + } + else { + break; + } + } + key = chunk[0]; + input = chunk[1]; + } + else { + key = chunk[0]; + input = chunk[1]; + } + + // read the first value. + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + + while (input.length) { + c = input.charAt(0); + if (c === ' ') { + input = input.slice(1); + continue; + } + if (c === ',') { + input = input.slice(1); + } + else if (c === ')') { + input = input.slice(1); + break; + } + chunk = eatKey(input); + key = chunk[0]; + input = chunk[1]; + if (input.length) { + chunk = eatValue(input, classes); + value = chunk[0]; + input = chunk[1]; + record[key] = value; + } + else { + record[key] = null; + } + } + + if (classes && record['@class'] && classes[record['@class']]) { + record = classes[record['@class']](record); + } + + return [record, input]; +} + +/** + * Consume a RID Bag. + * + * @param {String} input The input to parse. + * @return {[Object, String]} The collected bag, and any remaining input. + */ +function eatBag (input) { + var length = input.length, + collected = '', + i, bag, chunk, c; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === ';') { + break; + } + else { + collected += c; + } + } + input = input.slice(i + 1); + + if (exports.enableRIDBags) { + return [new Bag(collected), input]; + } + else { + return [new Bag(collected).all(), input]; + } +} + + +/** + * Consume a binary buffer. + * + * @param {String} input The input to parse. + * @return {[Object, String]} The collected bag, and any remaining input. + */ +function eatBinary (input) { + var length = input.length, + collected = '', + i, bag, chunk, c; + + for (i = 0; i < length; i++) { + c = input.charAt(i); + if (c === '_' || c === ',' || c === ')' || c === '>' || c === '}' || c === ']') { + break; + } + else { + collected += c; + } + } + input = input.slice(i + 1); + + return [new Buffer(collected, 'base64'), input]; +} + + +exports.enableRIDBags = true; +exports.deserialize = deserialize; +exports.eatKey = eatKey; +exports.eatValue = eatValue; +exports.eatString = eatString; +exports.eatNumber = eatNumber; +exports.eatRID = eatRID; +exports.eatArray = eatArray; +exports.eatSet = eatSet; +exports.eatMap = eatMap; +exports.eatRecord = eatRecord; +exports.eatBag = eatBag; +exports.eatBinary = eatBinary; diff --git a/lib/transport/binary/protocol28/index.js b/lib/transport/binary/protocol28/index.js new file mode 100644 index 0000000..f022070 --- /dev/null +++ b/lib/transport/binary/protocol28/index.js @@ -0,0 +1,7 @@ +"use strict"; +exports.Operation = require('./operation'); +exports.OperationQueue = require('./operation-queue'); +exports.constants = require('./constants'); +exports.serializer = require('./serializer'); +exports.deserializer = require('./deserializer'); +exports.operations = require('./operations'); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operation-queue.js b/lib/transport/binary/protocol28/operation-queue.js new file mode 100644 index 0000000..d29b353 --- /dev/null +++ b/lib/transport/binary/protocol28/operation-queue.js @@ -0,0 +1,184 @@ +"use strict"; + +var Promise = require('bluebird'), + Operation = require('./operation'), + operations = require('./operations'), + Emitter = require('events').EventEmitter, + errors = require('../../../errors'), + util = require('util'); + +function OperationQueue (socket) { + this.socket = socket || null; + this.items = []; + this.writes = []; + this.remaining = null; + if (socket) { + this.bindToSocket(); + } + Emitter.call(this); +} + +util.inherits(OperationQueue, Emitter); + +module.exports = OperationQueue; + +/** + * Add an operation to the queue. + * + * @param {String|Operation} op The operation name or instance. + * @param {Object} params The parameters for the operation, if op is a string. + * @promise {Object} The result of the operation. + */ +OperationQueue.prototype.add = function (op, params) { + if (typeof op === 'string') { + op = new operations[op](params || {}); + } + var deferred = Promise.defer(), + buffer; + // define the write operations + op.writer(); + // define the read operations + op.reader(); + buffer = op.buffer(); + if (this.socket) { + this.socket.write(buffer); + } + else { + this.writes.push(buffer); + } + if (op.id === 'REQUEST_DB_CLOSE') { + deferred.resolve({}); + } + else { + this.items.push([op, deferred]); + } + return deferred.promise; +}; + +/** + * Cancel all the operations in the queue. + * + * @param {Error} err The error object, if any. + * @return {OperationQueue} The now empty queue. + */ +OperationQueue.prototype.cancel = function (err) { + var item, op, deferred; + while ((item = this.items.shift())) { + op = item[0]; + deferred = item[1]; + deferred.reject(err); + } + return this; +}; + +/** + * Bind to events on the socket. + */ +OperationQueue.prototype.bindToSocket = function (socket) { + var total, i; + if (socket) { + this.socket = socket; + } + this.socket.on('data', this.handleChunk.bind(this)); + if ((total = this.writes.length)) { + if (this.socket.connected) { + for (i = 0; i < total; i++) { + this.socket.write(this.writes[i]); + } + this.writes = []; + } + else { + this.socket.once('connect', function () { + var total = this.writes.length, + i; + for (i = 0; i < total; i++) { + this.socket.write(this.writes[i]); + } + this.writes = []; + }.bind(this)); + } + } +}; + +/** + * Unbind from socket events. + */ +OperationQueue.prototype.unbindFromSocket = function () { + this.socket.removeAllListeners('data'); + delete this.socket; +}; + +/** + * Handle a chunk of data from the socket and attempt to process it. + * + * @param {Buffer} data The data received from the server. + */ +OperationQueue.prototype.handleChunk = function (data) { + var buffer, result, offset; + if (this.remaining) { + buffer = new Buffer(this.remaining.length + data.length); + this.remaining.copy(buffer); + data.copy(buffer, this.remaining.length); + } + else { + buffer = data; + } + offset = this.process(buffer); + if (buffer.length - offset === 0) { + this.remaining = null; + } + else { + this.remaining = buffer.slice(offset); + } +}; + +/** + * Process the operations in the queue against the given buffer. + * + * + * @param {Buffer} buffer The buffer to process. + * @param {Integer} offset The offset to start processing from, defaults to 0. + * @return {Integer} The offset that was successfully read up to. + */ +OperationQueue.prototype.process = function (buffer, offset) { + var code, parsed, result, status, item, op, deferred, err; + offset = offset || 0; + while ((item = this.items.shift())) { + op = item[0]; + deferred = item[1]; + parsed = op.consume(buffer, offset); + status = parsed[0]; + offset = parsed[1]; + result = parsed[2]; + if (status === Operation.READING) { + // operation is incomplete, buffer does not contain enough data + this.items.unshift(item); + return offset; + } + else if (status === Operation.PUSH_DATA) { + this.emit('update-config', result); + this.items.unshift(item); + return offset; + } + else if (status === Operation.COMPLETE) { + deferred.resolve(result); + } + else if (status === Operation.ERROR) { + if (result.status.error) { + // this is likely a recoverable error + deferred.reject(result.status.error); + } + else { + // cannot recover, reject everything and let the application decide what to do + err = new errors.Protocol('Unknown Error on operation id ' + op.id, result); + deferred.reject(err); + this.cancel(err); + this.emit('error', err); + } + } + else { + deferred.reject(new errors.Protocol('Unsupported operation status: ' + status)); + } + } + return offset; +}; \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operation.js b/lib/transport/binary/protocol28/operation.js new file mode 100644 index 0000000..9a75b4f --- /dev/null +++ b/lib/transport/binary/protocol28/operation.js @@ -0,0 +1,956 @@ +"use strict"; + +var constants = require('./constants'), + utils = require('../../../utils'), + Long = require('../../../long').Long, + errors = require('../../../errors'), + deserializer = require('./deserializer'); + + +/** + * # Operations + * + * The base class for operations, provides a simple DSL for defining + * the steps required to send a command to the server, and then read + * the response. + * + * Each operation should implement the `writer()` and `reader()` methods. + * + * @param {Object} data The data for the operation. + */ +function Operation (data) { + this.status = Operation.PENDING; + this.writeOps = []; + this.readOps = []; + this.stack = [{}]; + this.data = data || {}; +} + +module.exports = Operation; + +// operation statuses + +var statuses = require('../operation-status'); + +Operation.PENDING = statuses.PENDING; +Operation.WRITTEN = statuses.WRITTEN; +Operation.READING = statuses.READING; +Operation.COMPLETE = statuses.COMPLETE; +Operation.ERROR = statuses.ERROR; +Operation.PUSH_DATA = statuses.PUSH_DATA; +Operation.LIVE_RESULT = statuses.LIVE_RESULT; + + +// make it easy to inherit from the base class +Operation.extend = utils.extend; + +/** + * Declares the commands to send to the server. + * Child classes should implement this function. + */ +Operation.prototype.writer = function () { + +}; + +/** + * Declares the steps required to recieve data for the operation. + * Child classes should implement this function. + */ +Operation.prototype.reader = function () { + +}; + +/** + * Prepare the buffer for the operation. + * + * @return {Buffer} The buffer containing the commands to send to the server. + */ +Operation.prototype.buffer = function () { + + if (!this.writeOps.length) { + this.writer(); + } + + var total = this.writeOps.length, + size = 0, + commands = [], + item, i, fn, offset, data, buffer; + + for (i = 0; i < total; i++) { + item = this.writeOps[i]; + offset = size; + commands.push([item[1], offset]); + size += item[0]; + } + + buffer = new Buffer(size); + + for (i = 0; i < total; i++) { + item = commands[i]; + fn = item[0]; + offset = item[1]; + fn(buffer, offset); + } + + return buffer; +}; + +/** + * Write a request header. + * + * @param {Integer} opCode The operation code. + * @param {Integer} sessionId The session ID. + * @param {Buffer} token The token, if any. + * @return {Operation} The operation instance. + */ +Operation.prototype.writeHeader = function (opCode, sessionId, token) { + this + .writeByte(opCode) + .writeInt(sessionId || -1); + if (token && token.length) { + this.writeBytes(token); + } + return this; +}; + +/** + * Write a byte. + * + * @param {Mixed} data The data. + * @return {Operation} The operation instance. + */ +Operation.prototype.writeByte = function (data) { + this.writeOps.push([1, function (buffer, offset) { + buffer[offset] = data; + }]); + return this; +}; + +/** + * Write a boolean. + * + * @param {Mixed} data The data. + * @return {Operation} The operation instance. + */ +Operation.prototype.writeBoolean = function (data) { + this.writeOps.push([1, function (buffer, offset) { + buffer[offset] = data ? 1 : 0; + }]); + return this; +}; + + +/** + * Write a single character. + * + * @param {Mixed} data The data. + * @return {Operation} The operation instance. + */ +Operation.prototype.writeChar = function (data) { + this.writeOps.push([1, function (buffer, offset) { + buffer[offset] = (''+data).charCodeAt(0); + }]); + return this; +}; + +/** + * Parse data to 4 bytes which represents integer value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeInt = function (data) { + this.writeOps.push([constants.BYTES_INT, function (buffer, offset) { + buffer.fill(0, offset, offset + constants.BYTES_INT); + buffer.writeInt32BE(data, offset); + }]); + return this; +}; + +/** + * Parse data to 8 bytes which represents a long value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeLong = function (data) { + this.writeOps.push([constants.BYTES_LONG, function (buffer, offset) { + data = Long.fromNumber(data); + buffer.fill(0, offset, offset + constants.BYTES_LONG); + buffer.writeInt32BE(data.high_, offset); + buffer.writeInt32BE(data.low_, offset + constants.BYTES_INT); + + }]); + return this; +}; + +/** + * Parse data to 2 bytes which represents short value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeShort = function (data) { + this.writeOps.push([constants.BYTES_SHORT, function (buffer, offset) { + buffer.fill(0, offset, offset + constants.BYTES_SHORT); + buffer.writeInt16BE(data, offset); + }]); + return this; +}; + +/** + * Write bytes to a buffer + * @param {Buffer} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeBytes = function (data) { + this.writeOps.push([constants.BYTES_INT + data.length, function (buffer, offset) { + buffer.writeInt32BE(data.length, offset); + data.copy(buffer, offset + constants.BYTES_INT); + }]); + return this; +}; + +/** + * Parse string data to buffer with UTF-8 encoding. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +Operation.prototype.writeString = function (data) { + if (data == null) { + return this.writeInt(-1); + } + var encoded = encodeString(data), + length = encoded.length; + this.writeOps.push([constants.BYTES_INT + length, function (buffer, offset) { + buffer.writeInt32BE(length, offset); + encoded.copy(buffer, offset + constants.BYTES_INT); + }]); + return this; +}; + +function encodeString (data) { + var length = data.length, + output = new Buffer(length * 3), // worst case, all chars could require 3-byte encodings. + j = 0, // index output + i, c; + + for (i = 0; i < length; i++) { + c = data.charCodeAt(i); + if (c < 0x80) { + // 7-bits done in one byte. + output[j++] = c; + } + else if (c < 0x800) { + // 8-11 bits done in 2 bytes + output[j++] = (0xC0 | c >> 6); + output[j++] = (0x80 | c & 0x3F); + } + else { + // 12-16 bits done in 3 bytes + output[j++] = (0xE0 | c >> 12); + output[j++] = (0x80 | c >> 6 & 0x3F); + output[j++] = (0x80 | c & 0x3F); + } + } + return output.slice(0, j); +} + +// # Read Operations + + +/** + * Read a status from the server response. + * If the status contains an error, that error + * will be read instead of any subsequently queued commands. + * + * @param {String} fieldName The name of the data field to populate. + * @param {Function} reader The function that should be invoked after this value is read. if any. + * @return {Operation} The operation instance. + */ +Operation.prototype.readStatus = function (fieldName, reader) { + var value = {}; + fieldName = fieldName || 'status'; + + this.readOps.push(function (data) { + data[fieldName] = value; + this.stack.push(data[fieldName]); + }); + this.readByte('code'); + if (this.opCode !== 2 && this.opCode !== 3 && this.data.token && this.data.token.length) { + this.readInt('sessionId'); + this.readBytes('token', next); + } + else { + this.readInt('sessionId', next); + } + return this; + function next (data) { + if (data.code === 1) { + this.readError('error', function () { + if (reader) { + reader.call(this, value, fieldName); + } + this.readOps.push(function () { + this.stack.pop(); + }); + }); + } + else { + if (reader) { + reader.call(this, value, fieldName); + } + this.stack.pop(); + } + } +}; + +/** + * Read an error from the server response. + * Any subsequently queued commands will not run. + * + * @param {String} fieldName The name of the data field to populate. + * @param {Function} reader The function that should be invoked after this value is read. if any. + * @return {Operation} The operation instance. + */ +Operation.prototype.readError = function (fieldName, reader) { + this.readOps = [['Error', [fieldName, reader]]]; + return this; +}; + +/** + * Read an object from the server response. + * This is the same as `readString` but deserializes the returned string + * into an object. + * + * @param {String} fieldName The name of the data field to populate. + * @param {Function} reader The function that should be invoked after this value is read. if any. + * @return {Operation} The operation instance. + */ +Operation.prototype.readObject = function (fieldName, reader) { + this.readOps.push(['String', [fieldName, function (data, fieldName) { + data[fieldName] = deserializer.deserialize(data[fieldName], this.data.transformerFunctions); + if (reader) { + reader.call(this, data, fieldName); + } + }]]); + return this; +}; + + +// Add the `readByte`, `readInt` etc methods. +// these are just shortcuts + +[ + 'Byte', + 'Bytes', + 'Int', + 'Short', + 'Long', + 'String', + 'Array', + 'Record', + 'Char', + 'Boolean', + 'Collection' +] +.forEach(function (name) { + this['read' + name] = function (fieldName, reader) { + this.readOps.push([name, arguments]); + return this; + }; +}, Operation.prototype); + + +/** + * Consume the buffer starting from the given offset. + * Returns an array containing the operation status, the + * new offset and any collected result. + * + * If the buffer doesn't contain enough data for the operation + * to complete, it will process as much as possible and return + * a partial result with a status code of `Operation.READING` meaning + * that the operation is still in the reading state. + * + * If the operation completes successfully, the status code will be + * `Operation.COMPLETE`. + * + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset + * @return {Array} The array containing the status, new offset and result. + */ +Operation.prototype.consume = function (buffer, offset) { + var obj, code, context, item, type, args; + offset = offset || 0; + if (this.status === Operation.PENDING) { + // this is the first time consume has been called for this + // operation. We need to determine whether the response + // we're reading is really for us or whether it's a + // PUSH_DATA command. + if (buffer.length < offset + 1) { + // not enough bytes in the buffer to check. + return [Operation.READING, offset, {}]; + } + + code = buffer.readUInt8(offset); + if (code === 3) { + var sessionId = buffer.readInt32BE(offset+1); + var pushType = buffer.readUInt8(offset + 5); + + if (pushType === 81) { + offset += 6; // ignore the next integer + var length = buffer.readInt32BE(offset); + offset+=4; + var operation = buffer.readUInt8(offset); + offset+=1; + var token = buffer.readInt32BE(offset); + offset+=4; + var recordType = buffer.readUInt8(offset); + offset+=1; + var version = buffer.readInt32BE(offset); + offset+=4; + var clusterId = buffer.readInt16BE(offset); + offset+=2; + var clusterPosition = Long.fromBits( + buffer.readUInt32BE(offset + 4), + buffer.readInt32BE(offset) + ).toNumber(); + offset+=8; + var contentLenght = buffer.readInt32BE(offset); + offset+=4; + + var asString = buffer.toString('utf8', offset, offset + contentLenght); + offset += contentLenght; + var content = deserializer.deserialize(asString, this.data.transformerFunctions); + + return [ + Operation.LIVE_RESULT, + token, + operation, + { + content: content, + type: 'd', + cluster: clusterId, + position: clusterPosition, + version: version + }, + offset + ]; + + } else { + offset += 5; // ignore the next integer + obj = {}; + offset += this.parsePushedData(buffer, offset, obj, 'data'); + return [Operation.PUSH_DATA, offset, obj.data]; + } + } + + this.status = Operation.READING; + } + if (this.readOps.length === 0) { + return [Operation.COMPLETE, offset, this.stack[0]]; + } + while ((item = this.readOps.shift())) { + context = this.stack[this.stack.length - 1]; + if (typeof item === 'function') { + // this is a nop, just execute it. + item.call(this, context, buffer, offset); + continue; + } + type = item[0]; + args = item[1]; + if (!this.canRead(type, buffer, offset)) { + // not enough bytes in the buffer, operation is still reading. + this.readOps.unshift(item); + return [Operation.READING, offset, this.stack[0]]; + } + offset += this['parse' + type](buffer, offset, context, args[0], args[1]); + } + if (this.stack[0] && this.stack[0].status && this.stack[0].status.code) { + return [Operation.ERROR, offset, this.stack[0]]; + } + else { + return [Operation.COMPLETE, offset, this.stack[0]]; + } +}; + +/** + * Defetermine whether the operation can read a value of the given + * type from the buffer at the given offset. + * + * @param {String} type The value type. + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading at. + * @return {Boolean} true if the value can be read. + */ +Operation.prototype.canRead = function (type, buffer, offset) { + var length = buffer.length; + if (offset > length) { + return false; + } + switch (type) { + case 'Array': + case 'Error': + return true; + case 'Byte': + case 'Char': + case 'Boolean': + return length >= offset + 1; + case 'Short': + case 'Record': + return length >= offset + constants.BYTES_SHORT; + case 'Long': + return length >= offset + constants.BYTES_LONG; + case 'Int': + case 'Collection': + return length >= offset + constants.BYTES_INT; + case 'Bytes': + case 'String': + if (length < offset + constants.BYTES_INT) { + return false; + } + else { + return length >= offset + constants.BYTES_INT + buffer.readInt32BE(offset); + } + break; + default: + return false; + } +}; + + +/** + * Parse a byte from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseByte = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = buffer.readUInt8(offset); + if (reader) { + reader.call(this, context, fieldName); + } + return 1; +}; + +/** + * Parse a character from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseChar = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = String.fromCharCode(buffer.readUInt8(offset)); + if (reader) { + reader.call(this, context, fieldName); + } + return 1; +}; + +/** + * Parse a boolean from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseBoolean = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = Boolean(buffer.readUInt8(offset)); + if (reader) { + reader.call(this, context, fieldName); + } + return 1; +}; + + +/** + * Parse a short from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseShort = function (buffer, offset, context, fieldName, reader) { + + context[fieldName] = buffer.readInt16BE(offset); + if (reader) { + reader.call(this, context, fieldName); + } + return constants.BYTES_SHORT; +}; + +/** + * Parse an integer from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseInt = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = buffer.readInt32BE(offset); + if (reader) { + reader.call(this, context, fieldName); + } + return constants.BYTES_INT; +}; + +/** + * Parse a long from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseLong = function (buffer, offset, context, fieldName, reader) { + context[fieldName] = Long + .fromBits( + buffer.readUInt32BE(offset + constants.BYTES_INT), + buffer.readInt32BE(offset) + ) + .toNumber(); + + if (reader) { + reader.call(this, context, fieldName); + } + return constants.BYTES_LONG; +}; + +/** + * Parse some bytes from the given buffer at the given offset and + * insert them into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseBytes = function (buffer, offset, context, fieldName, reader) { + var length = buffer.readInt32BE(offset); + offset += constants.BYTES_INT; + if (length < 0) { + context[fieldName] = null; + } + else { + context[fieldName] = buffer.slice(offset, offset + length); + } + if (reader) { + reader.call(this, context, fieldName); + } + return length > 0 ? length + constants.BYTES_INT : constants.BYTES_INT; +}; + + +/** + * Parse a string from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseString = function (buffer, offset, context, fieldName, reader) { + var length = buffer.readInt32BE(offset); + offset += constants.BYTES_INT; + if (length < 0) { + context[fieldName] = null; + } + else { + context[fieldName] = buffer.toString('utf8', offset, offset + length); + } + if (reader) { + reader.call(this, context, fieldName); + } + return length > 0 ? length + constants.BYTES_INT : constants.BYTES_INT; +}; + +/** + * Parse a record from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseRecord = function (buffer, offset, context, fieldName, reader) { + var remainingOps = this.readOps, + record = {}; + this.readOps = []; + this.stack.push(record); + if (Array.isArray(context[fieldName])) { + context[fieldName].push(record); + } + else { + context[fieldName] = record; + } + this.readShort('classId', function (record, fieldName) { + if (record[fieldName] === -1) { + record.value = new errors.Protocol('No class for record, cannot proceed.'); + this.stack.pop(); + this.readOps.push(function () { + if (reader) { + reader.call(this, context, fieldName); + } + }); + this.readOps.push.apply(this.readOps, remainingOps); + return; + } + else if (record[fieldName] === -2) { + record.value = null; + this.stack.pop(); + this.readOps.push(function () { + if (reader) { + reader.call(this, context, fieldName); + } + }); + this.readOps.push.apply(this.readOps, remainingOps); + return; + } + else if (record[fieldName] === -3) { + record.type = 'd'; + this + .readShort('cluster') + .readLong('position') + .readOps.push(function () { + this.stack.pop(); + this.readOps.push(function () { + if (reader) { + reader.call(this, context, fieldName); + } + }); + this.readOps.push.apply(this.readOps, remainingOps); + }); + } + else if (record[fieldName] > -1) { + this + .readChar('type') + .readShort('cluster') + .readLong('position') + .readInt('version') + .readString('value', function (data, key) { + data[key] = deserializer.deserialize(data[key], this.data.transformerFunctions); + this.stack.pop(); + this.readOps.push(function () { + if (reader) { + reader.call(this, context, fieldName); + } + }); + this.readOps.push.apply(this.readOps, remainingOps); + }); + } + }); + + + + return 0; +}; + + +/** + * Parse a collection from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseCollection = function (buffer, offset, context, fieldName, reader) { + var remainingOps = this.readOps, + records = [], + total = buffer.readInt32BE(offset), + i; + offset += 4; + this.readOps = []; + context[fieldName] = records; + for (i = 0; i < total; i++) { + this.readRecord(fieldName); + } + if (reader) { + this.readOps.push(function () { + reader.call(this, records); + }); + } + this.readOps.push.apply(this.readOps, remainingOps); + return 4; +}; + + +/** + * Parse an array from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * > Note. this differs from the other `parseXYZ` methods in that `reader` + * is required, and MUST return an array of functions. Each function in the + * array represents a 'scope' for an item in the array and will be invoked in order. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseArray = function (buffer, offset, context, fieldName, reader) { + var items = reader.call(this, context), + remainingOps = this.readOps; + + this.readOps = []; + + context[fieldName] = []; + this.stack.push(context[fieldName]); + + items.map(function (item) { + var childContext = {}; + context[fieldName].push(childContext); + this.readOps.push(function () { + this.stack.push(childContext); + }); + + item.call(this, childContext); + + this.readOps.push(function () { + this.stack.pop(); + }); + }, this); + + + this.readOps.push(function () { + this.stack.pop(); + }); + + this.readOps.push.apply(this.readOps, remainingOps); + return 0; +}; + +/** + * Parse an error from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * > Note: this implementation differs from the others in that + * when an error is encountered, any subsequent `readXYZ()` commands + * that were due to be run will be skipped. + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parseError = function (buffer, offset, context, fieldName, reader) { + var err = new errors.Request(); + err.previous = []; + // remove any ops we were expecting to run. + this.readOps = []; + + context[fieldName] = err; + this.stack.push(err); + + if (this.opCode === 3 && this.data.token) { + this.readBytes('token'); + } + + this.readByte('id'); + + + function readItem () { + this.readString('type'); + this.readString('message'); + this.readByte('hasMore', function (data) { + var prev; + if (data.hasMore) { + prev = new errors.Request(); + prev.type = data.type; + prev.message = data.message; + err.previous.push(prev); + this.stack.pop(); + this.stack.push(prev); + readItem.call(this); + } + else { + err.type = data.type; + err.message = data.message; + this.readBytes('javaStackTrace', function (data) { + this.readOps.push(function (data) { + this.stack.pop(); + }); + }); + } + }); + } + readItem.call(this); + if (reader) { + reader.call(this, context, fieldName); + } + return 0; +}; + + +/** + * Parse any pushed data from the given buffer at the given offset and + * insert it into the context under the given field name. + * + * + * @param {Buffer} buffer The buffer to read from. + * @param {Integer} offset The offset to start reading from. + * @param {Object} context The context to add the value to. + * @param {String} fieldName The name of the field in the context. + * @param {Function} reader The function that should be invoked after the value is read, if any. + * @return {Integer} The number of bytes read. + */ +Operation.prototype.parsePushedData = function (buffer, offset, context, fieldName, reader) { + var length = buffer.readInt32BE(offset), + asString; + offset += constants.BYTES_INT; + asString = buffer.toString('utf8', offset, offset + length); + switch (asString.charAt(0)) { + case 'R': + context[fieldName] = deserializer.deserialize(asString.slice(1), this.data.transformerFunctions); + break; + default: + console.log('unsupported pushed data format: ' + asString); + } + if (reader) { + reader.call(this, context, fieldName); + } + return length + constants.BYTES_INT; +}; + diff --git a/lib/transport/binary/protocol28/operations/command.js b/lib/transport/binary/protocol28/operations/command.js new file mode 100644 index 0000000..abf5024 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/command.js @@ -0,0 +1,188 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + serializer = require('../serializer'), + writer = require('../writer'), + RID = require('../../../../recordid'), + utils = require('../../../../utils'); + +module.exports = Operation.extend({ + id: 'REQUEST_COMMAND', + opCode: 41, + writer: function () { + if (this.data.mode === 'a' && !this.data.class) { + this.data.class = 'com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery'; + } + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeChar(this.data.mode || 's') + .writeBytes(this.serializeQuery()); + + }, + serializeQuery: function () { + var buffers = [writer.writeString(this.data.class)]; + + var text = this.data.query; + var params = this.data.params; + // if there are bound parameters, force prepare them, OrientDB's support is limited in this version. + if (this.data.db && this.data.db.forcePrepare && params && params.params) { + text = utils.prepare(text, params.params); + params = undefined; + } + + if (this.data.class === 'q' || + this.data.class === 'com.orientechnologies.orient.core.sql.query.OSQLSynchQuery' || + this.data.class === 'com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery') { + buffers.push( + writer.writeString(text), + writer.writeInt(this.data.limit), + writer.writeString(this.data.fetchPlan || '') + ); + + if (params) { + buffers.push(writer.writeString(serializeParams(params))); + } + else { + buffers.push(writer.writeInt(0)); + } + } + else if ( + this.data.class === 's' || + this.data.class === 'com.orientechnologies.orient.core.command.script.OCommandScript') { + buffers.push( + writer.writeString(this.data.language || 'sql'), + writer.writeString(text) + ); + if (params && params.params && Object.keys(params.params).length) { + buffers.push( + writer.writeBoolean(true), + writer.writeString(serializeParams(params)) + ); + } + else { + buffers.push(writer.writeBoolean(false)); + } + buffers.push(writer.writeByte(0)); + } + else { + buffers.push(writer.writeString(text)); + if (params) { + buffers.push( + writer.writeBoolean(true), + writer.writeString(serializeParams(params)) + ); + } + else { + buffers.push(writer.writeBoolean(false)); + } + buffers.push(writer.writeBoolean(false)); + } + return Buffer.concat(buffers); + }, + reader: function () { + this + .readStatus('status') + .readCommandResult('results'); + }, + readCommandResult: function (fieldName, reader) { + this.payloads = []; + this.readOps.push(function (data) { + data[fieldName] = this.payloads; + this.stack.push(data[fieldName]); + this.readPayload('payloadStatus', function () { + this.stack.pop(); + }); + }); + return this; + }, + readPayload: function (payloadFieldName, reader) { + + return this.readByte(payloadFieldName, function (data, fieldName) { + var record = {}; + switch (data[fieldName]) { + case 0: + if (reader) { + reader.call(this); + } + break; + case 110: // null + record.type = 'r'; + record.content = null; + this.payloads.push(record); + this.readPayload(payloadFieldName, reader); + break; + case 1: + case 114: + // a record + record.type = 'r'; + this.payloads.push(record); + this.stack.push(record); + this.readRecord('content', function () { + this.stack.pop(); + this.readPayload(payloadFieldName, reader); + }); + break; + case 2: + // prefeteched record + record.type = 'p'; + this.payloads.push(record); + this.stack.push(record); + this.readRecord('content', function (data) { + this.stack.pop(); + this.readPayload(payloadFieldName, reader); + }); + break; + case 97: + // serialized result + record.type = 'f'; + this.payloads.push(record); + this.stack.push(record); + this.readString('content', function () { + this.stack.pop(); + this.readPayload(payloadFieldName, reader); + }); + break; + case 108: + // collection of records + record.type = 'l'; + this.payloads.push(record); + this.stack.push(record); + this.readCollection('content', function (data) { + this.stack.pop(); + this.readPayload(payloadFieldName, reader); + }); + break; + default: + reader.call(this); + } + }); + } +}); + +/** + * Serialize the parameters for a query. + * + * > Note: There is a bug in OrientDB where special kinds of string values + * > need to be twice quoted *in parameters*. Hence the need for this specialist function. + * + * @param {Object} data The data to serialize. + * @return {String} The serialized data. + */ +function serializeParams (data) { + var keys = Object.keys(data.params || {}), + total = keys.length, + c, i, key, value; + + for (i = 0; i < total; i++) { + key = keys[i]; + value = data.params[key]; + if (typeof value === 'string') { + c = value.charAt(0); + if (c === '.' || c === '#' || c === '<' || c === '[' || c === '(' || c === '{' || c === '0' || +c) { + data.params[key] = '"' + value + '"'; + } + } + } + return serializer.serializeDocument(data); +} \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/config-get.js b/lib/transport/binary/protocol28/operations/config-get.js new file mode 100644 index 0000000..8a61829 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/config-get.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_CONFIG_GET', + opCode: 70, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeString(this.data.key); + }, + reader: function () { + this + .readStatus('status') + .readString('value'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/config-list.js b/lib/transport/binary/protocol28/operations/config-list.js new file mode 100644 index 0000000..ff7ba23 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/config-list.js @@ -0,0 +1,29 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_CONFIG_LIST', + opCode: 72, + writer: function () { + this.writeHeader(this.opCode, this.data.sessionId, this.data.token); + }, + reader: function () { + this + .readStatus('status') + .readShort('total') + .readArray('items', function (data) { + var items = [], + i; + for (i = 0; i < data.total; i++) { + items.push(function () { + this + .readString('key') + .readString('value'); + }); + } + return items; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/config-set.js b/lib/transport/binary/protocol28/operations/config-set.js new file mode 100644 index 0000000..5ff03ad --- /dev/null +++ b/lib/transport/binary/protocol28/operations/config-set.js @@ -0,0 +1,22 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_CONFIG_SET', + opCode: 71, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeString(this.data.key) + .writeString(this.data.value); + }, + reader: function () { + this + .readStatus('status') + .readOps.push(function (data) { + data.success = true; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/connect.js b/lib/transport/binary/protocol28/operations/connect.js new file mode 100644 index 0000000..d0b823a --- /dev/null +++ b/lib/transport/binary/protocol28/operations/connect.js @@ -0,0 +1,29 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + npmPackage = require('../../../../../package.json'); + +module.exports = Operation.extend({ + id: 'REQUEST_CONNECT', + opCode: 2, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1) + .writeString(npmPackage.name) + .writeString(npmPackage.version) + .writeShort(+constants.PROTOCOL_VERSION) + .writeString('') // client id + .writeString('ORecordDocument2csv') // serialization format + .writeBoolean(this.data.useToken) // use JWT? + .writeString(this.data.username) + .writeString(this.data.password); + }, + reader: function () { + this + .readStatus('status') + .readInt('sessionId') + .readBytes('token'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/datacluster-add.js b/lib/transport/binary/protocol28/operations/datacluster-add.js new file mode 100644 index 0000000..31143d1 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/datacluster-add.js @@ -0,0 +1,20 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DATACLUSTER_ADD', + opCode: 10, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeString(this.data.name) + .writeShort(this.data.id || -1); + }, + reader: function () { + this + .readStatus('status') + .readShort('id'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/datacluster-count.js b/lib/transport/binary/protocol28/operations/datacluster-count.js new file mode 100644 index 0000000..4ec8334 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/datacluster-count.js @@ -0,0 +1,33 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DATACLUSTER_COUNT', + opCode: 12, + writer: function () { + var total, item, i; + + this.writeHeader(this.opCode, this.data.sessionId, this.data.token); + + if (Array.isArray(this.data.id)) { + total = this.data.id.length; + this.writeShort(total); + for (i = 0; i < total; i++) { + this.writeShort(this.data.id[i]); + } + } + else { + this + .writeShort(1) + .writeShort(this.data.id); + } + this.writeByte(this.data.tombstones || false); + }, + reader: function () { + this + .readStatus('status') + .readLong('count'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/datacluster-datarange.js b/lib/transport/binary/protocol28/operations/datacluster-datarange.js new file mode 100644 index 0000000..7f18fef --- /dev/null +++ b/lib/transport/binary/protocol28/operations/datacluster-datarange.js @@ -0,0 +1,22 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DATACLUSTER_DATARANGE', + opCode: 13, + writer: function () { + var total, i; + + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeShort(this.data.id); + }, + reader: function () { + this + .readStatus('status') + .readLong('begin') + .readLong('end'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/datacluster-drop.js b/lib/transport/binary/protocol28/operations/datacluster-drop.js new file mode 100644 index 0000000..3349759 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/datacluster-drop.js @@ -0,0 +1,21 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DATACLUSTER_DROP', + opCode: 11, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeShort(this.data.id); + }, + reader: function () { + this + .readStatus('status') + .readByte('success', function (data, fieldName) { + data[fieldName] = Boolean(data[fieldName]); + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/db-close.js b/lib/transport/binary/protocol28/operations/db-close.js new file mode 100644 index 0000000..b76bf21 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-close.js @@ -0,0 +1,13 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_CLOSE', + opCode: 5, + writer: function () { + this.writeHeader(this.opCode, this.data.sessionId, this.data.token); + }, + reader: function () {} +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/db-countrecords.js b/lib/transport/binary/protocol28/operations/db-countrecords.js new file mode 100644 index 0000000..2034665 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-countrecords.js @@ -0,0 +1,17 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_COUNTRECORDS', + opCode: 9, + writer: function () { + this.writeHeader(this.opCode, this.data.sessionId, this.data.token); + }, + reader: function () { + this + .readStatus('status') + .readLong('count'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/db-create.js b/lib/transport/binary/protocol28/operations/db-create.js new file mode 100644 index 0000000..19e5215 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-create.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_CREATE', + opCode: 4, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeString(this.data.name) + .writeString(this.data.type || 'graph') + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/db-delete.js b/lib/transport/binary/protocol28/operations/db-delete.js new file mode 100644 index 0000000..60cfaff --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-delete.js @@ -0,0 +1,18 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_DROP', + opCode: 7, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeString(this.data.name) + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/db-exists.js b/lib/transport/binary/protocol28/operations/db-exists.js new file mode 100644 index 0000000..c8dfc4d --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-exists.js @@ -0,0 +1,22 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_EXIST', + opCode: 6, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeString(this.data.name) + .writeString(this.data.storage || 'local'); + }, + reader: function () { + this + .readStatus('status') + .readByte('exists', function (data) { + data.exists = Boolean(data.exists); + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/db-freeze.js b/lib/transport/binary/protocol28/operations/db-freeze.js new file mode 100644 index 0000000..cac9f79 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-freeze.js @@ -0,0 +1,18 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_FREEZE', + opCode: 94, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeString(this.data.name) + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); diff --git a/lib/transport/binary/protocol28/operations/db-list.js b/lib/transport/binary/protocol28/operations/db-list.js new file mode 100644 index 0000000..54a68df --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-list.js @@ -0,0 +1,19 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_LIST', + opCode: 74, + writer: function () { + this.writeHeader(this.opCode, this.data.sessionId, this.data.token); + }, + reader: function () { + this + .readStatus('status') + .readObject('databases', function (data, fieldName) { + data[fieldName] = data[fieldName].databases; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/db-open.js b/lib/transport/binary/protocol28/operations/db-open.js new file mode 100644 index 0000000..35fa081 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-open.js @@ -0,0 +1,48 @@ +"use strict"; +var Operation = require('../operation'), + constants = require('../constants'), + npmPackage = require('../../../../../package.json'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_OPEN', + opCode: 3, + writer: function () { + this + .writeByte(this.opCode) + .writeInt(this.data.sessionId || -1) + .writeString(npmPackage.name) + .writeString(npmPackage.version) + .writeShort(+constants.PROTOCOL_VERSION) + .writeString('') // client id + .writeString('ORecordDocument2csv') // serialization format + .writeBoolean(this.data.useToken) // tokens please! + .writeString(this.data.name) + .writeString(this.data.type) + .writeString(this.data.username) + .writeString(this.data.password); + }, + reader: function () { + this + .readStatus('status') + .readInt('sessionId') + .readBytes('token'); + + this + .readShort('totalClusters') + .readArray('clusters', function (data) { + var clusters = [], + total = data.totalClusters, + i; + + for (i = 0; i < total; i++) { + clusters.push(function (data) { + this.readString('name') + .readShort('id'); + }); + } + return clusters; + }) + .readObject('serverCluster') + .readString('release'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/db-release.js b/lib/transport/binary/protocol28/operations/db-release.js new file mode 100644 index 0000000..43e2dc0 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-release.js @@ -0,0 +1,18 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_RELEASE', + opCode: 95, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeString(this.data.name) + .writeString(this.data.storage || 'plocal'); + }, + reader: function () { + this.readStatus('status'); + } +}); diff --git a/lib/transport/binary/protocol28/operations/db-reload.js b/lib/transport/binary/protocol28/operations/db-reload.js new file mode 100644 index 0000000..cd5ed58 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-reload.js @@ -0,0 +1,30 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_RELOAD', + opCode: 73, + writer: function () { + this.writeHeader(this.opCode, this.data.sessionId, this.data.token); + }, + reader: function () { + this + .readStatus('status') + .readShort('totalClusters') + .readArray('clusters', function (data) { + var clusters = [], + total = data.totalClusters, + i; + + for (i = 0; i < total; i++) { + clusters.push(function (data) { + this.readString('name') + .readShort('id'); + }); + } + return clusters; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/db-size.js b/lib/transport/binary/protocol28/operations/db-size.js new file mode 100644 index 0000000..c3b5f9e --- /dev/null +++ b/lib/transport/binary/protocol28/operations/db-size.js @@ -0,0 +1,17 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_SIZE', + opCode: 8, + writer: function () { + this.writeHeader(this.opCode, this.data.sessionId, this.data.token); + }, + reader: function () { + this + .readStatus('status') + .readLong('size'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/index.js b/lib/transport/binary/protocol28/operations/index.js new file mode 100644 index 0000000..be581ca --- /dev/null +++ b/lib/transport/binary/protocol28/operations/index.js @@ -0,0 +1,34 @@ +"use strict"; /*jshint sub:true*/ + +exports['connect'] = require('./connect'); +exports['db-open'] = require('./db-open'); +exports['db-create'] = require('./db-create'); +exports['db-exists'] = require('./db-exists'); +exports['db-delete'] = require('./db-delete'); +exports['db-size'] = require('./db-size'); +exports['db-countrecords'] = require('./db-countrecords'); +exports['db-reload'] = require('./db-reload'); +exports['db-list'] = require('./db-list'); +exports['db-freeze'] = require('./db-freeze'); +exports['db-release'] = require('./db-release'); +exports['db-close'] = require('./db-close'); + + +exports['datacluster-add'] = require('./datacluster-add'); +exports['datacluster-count'] = require('./datacluster-count'); +exports['datacluster-datarange'] = require('./datacluster-datarange'); +exports['datacluster-drop'] = require('./datacluster-drop'); + +exports['record-create'] = require('./record-create'); +exports['record-load'] = require('./record-load'); +exports['record-metadata'] = require('./record-metadata'); +exports['record-update'] = require('./record-update'); +exports['record-delete'] = require('./record-delete'); +exports['record-clean-out'] = require('./record-clean-out'); + +exports['command'] = require('./command'); +exports['tx-commit'] = require('./tx-commit'); + +exports['config-list'] = require('./config-list'); +exports['config-get'] = require('./config-get'); +exports['config-set'] = require('./config-set'); diff --git a/lib/transport/binary/protocol28/operations/record-clean-out.js b/lib/transport/binary/protocol28/operations/record-clean-out.js new file mode 100644 index 0000000..b8dbbf6 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/record-clean-out.js @@ -0,0 +1,34 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_CLEAN_OUT', + opCode: 38, + writer: function () { + var rid, cluster, position; + if (this.data.record && this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + position = this.data.position || rid.position; + } + else { + cluster = this.data.cluster; + position = this.data.position; + } + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeShort(cluster) + .writeLong(position) + .writeInt(this.data.version || -1) + .writeBoolean(this.data.mode); + }, + reader: function () { + this + .readStatus('status') + .readBoolean('success'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/record-create.js b/lib/transport/binary/protocol28/operations/record-create.js new file mode 100644 index 0000000..abb0499 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/record-create.js @@ -0,0 +1,50 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_CREATE', + opCode: 31, + writer: function () { + var rid, cluster; + if (this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + } + else { + cluster = this.data.cluster; + } + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeShort(cluster) + .writeBytes(serializer.encodeRecordData(this.data.record)) + .writeByte(constants.RECORD_TYPES[this.data.type || 'd']) + .writeByte(this.data.mode || 0); + }, + reader: function () { + this + .readStatus('status') + .readShort('cluster') + .readLong('position') + .readInt('version') + .readInt('totalChanges') + .readArray('changes', function (data) { + var items = [], + i; + for (i = 0; i < data.totalChanges; i++) { + items.push(function () { + this + .readLong('uuidHigh') + .readLong('uuidLow') + .readLong('fileId') + .readLong('pageIndex') + .readInt('pageOffset'); + }); + } + return items; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/record-delete.js b/lib/transport/binary/protocol28/operations/record-delete.js new file mode 100644 index 0000000..420995a --- /dev/null +++ b/lib/transport/binary/protocol28/operations/record-delete.js @@ -0,0 +1,45 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_DELETE', + opCode: 33, + writer: function () { + var rid, cluster, position, version; + if (this.data.record && this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + position = this.data.position || rid.position; + } + else { + cluster = this.data.cluster; + position = this.data.position; + } + if (this.data.version != null) { + version = this.data.version; + } + else if (this.data.record && this.data.record['@version'] != null) { + version = this.data.record['@version']; + } + else { + version = -1; + } + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeShort(cluster) + .writeLong(position) + .writeInt(version) + .writeByte(this.data.mode || 0); + }, + reader: function () { + this + .readStatus('status') + .readByte('success', function (data, fieldName) { + data[fieldName] = Boolean(data[fieldName]); + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/record-load.js b/lib/transport/binary/protocol28/operations/record-load.js new file mode 100644 index 0000000..6a356fc --- /dev/null +++ b/lib/transport/binary/protocol28/operations/record-load.js @@ -0,0 +1,119 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'), + deserializer = require('../deserializer'), + errors = require('../../../../errors'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_LOAD', + opCode: 30, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeShort(this.data.cluster) + .writeLong(this.data.position) + .writeString(this.data.fetchPlan || '') + .writeByte(this.data.ignoreCache || 0) + .writeByte(this.data.tombstones || 0); + }, + reader: function () { + var records = []; + this.readStatus('status'); + this.readOps.push(function (data) { + data.records = records; + this.stack.push(data.records); + this.readPayload(records, function () { + this.stack.pop(); + data.records = data.records.map(function (record) { + var r; + if (record.type === 'd') { + r = record.content || {}; + r['@rid'] = r['@rid'] ||new RID({ + cluster: record.cluster, + position: record.position + }); + r['@version'] = record.version; + r['@type'] = record.type; + } + else { + r = { + '@rid': new RID({ + cluster: record.cluster, + position: record.position + }), + '@version': record.version, + '@type': record.type, + value: record.content + }; + + } + return r; + }, this); + }); + }); + }, + readPayload: function (records, ender) { + + return this.readByte('payloadStatus', function (data, fieldName) { + var record = {}; + switch (data[fieldName]) { + case 0: + // nothing to do. + if (ender) { + ender.call(this); + } + break; + case 1: + // a record + records.push(record); + this.stack.push(record); + this + .readChar('type') + .readInt('version') + .readString('content', function (data, fieldName) { + data.cluster = this.data.cluster; + data.position = this.data.position; + if (data.type === 'd') { + data.content = deserializer.deserialize(data.content, this.data.transformerFunctions); + } + this.stack.pop(); + this.readPayload(records, ender); + }); + break; + case 2: + // a sub record + records.push(record); + this.stack.push(record); + this.readShort('classId', function (data, fieldName) { + switch (data[fieldName]) { + case -2: + this.stack.pop(); + this.readPayload(records, ender); + break; + case -3: + throw new errors.Protocol('ClassID ' + data[fieldName] + ' is not supported.'); + default: + this + .readChar('type') + .readShort('cluster') + .readLong('position') + .readInt('version') + .readString('content', function (data, fieldName) { + if (data.type === 'd') { + data.content = deserializer.deserialize(data.content, this.data.transformerFunctions); + } + this.stack.pop(); + this.readPayload(records, ender); + }); + } + }); + break; + default: + this.readPayload(records, ender); + } + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/record-metadata.js b/lib/transport/binary/protocol28/operations/record-metadata.js new file mode 100644 index 0000000..d5b1814 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/record-metadata.js @@ -0,0 +1,34 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_METADATA', + opCode: 29, + writer: function () { + var rid, cluster, position; + if (this.data.record && this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + position = this.data.position || rid.position; + } + else { + cluster = this.data.cluster; + position = this.data.position; + } + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeShort(cluster) + .writeLong(position); + }, + reader: function () { + this + .readStatus('status') + .readShort('cluster') + .readLong('position') + .readInt('version'); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/record-update.js b/lib/transport/binary/protocol28/operations/record-update.js new file mode 100644 index 0000000..124dfe4 --- /dev/null +++ b/lib/transport/binary/protocol28/operations/record-update.js @@ -0,0 +1,62 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_UPDATE', + opCode: 32, + writer: function () { + var rid, cluster, position, version; + if (this.data.record['@rid']) { + rid = RID.parse(this.data.record['@rid']); + cluster = this.data.cluster || rid.cluster; + position = this.data.position || rid.position; + } + else { + cluster = this.data.cluster; + position = this.data.position; + } + if (this.data.version != null) { + version = this.data.version; + } + else if (this.data.record['@version'] != null) { + version = this.data.record['@version']; + } + else { + version = -1; + } + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeShort(cluster) + .writeLong(position) + .writeBoolean(true) + .writeBytes(serializer.encodeRecordData(this.data.record)) + .writeInt(version) + .writeByte(constants.RECORD_TYPES[this.data.type || 'd']) + .writeByte(this.data.mode || 0); + }, + reader: function () { + this + .readStatus('status') + .readInt('version') + .readInt('totalChanges') + .readArray('changes', function (data) { + var items = [], + i; + for (i = 0; i < data.totalChanges; i++) { + items.push(function () { + this + .readLong('uuidHigh') + .readLong('uuidLow') + .readLong('fileId') + .readLong('pageIndex') + .readInt('pageOffset'); + }); + } + return items; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/operations/tx-commit.js b/lib/transport/binary/protocol28/operations/tx-commit.js new file mode 100644 index 0000000..d224fdd --- /dev/null +++ b/lib/transport/binary/protocol28/operations/tx-commit.js @@ -0,0 +1,112 @@ +"use strict"; + +var Operation = require('../operation'), + constants = require('../constants'), + RID = require('../../../../recordid'), + serializer = require('../serializer'); + +module.exports = Operation.extend({ + id: 'REQUEST_TX_COMMIT', + opCode: 60, + writer: function () { + this + .writeHeader(this.opCode, this.data.sessionId, this.data.token) + .writeInt(this.data.txId) + .writeByte(this.data.txLog); // use transaction log + + + // creates + var total = this.data.creates.length, + item, i; + + for (i = 0; i < total; i++) { + item = this.data.creates[i]; + this.writeByte(1); // mark the start of an entry. + this.writeByte(3); // create. + this.writeShort(item['@rid'].cluster); + this.writeLong(item['@rid'].position); + this.writeByte(constants.RECORD_TYPES[item['@type'] || 'd'] || 100); // document by default + this.writeBytes(serializer.encodeRecordData(item)); + } + + // updates + total = this.data.updates.length; + + for (i = 0; i < total; i++) { + item = this.data.updates[i]; + this.writeByte(1); // mark the start of an entry. + this.writeByte(1); // update. + this.writeShort(item['@rid'].cluster); + this.writeLong(item['@rid'].position); + this.writeByte(constants.RECORD_TYPES[item['@type'] || 'd'] || 100); // document by default + this.writeInt(item['@version'] || 0); + this.writeBytes(serializer.encodeRecordData(item)); + this.writeBoolean(true); + } + + // deletes + total = this.data.deletes.length; + + for (i = 0; i < total; i++) { + item = this.data.deletes[i]; + this.writeByte(1); // mark the start of an entry. + this.writeByte(2); // delete + this.writeShort(item['@rid'].cluster); + this.writeLong(item['@rid'].position); + this.writeByte(constants.RECORD_TYPES[item['@type'] || 'd'] || 100); // document by default + this.writeInt(item['@version'] || 0); + } + this.writeByte(0); // no more documents + this.writeString(''); + }, + reader: function () { + this + .readStatus('status') + .readInt('totalCreated') + .readArray('created', function (data) { + var items = [], + i; + for (i = 0; i < data.totalCreated; i++) { + items.push(function () { + this + .readShort('tmpCluster') + .readLong('tmpPosition') + .readShort('cluster') + .readLong('position'); + }); + } + return items; + }) + .readInt('totalUpdated') + .readArray('updated', function (data) { + var items = [], + i; + for (i = 0; i < data.totalUpdated; i++) { + items.push(function () { + this + .readShort('cluster') + .readLong('position') + .readInt('version'); + }); + } + return items; + }); + + this.readInt('totalChanges') + .readArray('changes', function (data) { + var items = [], + i; + for (i = 0; i < data.totalChanges; i++) { + items.push(function () { + this + .readLong('uuidHigh') + .readLong('uuidLow') + .readLong('fileId') + .readLong('pageIndex') + .readInt('pageOffset'); + }); + } + return items; + }); + } +}); \ No newline at end of file diff --git a/lib/transport/binary/protocol28/serializer.js b/lib/transport/binary/protocol28/serializer.js new file mode 100644 index 0000000..5d9b993 --- /dev/null +++ b/lib/transport/binary/protocol28/serializer.js @@ -0,0 +1,141 @@ +"use strict"; + +var RecordID = require('../../../recordid'); + +/** + * Serialize a record and return it as a buffer. + * + * @param {Object} content The record to serialize. + * @return {Buffer} The buffer containing the content. + */ +function encodeRecordData (content) { + return new Buffer(serializeDocument(content), 'utf8'); +} + +/** + * Serialize a document. + * + * @param {Object} document The document to serialize. + * @param {Boolean} isMap Whether to serialize the document as a map. + * @return {String} The serialized document. + */ +function serializeDocument (document, isMap) { + if (typeof document.toOrient === 'function') { + document = document.toOrient(); + } + + var result = '', + className = '', + fieldNames = Object.keys(document), + totalFields = fieldNames.length, + fieldWrap, value, field, i; + + for (i = 0; i < totalFields; i++) { + field = fieldNames[i]; + value = document[field]; + if (field === '@class') { + className = value; + } + else if (field.charAt(0) === '@' || value === undefined) { + continue; + } + else { + if (isMap) { + fieldWrap = '"'; + } + else { + fieldWrap = ''; + } + result += fieldWrap + field + fieldWrap + ':' + serializeValue(value) + ','; + } + } + + if (className !== '') { + result = className + '@' + result; + } + + if (result[result.length - 1] === ',') { + result = result.slice(0, -1); + } + + return result; +} + +/** + * Serialize a given value according to its type. + * @param {Object} value The value to serialize. + * @return {String} The serialized value. + */ +function serializeValue (value) { + var type = typeof value; + if (type === 'string') { + return '"' + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + } + else if (type === 'number') { + return ~value.toString().indexOf('.') ? value + 'f' : value; + } + else if (type === 'boolean') { + return value ? true : false; + } + else if (Object.prototype.toString.call(value) === '[object Date]') { + return value.getTime() + 't'; + } + else if (Array.isArray(value)) { + return serializeArray(value); + } + else if (value === Object(value)) { + return serializeObject(value); + } + else { + return ''; + } +} + + +/** + * Serialize an array of values. + * @param {Array} value The value to serialize. + * @return {String} The serialized value. + */ +function serializeArray (value) { + var result = '[', i, limit; + for (i = 0, limit = value.length; i < limit; i++) { + if (value[i] === Object(value[i])) { + result += serializeObject(value[i]); + } + else { + result += serializeValue(value[i]); + } + if (i < limit - 1) { + result += ','; + } + } + result += ']'; + return result; +} + +/** + * Serialize an object. + * @param {Object} value The value to serialize. + * @return {String} The serialized value. + */ +function serializeObject (value) { + if (value instanceof RecordID) { + return value.toString(); + } + else if (value['@type'] && value['@type'].charAt(0) === 'd') { + return '(' + serializeDocument(value, false) + ')'; + } + else { + return '{' + serializeDocument(value, true) + '}'; + } +} + + + + +// export the public methods + +exports.serializeDocument = serializeDocument; +exports.serializeValue = serializeValue; +exports.encodeRecordData = encodeRecordData; \ No newline at end of file diff --git a/lib/transport/binary/protocol28/writer.js b/lib/transport/binary/protocol28/writer.js new file mode 100644 index 0000000..5c21cd2 --- /dev/null +++ b/lib/transport/binary/protocol28/writer.js @@ -0,0 +1,123 @@ +"use strict"; + +var Long = require('../../../long').Long, + constants = require('./constants'); + +/** + * Parse data to 4 bytes which represents integer value. + * + * @fixme this is a super misleading function name and comment! + * + * @param {Mixed} data The data. + * @return {Buffer} The buffer containing the data. + */ +function writeByte (data) { + return new Buffer([data]); +} + +/** + * Parse data to 4 bytes which represents integer value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeInt (data) { + var buf = new Buffer(constants.BYTES_INT); + buf.writeInt32BE(data, 0); + return buf; +} + +/** + * Parse data to 8 bytes which represents a long value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeLong (data) { + var buf = new Buffer(constants.BYTES_LONG), + value = Long.fromNumber(data); + + buf.fill(0); + buf.writeInt32BE(value.high_, 0); + buf.writeInt32BE(value.low_, constants.BYTES_INT); + + return buf; +} + +/** + * Parse data to 2 bytes which represents short value. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeShort (data) { + var buf = new Buffer(constants.BYTES_SHORT); + buf.writeInt16BE(data, 0); + return buf; +} + +/** + * Write bytes to a buffer + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeBytes (data) { + var length = data.length, + buf = new Buffer(constants.BYTES_INT + length); + buf.writeInt32BE(length, 0); + data.copy(buf, constants.BYTES_INT); + return buf; +} + +/** + * Parse string data to buffer with UTF-8 encoding. + * + * @param {Mixed} data The data to write. + * @return {Buffer} The buffer containing the data. + */ +function writeString (data) { + if (data === null) { + return writeInt(-1); + } + var stringBuf = encodeString(data), + length = stringBuf.length, + buf = new Buffer(constants.BYTES_INT + length); + buf.writeInt32BE(length, 0); + stringBuf.copy(buf, constants.BYTES_INT, 0, stringBuf.length); + return buf; +} + +function encodeString (data) { + var length = data.length, + output = new Buffer(length * 3), // worst case, all chars could require 3-byte encodings. + j = 0, // index output + i, c; + + for (i = 0; i < length; i++) { + c = data.charCodeAt(i); + if (c < 0x80) { + // 7-bits done in one byte. + output[j++] = c; + } + else if (c < 0x800) { + // 8-11 bits done in 2 bytes + output[j++] = (0xC0 | c >> 6); + output[j++] = (0x80 | c & 0x3F); + } + else { + // 12-16 bits done in 3 bytes + output[j++] = (0xE0 | c >> 12); + output[j++] = (0x80 | c >> 6 & 0x3F); + output[j++] = (0x80 | c & 0x3F); + } + } + return output.slice(0, j); +} + +exports.writeByte = writeByte; +exports.writeBoolean = writeByte; +exports.writeBytes = writeBytes; +exports.writeShort = writeShort; +exports.writeInt = writeInt; +exports.writeLong = writeLong; +exports.writeString = writeString; \ No newline at end of file diff --git a/lib/transport/index.js b/lib/transport/index.js new file mode 100644 index 0000000..fb30c9e --- /dev/null +++ b/lib/transport/index.js @@ -0,0 +1,4 @@ +"use strict"; + +exports.Binary = exports.BinaryTransport = require('./binary'); +exports.Rest = exports.RestTransport = require('./rest'); \ No newline at end of file diff --git a/lib/transport/rest/index.js b/lib/transport/rest/index.js new file mode 100644 index 0000000..c784c54 --- /dev/null +++ b/lib/transport/rest/index.js @@ -0,0 +1,155 @@ +"use strict"; + +var utils = require('../../utils'), + errors = require('../../errors'), + Db = require('../../db/index'), + Promise = require('bluebird'), + request = require('request'), + requestAsync = Promise.promisify(request), + util = require('util'), + deserializer = require('./protocol/deserializer'), + operations = require('./protocol/operations'), + EventEmitter = require('events').EventEmitter, + npmPackage = require('../../../package.json'); + + +/** + * # Binary Transport + * + * @param {Object} config The configuration for the transport. + */ +function RestTransport (config) { + EventEmitter.call(this); + this.setMaxListeners(Infinity); + this.configure(config || {}); +} + +util.inherits(RestTransport, EventEmitter); + +RestTransport.extend = utils.extend; +RestTransport.prototype.augment = utils.augment; + +RestTransport.protocol = require('./protocol'); + + +module.exports = RestTransport; + + +/** + * Configure the transport. + * + * @param {Object} config The transport configuration. + */ +RestTransport.prototype.configure = function (config) { + this.connecting = false; + this.closing = false; + + this.host = config.host || config.hostname || 'localhost'; + this.port = config.port || 2424; + this.username = config.username || 'root'; + this.password = config.password || ''; + + this.sessionId = -1; + this.configureLogger(config.logger || {}); +}; + +/** + * Configure the logger for the transport. + * + * @param {Object} config The logger config + * @return {RestTransport} The transport instance with the configured logger. + */ +RestTransport.prototype.configureLogger = function (config) { + this.logger = { + error: config.error || console.error.bind(console), + log: config.log || console.log.bind(console), + debug: config.debug || function () {} // do not log debug by default + }; + return this; +}; + + +/** + * Send an operation to the server, + * + * @param {Integer} operation The id of the operation to send. + * @param {Object} options The options for the operation. + * @promise {Mixed} The result of the operation. + */ +RestTransport.prototype.send = function (operation, options) { + options = options || {}; + return this.process(operation, options); +}; + + +/** + * Close the connection to the server. + * + * @return {Server} the disconnected server instance + */ +RestTransport.prototype.close = function () { + if (!this.closing && this.socket) { + this.closing = false; + this.sessionId = -1; + } + return this; +}; + + +RestTransport.prototype.process = function (op, params) { + if (typeof op === 'string') { + op = new operations[op](params || {}); + } + + var prepared = this.prepareRequest(op); + + return requestAsync(prepared) + .spread(this.handleResponse.bind(this, op, prepared)) + .bind(op) + .then(op.processResponse); +}; + +RestTransport.prototype.prepareRequest = function (op) { + var config = op.requestConfig(); + config.url = 'http://' + this.host + ':' + this.port + (config.url || '/'); + config.headers = config.headers || {}; + config.headers['User-Agent'] = npmPackage.name + ' v' + npmPackage.version; + if (!op.jar && !this.jar) { + config.jar = this.jar = request.jar(); + } + else { + config.jar = op.jar || this.jar; + } + + if (!config.jar.getCookieString('OSESSIONID')) { + this.applyAuth(config); + } + + return config; +}; + +RestTransport.prototype.applyAuth = function (config) { + config.auth = { + username: this.username, + password: this.password + }; + return config; +}; + +RestTransport.prototype.handleResponse = function (op, prepared, response) { + if (response.statusCode === 401) { + return requestAsync(this.applyAuth(prepared)) + .bind(this) + .spread(function (response) { + if (response.statusCode > 399) { + return Promise.reject(new errors.Request('Authorization Error')); + } + else { + return deserializer.deserializeDocument(response.body); + } + }); + } + else { + return deserializer.deserializeDocument(response.body); + } +}; \ No newline at end of file diff --git a/lib/transport/rest/protocol/deserializer.js b/lib/transport/rest/protocol/deserializer.js new file mode 100644 index 0000000..e64c016 --- /dev/null +++ b/lib/transport/rest/protocol/deserializer.js @@ -0,0 +1,69 @@ +"use strict"; + +var RecordID = require('../../../recordid'), + errors = require('../../../errors'), + Long = require('../../../long').Long; + + +/** + * Deserialize a given serialized document. + * + * @param {String} serialized The serialized document. + * @return {Object} The deserialized document + */ +function deserializeDocument (serialized) { + try { + return JSON.parse(serialized, function (key, value) { + if (key === '@rid') { + return new RecordID(value); + } + else if (key === '@fieldTypes') { + applyFieldTypes(this); + } + else { + return value; + } + }); + } + catch (e) { + throw new errors.Request(serialized); + } +} + +function applyFieldTypes (subject) { + var types = subject['@fieldTypes'].split(','), + total = types.length, + i, parts, fieldName, type; + for (i = 0; i < total; i++) { + parts = types[i].split('='); + fieldName = parts[0]; + type = parts[1]; + subject[fieldName] = applyFieldType(type, subject[fieldName]); + } + return subject; +} + +function applyFieldType (type, value) { + switch (type) { + case 'f': // float + return parseFloat(value); + case 'l': // long + return Long.fromString(value); + case 's': //short + return +value; + case 'a': // date + case 't': // datetime + return new Date(value); + case 'e': // set + case 'c': // decimal + case 'd': // double + case 'b': // byte + return value; + default: + return value; + } +} + +// export the public methods + +exports.deserializeDocument = deserializeDocument; \ No newline at end of file diff --git a/lib/transport/rest/protocol/index.js b/lib/transport/rest/protocol/index.js new file mode 100644 index 0000000..5155f2f --- /dev/null +++ b/lib/transport/rest/protocol/index.js @@ -0,0 +1,5 @@ +"use strict"; + +exports.operations = require('./operations'); +exports.deserializer = require('./deserializer'); +exports.Operation = require('./operation'); \ No newline at end of file diff --git a/lib/transport/rest/protocol/operation.js b/lib/transport/rest/protocol/operation.js new file mode 100644 index 0000000..22c4793 --- /dev/null +++ b/lib/transport/rest/protocol/operation.js @@ -0,0 +1,38 @@ +"use strict"; + +var utils = require('../../../utils'); + +/** + * # REST Operations + * + * The base class for REST operations, provides a simple DSL for defining + * the steps required to send a command to the server, and then read + * the response. + * + * Each operation should implement the `write()` and `read()` methods. + * + * @param {Object} data The data for the operation. + */ +function Operation (data) { + this.status = Operation.PENDING; + this.writeOps = []; + this.readOps = []; + this.stack = [{}]; + this.data = data || {}; +} + +module.exports = Operation; + +// operation statuses + + +Operation.PENDING = 0; +Operation.WRITTEN = 1; +Operation.READING = 2; +Operation.COMPLETE = 3; +Operation.ERROR = 4; +Operation.PUSH_DATA = 5; + +// make it easy to inherit from the base class +Operation.extend = utils.extend; + diff --git a/lib/transport/rest/protocol/operations/command.js b/lib/transport/rest/protocol/operations/command.js new file mode 100644 index 0000000..ec4b83c --- /dev/null +++ b/lib/transport/rest/protocol/operations/command.js @@ -0,0 +1,31 @@ +"use strict"; + +var Operation = require('../operation'), + utils = require('../../../../utils'); + +module.exports = Operation.extend({ + id: 'REQUEST_COMMAND', + opCode: 41, + requestConfig: function () { + var prepared = utils.prepare(this.data.query, this.data.params), + url = '/command/' + this.data.database + '/sql/' + prepared; + if (this.data.limit) { + url += '/' + this.data.limit; + } + return { + method: 'POST', + url: url + }; + }, + processResponse: function (response) { + return { + status: {code: 0, sessionId: -1}, + results: [ + { + type: 'l', + content: response.result + } + ] + }; + } +}); diff --git a/lib/transport/rest/protocol/operations/db-open.js b/lib/transport/rest/protocol/operations/db-open.js new file mode 100644 index 0000000..3deb498 --- /dev/null +++ b/lib/transport/rest/protocol/operations/db-open.js @@ -0,0 +1,31 @@ +"use strict"; + +var Operation = require('../operation'), + npmPackage = require('../../../../../package.json'); + +module.exports = Operation.extend({ + id: 'REQUEST_DB_OPEN', + opCode: 2, + requestConfig: function () { + return { + method: 'GET', + url: '/database/' + this.data.name + }; + }, + processResponse: function (response) { + return { + status: { + code: 0, + sessionId: -1 + }, + sessionId: -1, + totalClusters: response.clusters.length, + clusters: response.clusters, + serverCluster: {}, + release: response.server.version + ' (build ' + response.server.build + ')', + classes: response.classes, + indexes: response.indexes, + config: response.config + }; + } +}); diff --git a/lib/transport/rest/protocol/operations/index.js b/lib/transport/rest/protocol/operations/index.js new file mode 100644 index 0000000..4db9221 --- /dev/null +++ b/lib/transport/rest/protocol/operations/index.js @@ -0,0 +1,5 @@ +"use strict"; /*jshint sub:true*/ + +exports['db-open'] = require('./db-open'); +exports['record-load'] = require('./record-load'); +exports['command'] = require('./command'); \ No newline at end of file diff --git a/lib/transport/rest/protocol/operations/record-load.js b/lib/transport/rest/protocol/operations/record-load.js new file mode 100644 index 0000000..e8aa001 --- /dev/null +++ b/lib/transport/rest/protocol/operations/record-load.js @@ -0,0 +1,25 @@ +"use strict"; + +var Operation = require('../operation'), + npmPackage = require('../../../../../package.json'); + +module.exports = Operation.extend({ + id: 'REQUEST_RECORD_LOAD', + opCode: 30, + requestConfig: function () { + var url = '/document/' + this.data.database + '/' + this.data.cluster + ':' + this.data.position; + if (this.data.fetchPlan) { + url += '/' + this.data.fetchPlan; + } + return { + method: 'GET', + url: url + }; + }, + processResponse: function (response) { + return { + status: {code: 0, sessionId: -1}, + records: [response] + }; + } +}); diff --git a/lib/utils.js b/lib/utils.js index cc745ea..5814aa6 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,8 @@ +"use strict"; + +var RID = require('./recordid'), + Bag = require('./bag'); + /** * Make it easy to extend classes. * @@ -26,29 +31,37 @@ exports.extend = function (source) { var parent = this, child; - if (source.hasOwnProperty('constructor')) + if (source.hasOwnProperty('constructor')) { child = source.constructor; - else + } + else { child = function () { return parent.apply(this, arguments); }; + } - var Surrogate = function () { this.constructor = child; }; - Surrogate.prototype = parent.prototype; - child.prototype = new Surrogate; + child.prototype = Object.create(parent.prototype, { + constructor: { + value: child + } + }); var keys, key, i, limit; for (keys = Object.keys(parent), key = null, i = 0, limit = keys.length; i < limit; i++) { key = keys[i]; - if (key !== 'prototype') + if (key !== 'prototype') { child[key] = parent[key]; + } } for (keys = Object.keys(source), key = null, i = 0, limit = keys.length; i < limit; i++) { key = keys[i]; - if (key.charCodeAt(0) === 64) // @ + if (key.charCodeAt(0) === 64) { + // @ child[key.slice(1)] = source[key]; - else if (key !== 'constructor') + } + else if (key !== 'constructor') { child.prototype[key] = source[key]; + } } child.__super__ = child; @@ -88,10 +101,12 @@ exports.augment = function (name, props) { * @return {Mixed} The cloned item. */ exports.clone = function (item) { - if (Object(item) !== item) + if (Object(item) !== item) { return item; - else if (Array.isArray(item)) + } + else if (Array.isArray(item)) { return item.slice(); + } var keys = Object.keys(item), total = keys.length, @@ -102,4 +117,258 @@ exports.clone = function (item) { cloned[key] = item[key]; } return cloned; -} \ No newline at end of file +}; + +/** + * Escape the given input for use in a query. + * + * > NOTE: Because of a fun quirk in OrientDB's parser, this function can only be safely + * used on SQL segments that are enclosed in DOUBLE QUOTES (") not single quotes ('). + * + * @param {String} input The input to escape. + * @return {String} The escaped input. + */ +exports.escape = function (input) { + var text = ''+input; + var chars = new Array(text.length); + for (var i = 0; i < text.length; i++) { + var char = text.charAt(i); + if (char === '\r') { + chars[i] = '\\r'; + } + else if (char === '\n') { + chars[i] = '\\n'; + } + else if (char === '"') { + chars[i] = '\\"'; + } + else if (char === '\\') { + chars[i] = '\\\\'; + } + else { + chars[i] = char; + } + } + return chars.join(''); +}; + +/** + * Prepare a query. + * + * @param {String} query The query to prepare. + * @param {Object} params The bound parameters for the query. + * @return {String} The prepared query. + */ +exports.prepare = function (query, params) { + if (!params) { + return query; + } + else if (params instanceof ArrayLike || Array.isArray(params)) { + return prepareArray(query, params); + } + else { + return prepareObject(query, params); + } +}; + + +function prepareArray (query, params) { + var pattern = /"(\\[\s\S]|[^"])*"|'(\\[\s\S]|[^'])*'|(\?)/g; + var n = 0; + return query.replace(pattern, function (all, double, single, param) { + if (param) { + return exports.encode(params[n++]); + } + else { + return all; + } + }); +} + +function prepareObject (query, params) { + var pattern = /"(\\[\s\S]|[^"])*"|'(\\[\s\S]|[^'])*'|([^A-Za-z0-9]:([A-Za-z][A-Za-z0-9_-]*))/g; + return query.replace(pattern, function (all, double, single, char, param) { + if (param && params[param] !== undefined) { + return char.charAt(0) + exports.encode(params[param]); + } + else { + return all; + } + }); +} + +/** + * Encode a value for use in a query, escaping and quoting it if required. + * + * @param {Mixed} value The value to encode. + * @return {Mixed} The encoded value. + */ +exports.encode = function encode (value) { + if (value == null) { + return 'null'; + } + else if (typeof value === 'number') { + return value; + } + else if (typeof value === 'boolean') { + return value; + } + else if (typeof value === 'string') { + return '"' + exports.escape(value) + '"'; + } + else if (value instanceof Date) { + return 'date("' + getOrientDbUTCDate(value) + '", "yyyy-MM-dd HH:mm:ss.SSS", "UTC")'; + } + else if (value instanceof RID) { + return value.toString(); + } + else if (typeof value.toOrient === 'function') { + return encode(value.toOrient()); + } + else if (Array.isArray(value)) { + return '[' + value.map(encode) + ']'; + } + else { + + var keys = Object.keys(value), + length = keys.length; + + var parts = new Array(length), + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + parts[i] = '"' + exports.escape(key) + '":'+encode(value[key]); + } + return '{'+parts.join(',')+'}'; + } +}; + + +/** + * Safely encode a value as JSON, allowing circular references. + * When a record is encountered more than once, subsequent references + * will embed the record's RID rather than the record itself. + * + * @param {Mixed} value The value to JSON stringify. + * @param {Integer} indentlevel The indentation level, if specified the JSON will be pretty printed. + * @return {String} The JSON string. + */ +exports.jsonify = function (value, indentlevel) { + var seen = []; + return JSON.stringify(value, function (key, value) { + if (value && typeof value === 'object') { + if (~seen.indexOf(value)) { + if (value['@rid']) { + return value['@rid'].toJSON(); + } + return; + } + seen.push(value); + } + return value; + }, indentlevel); +}; + + +/** + * Define a deprecated method or property. + * + * A warning message will be displayed the first time the method is called, regardless of the object. + * + * @param {Object} context The context for the method. + * @param {String} name The name of the deprecated method. + * @param {String} message The message to display. + * @param {Function} fn The function to call, it should restore the real property. + */ +exports.deprecate = function (context, name, message, fn) { + var shown = false; + Object.defineProperty(context, name, { + configurable: true, + enumerable: true, + get: function () { + if (!shown) { + console.warn(message); + shown = true; + } + delete this[name]; + fn.call(this, name); + return this[name]; + } + }); +}; + + +/** + * Converts a date object to string observing OrientDB's default format + * + * @param {Date} The value to convert + * @return {String} The string formatted as 'yyyy-MM-dd HH:mm:ss' + */ +function getOrientDbUTCDate(date){ + var yyyy = pad(date.getUTCFullYear(), 4); + var MM = pad(date.getUTCMonth() + 1, 2); + var dd = pad(date.getUTCDate(), 2); + var HH = pad(date.getUTCHours(), 2); + var mm = pad(date.getUTCMinutes(), 2); + var ss = pad(date.getUTCSeconds(), 2); + var SSS = pad(date.getUTCMilliseconds(), 3); + + return yyyy + '-' + MM + '-' + dd + ' ' + HH + ':' + mm + ':' + ss + '.' + SSS; +} + +function pad (number, size){ + return (1e4 + number + "").slice(-size); +} + +/** + * Determine whether the given expression needs to be wrapped in parentheses or not. + * @param {String} input The string to check. + * @return {Boolean} `true` if parentheses are required, otherwise false. + */ +exports.requiresParens = function (input) { + if (typeof input !== 'string') { + return false; + } + var text = input.trim(); + var exprCount = 0; + var inParens = 0; + var inQuotes = false; + for (var i = 0; i < text.length; i++) { + var char = text.charAt(i); + if (inQuotes) { + if (char === '\\') { + i += 1; + } + else if (char === inQuotes) { + inQuotes = false; + } + } + else if (char === '"' || char === "'") { + inQuotes = char; + } + else if (char === '(') { + if (inParens === 0) { + exprCount++; + } + inParens++; + } + else if (char === ')') { + inParens--; + } + else if (char === "\t" || char === "\r" || char === "\n" || char === " ") { + if (inParens === 0) { + return true; + } + } + } + return false; +}; + + +/** + * A class used solely to indicate that an object is an "array like" + * It should never be used directly and is a temporary hack around some poor design decisions. + */ +function ArrayLike () {} + +exports.ArrayLike = ArrayLike; diff --git a/package.json b/package.json index abdfa58..37d881f 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oriento", - "description": "A fast, lightweight client for the orientdb binary protocol, supporting the latest version of orient.", + "description": "Official node.js driver for OrientDB. Fast, lightweight, uses the binary protocol.", "keywords": [ "orientdb", "orient", @@ -12,7 +12,7 @@ "node", "node.js" ], - "version": "0.2.1", + "version": "1.2.0", "author": { "name": "Charles Pick", "email": "charles@codemix.com" @@ -44,14 +44,18 @@ "url": "http://github.com/codemix/oriento.git" }, "dependencies": { - "bluebird": "*", - "yargs": "*" + "bluebird": "~2.9.2", + "fast.js": "^0.1.1", + "parse-function": "^2.0.0", + "request": "~2.34.0", + "yargs": "~1.2.1" }, "devDependencies": { - "mocha": ">=1.18.2", - "should": "*", - "expect.js": "*", - "istanbul": "*" + "mocha": "^2.0.1", + "should": "~4.6.2", + "expect.js": "~0.3.1", + "istanbul": "~0.2.7", + "jshint": "~2.5.0" }, "main": "./lib/index.js", "directories": { @@ -64,9 +68,11 @@ "oriento": "./bin/oriento" }, "scripts": { - "test": "echo \"\n\nNOTICE: If tests fail, please ensure you've set the correct credentials in test/test-server.json\n\n\"; node ./node_modules/mocha/bin/mocha ./test/index.js ./test/**/*.js ./test/**/**/*.js --reporter=spec -t 10000", - "watch": "node ./node_modules/mocha/bin/mocha ./test/index.js ./test/**/*.js ./test/**/**/*.js --reporter=spec -t 10000 --watch", - "coverage": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha ./test/index.js ./test/**/*.js ./test/**/**/*.js --reporter=spec" + "pretest": "./node_modules/.bin/jshint ./lib", + "test": "echo \"\n\nNOTICE: If tests fail, please ensure you've set the correct credentials in test/test-server.json\n\n\"; node ./node_modules/mocha/bin/mocha ./test/index.js ./test/**/*.js ./test/**/**/*.js ./test/**/**/**/*.js ./test/**/**/**/**/*.js --reporter=spec -t 10000", + "watch": "node ./node_modules/mocha/bin/mocha ./test/index.js ./test/**/*.js ./test/**/**/*.js ./test/**/**/**/*.js ./test/**/**/**/**/*.js --reporter=spec -t 10000 --watch", + "coverage": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha ./test/index.js ./test/**/*.js ./test/**/**/*.js ./test/**/**/**/*.js ./test/**/**/**/**/*.js --reporter=spec", + "lint": "./node_modules/.bin/jshint ./lib" }, "licenses": [ { @@ -74,4 +80,4 @@ "url": "http://www.apache.org/licenses/LICENSE-2.0" } ] -} \ No newline at end of file +} diff --git a/test/bugs/110-connection-lifecycle.js b/test/bugs/110-connection-lifecycle.js new file mode 100644 index 0000000..b05c96f --- /dev/null +++ b/test/bugs/110-connection-lifecycle.js @@ -0,0 +1,71 @@ +var Statement = require('../../lib/db/statement'); + +describe("Bug #110: Connection lifecycle", function () { + var server, db; + before(function () { + server = new LIB.Server({ + host: TEST_SERVER_CONFIG.host, + port: TEST_SERVER_CONFIG.port, + username: TEST_SERVER_CONFIG.username, + password: TEST_SERVER_CONFIG.password, + transport: 'binary', + useToken: false + }); + db = server.use('testdb_bug_110'); + return CREATE_TEST_DB(this, 'testdb_bug_110') + .then(function () { + return db.select().from('OUser').limit(1).one(); // ensure the connection is established. + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_110'); + }); + it('should not crash if the connection is interrupted', function () { + var promise = db.class.list(); + db.server.transport.connection.socket.emit('error', {errnum: 100, message: 'ENETDOWN'}); + var counter = 0; + return promise + .then(function (results) { + throw new Error('Should never happen!'); + }) + .error(function (err) { + counter++; + }) + .bind(this) + .then(function () { + counter++; + return db.record.get('#5:0'); + }) + .then(function (rec) { + counter++; + var promise = db.record.get('#5:1'); + db.server.transport.connection.socket.emit('error', {errnum: 104, message: 'ECONNRESET'}); + return promise; + }) + .then(function () { + throw new Error('Should never happen!'); + }) + .error(function (err) { + counter++; + }) + .finally(function () { + counter.should.equal(4); + }); + }); + it('should close the database connection', function () { + return db.close() + .bind(this) + .then(function () { + return db.record.get('#5:0'); + }) + .then(function () { + throw new Error("Should never happen."); + }) + .error(function (err) { + return db.record.get('#5:0'); + }) + .then(function (rec) { + (''+rec['@rid']).should.equal('#5:0'); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/111-expand.js b/test/bugs/111-expand.js new file mode 100644 index 0000000..bde0dec --- /dev/null +++ b/test/bugs/111-expand.js @@ -0,0 +1,37 @@ +var Promise = require('bluebird'); + +describe("Bug #111: expand() returns only RIDs", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_111') + .bind(this) + .then(function () { + return Promise.map([ + 'CREATE CLASS Widget EXTENDS V', + 'CREATE PROPERTY Widget.name STRING', + 'CREATE INDEX UniqueWidgetName ON Widget (name) UNIQUE', + + 'CREATE CLASS WidgetHasWidget EXTENDS E', + 'CREATE PROPERTY WidgetHasWidget.in LINK Widget', + 'CREATE PROPERTY WidgetHasWidget.out LINK Widget', + 'ALTER PROPERTY WidgetHasWidget.out MANDATORY=true', + 'ALTER PROPERTY WidgetHasWidget.in MANDATORY=true', + 'CREATE INDEX UniqueWidgetHasWidget ON WidgetHasWidget (out, in) UNIQUE', + + 'INSERT INTO Widget SET name = "widget_A"', + 'INSERT INTO Widget SET name = "widget_B"', + + 'CREATE EDGE WidgetHasWidget FROM (SELECT FROM Widget WHERE name = "widget_A") TO (SELECT FROM Widget WHERE name="widget_B")', + 'CREATE EDGE WidgetHasWidget FROM (SELECT FROM Widget WHERE name = "widget_B") TO (SELECT FROM Widget WHERE name="widget_A")' + ], this.db.query.bind(this.db)); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_111'); + }); + it('should return the full record', function () { + return this.db.query('SELECT expand(out(\'WidgetHasWidget\')) FROM Widget WHERE name = "widget_A"') + .spread(function (result) { + result.name.should.equal('widget_B'); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/119-link-rid.js b/test/bugs/119-link-rid.js new file mode 100644 index 0000000..1484999 --- /dev/null +++ b/test/bugs/119-link-rid.js @@ -0,0 +1,35 @@ +var Statement = require('../../lib/db/statement'); + +describe("Bug #119: Set link field", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_119') + .bind(this) + .then(function () { + return this.db.class.create('SomeClass'); + }) + .then(function (item) { + this.class = item; + return this.class.property.create({ + name: 'link', + type: 'Link' + }); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_119'); + }); + it('should insert a RID in a link field', function () { + return this.db + .insert() + .into('SomeClass') + .set({ + nom: 'nom', + link: new LIB.RID('#5:0') + }) + .one() + .then(function (response) { + response.nom.should.equal('nom'); + response.link.should.be.an.instanceOf(LIB.RID); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/155-param-substitution.js b/test/bugs/155-param-substitution.js new file mode 100644 index 0000000..92cbc70 --- /dev/null +++ b/test/bugs/155-param-substitution.js @@ -0,0 +1,65 @@ +var Statement = require('../../lib/db/statement'); + +describe("Bug #155: db.query() parameters substitution fails for string represents number", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_155') + .bind(this) + .then(function () { + return this.db.class.create('SomeClass'); + }) + .then(function (item) { + this.class = item; + return this.class.property.create({ + name: 'val', + type: 'string' + }); + }) + .then(function () { + return this.db.insert().into('SomeClass').set({ + val: 123456 + }) + .one(); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_155'); + }); + it('should find by integer value, using query builder', function () { + return this.db.select().from('SomeClass').where({ + val: 123456 + }) + .one() + .then(function (doc) { + doc.val.should.equal('123456'); + }); + }); + it('should find by string value, using query builder', function () { + return this.db.select().from('SomeClass').where({ + val: '123456' + }) + .one() + .then(function (doc) { + doc.val.should.equal('123456'); + }); + }); + it('should find by integer value, using bound params', function () { + return this.db.query('SELECT FROM SomeClass WHERE val = :val', { + params: { + val: 123456 + } + }) + .spread(function (doc) { + doc.val.should.equal('123456'); + }); + }); + it('should find by string value, using bound params', function () { + return this.db.query('SELECT FROM SomeClass WHERE val = :val', { + params: { + val: '123456' + } + }) + .spread(function (doc) { + doc.val.should.equal('123456'); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/169-connect-directly-to-database.js b/test/bugs/169-connect-directly-to-database.js new file mode 100644 index 0000000..c3c94bf --- /dev/null +++ b/test/bugs/169-connect-directly-to-database.js @@ -0,0 +1,30 @@ +describe("Bug #169: Connect to db without server credentials", function () { + var server, db; + before(function () { + server = new LIB.Server({ + host: TEST_SERVER_CONFIG.host, + port: TEST_SERVER_CONFIG.port, + username: 'nope', + password: 'nope', + transport: 'binary', + useToken: false + }); + return CREATE_TEST_DB(this, 'testdb_bug_169') + .then(function () { + db = server.use({ + name: 'testdb_bug_169', + username: 'admin', + password: 'admin' + }); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_169'); + }); + it('should connect to the database directly', function () { + return db.select().from('OUser').all() + .then(function (results) { + results.length.should.be.above(0); + }); + }); +}); diff --git a/test/bugs/175-fetchplan-depth.js b/test/bugs/175-fetchplan-depth.js new file mode 100644 index 0000000..d3a986d --- /dev/null +++ b/test/bugs/175-fetchplan-depth.js @@ -0,0 +1,198 @@ +var Promise = require('bluebird'); + +describe("Bug #175: Fetchplan depth", function () { + var hasProtocolSupport; + function ifSupportedIt (text, fn) { + it(text, function () { + if (hasProtocolSupport) { + return fn.call(this); + } + }); + } + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_175') + .bind(this) + .then(function () { + hasProtocolSupport = this.db.server.transport.connection.protocolVersion >= 28; + return Promise.all([ + this.db.class.create('SomeVertex', 'V'), + this.db.class.create('SomeEdge', 'E'), + this.db.class.create('OtherEdge', 'E') + ]); + }) + .spread(function (vertex, edge1, edge2) { + return Promise.all([ + vertex.property.create([ + { + name: 'owner', + type: 'link', + linkedClass: 'OUser' + }, + { + name: 'val', + type: 'string' + } + ]), + edge1.property.create([ + { + name: 'foo', + type: 'string' + } + ]), + edge2.property.create([ + { + name: 'greeting', + type: 'string' + } + ]) + ]); + }) + .then(function () { + return this.db + .let('thing1', function (s) { + s + .create('VERTEX', 'SomeVertex') + .set({ + owner: new LIB.RID('#5:0'), + val: 'a' + }); + }) + .let('thing2', function (s) { + s + .create('VERTEX', 'SomeVertex') + .set({ + owner: new LIB.RID('#5:1'), + val: 'b' + }); + }) + .let('edge1', function (s) { + s + .create('EDGE', 'OtherEdge') + .set({ + greeting: 'Hello World' + }) + .from('$thing2') + .to('$thing1'); + }) + .create('EDGE', 'SomeEdge') + .set({ + foo: 'bar' + }) + .from('$thing1') + .to('$thing2') + .commit() + .all(); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_175'); + }); + ifSupportedIt('should return records using a fetchplan', function () { + return this.db + .select() + .from('SomeVertex') + .fetch({'*': 1}) + .limit(1) + .one() + .then(function (doc) { + doc.should.have.property('owner'); + doc.owner.should.have.property('name'); + doc.should.have.property('out_SomeEdge'); + doc.should.have.property('in_OtherEdge'); + + // allow for difference between 1.7 and 2.0 + if (doc.out_SomeEdge instanceof LIB.Bag) { + doc.out_SomeEdge = doc.out_SomeEdge.all(); + } + else if (!Array.isArray(doc.out_SomeEdge)) { + doc.out_SomeEdge = [doc.out_SomeEdge]; + } + if (doc.in_OtherEdge instanceof LIB.Bag) { + doc.in_OtherEdge = doc.in_OtherEdge.all(); + } + else if (!Array.isArray(doc.in_OtherEdge)) { + doc.in_OtherEdge = [doc.in_OtherEdge]; + } + + + doc.out_SomeEdge.forEach(function (item) { + item.should.not.be.an.instanceOf(LIB.RID); + }); + doc.in_OtherEdge.forEach(function (item) { + item.should.not.be.an.instanceOf(LIB.RID); + }); + }); + }); + ifSupportedIt('should return records, excluding edges using a fetchplan', function () { + return this.db.query('SELECT FROM SomeVertex LIMIT 1', { + fetchPlan: '*:1 in_*:-2 out_*:-2' + }) + .spread(function (doc) { + doc.should.have.property('owner'); + doc.owner.should.have.property('name'); + doc.should.have.property('out_SomeEdge'); + doc.should.have.property('in_OtherEdge'); + + // allow for difference between 1.7 and 2.0 + if (doc.out_SomeEdge instanceof LIB.Bag) { + doc.out_SomeEdge = doc.out_SomeEdge.all(); + } + else if (!Array.isArray(doc.out_SomeEdge)) { + doc.out_SomeEdge = [doc.out_SomeEdge]; + } + if (doc.in_OtherEdge instanceof LIB.Bag) { + doc.in_OtherEdge = doc.in_OtherEdge.all(); + } + else if (!Array.isArray(doc.in_OtherEdge)) { + doc.in_OtherEdge = [doc.in_OtherEdge]; + } + + doc.out_SomeEdge.forEach(function (item) { + item.should.be.an.instanceOf(LIB.RID); + }); + doc.in_OtherEdge.forEach(function (item) { + item.should.be.an.instanceOf(LIB.RID); + }); + }); + }); + ifSupportedIt('should return records, excluding edges using a fetchplan via the query builder', function () { + return this.db + .select() + .from('SomeVertex') + .fetch({ + '*': 1, + 'in_*':-2, + 'out_*':-2 + }) + .limit(1) + .one() + .then(function (doc) { + doc.should.have.property('owner'); + doc.owner.should.have.property('name'); + doc.should.have.property('out_SomeEdge'); + doc.should.have.property('in_OtherEdge'); + + // allow for difference between 1.7 and 2.0 + if (doc.out_SomeEdge instanceof LIB.Bag) { + doc.out_SomeEdge = doc.out_SomeEdge.all(); + } + else if (!Array.isArray(doc.out_SomeEdge)) { + doc.out_SomeEdge = [doc.out_SomeEdge]; + } + if (doc.in_OtherEdge instanceof LIB.Bag) { + doc.in_OtherEdge = doc.in_OtherEdge.all(); + } + else if (!Array.isArray(doc.in_OtherEdge)) { + doc.in_OtherEdge = [doc.in_OtherEdge]; + } + + + doc.out_SomeEdge.forEach(function (item) { + item.should.be.an.instanceOf(LIB.RID); + }); + doc.in_OtherEdge.forEach(function (item) { + item.should.be.an.instanceOf(LIB.RID); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/186-resolveReferences-duplicates.js b/test/bugs/186-resolveReferences-duplicates.js new file mode 100644 index 0000000..fde7e36 --- /dev/null +++ b/test/bugs/186-resolveReferences-duplicates.js @@ -0,0 +1,143 @@ +var Promise = require('bluebird'); + +describe("Bug #186: resolveReferences fail with duplicates present", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_186') + .bind(this) + .then(function () { + return Promise.map([ + 'create class Person extends V', + 'create class Restaurant extends V', + 'create class Eat extends E', + + 'create vertex Person set name = "Luca"', + 'create vertex Person set name = "Heisenberg"', + 'create vertex Restaurant set name = "Dante", type = "Pizza"', + 'create edge Eat from (select from Person where name = "Luca") to (select from Restaurant where name = "Dante") SET someProperty="something"', + 'create edge Eat from (select from Person where name = "Heisenberg") to (select from Restaurant where name = "Dante") SET someProperty="something"', + ], this.db.query.bind(this.db)); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_186'); + }); + + describe('Query a graph with fetchplan', function () { + function Person (data) { + if (!(this instanceof Person)) { + return new Person(data); + } + var keys = Object.keys(data), + length = keys.length, + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + this[key] = data[key]; + } + } + + function Restaurant (data) { + if (!(this instanceof Restaurant)) { + return new Restaurant(data); + } + var keys = Object.keys(data), + length = keys.length, + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + this[key] = data[key]; + } + } + + function Eat (data) { + if (!(this instanceof Eat)) { + return new Eat(data); + } + var keys = Object.keys(data), + length = keys.length, + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + this[key] = data[key]; + } + } + + function testPerson(person, verbose){ + (person.name === 'Luca' || person.name === 'Heisenberg').should.be.ok; + person.should.have.property('out_Eat'); + var edge = person.out_Eat; + if(edge instanceof LIB.Bag){ + edge = edge.all()[0]; + } + else if(edge instanceof Array){ + edge = edge[0]; // OrientDB 2.0 @this.toJSON() returns Array + } + if(verbose){ + console.log('edge (' + person.name + '): ' + require('util').inspect(edge, {depth: 3})); + } + edge.should.have.property('in'); + edge.in.should.have.property('name'); // breaks for depth 2: no name, as 'in' is a RID + edge.in.name.should.equal('Dante'); + } + + before(function () { + this.db.registerTransformer('Person', Person); + this.db.registerTransformer('Restaurant', Restaurant); + this.db.registerTransformer('Eat', Eat); + }); + + // Control tests + it('should return linked vertices when using @this.toJSON(fetchPlan) with depth 2', function () { + return this.db.query('SELECT @this.toJSON("fetchPlan:out_Eat:1 out_Eat.in:2") from Person').all() + .then(function (result) { + var people = []; + people[0] = JSON.parse(result[0].this); + people[1] = JSON.parse(result[1].this); + //console.log('people: ' + require('util').inspect(people, {depth: 3})); + people.length.should.be.equal(2); + people.forEach(function (person) { + testPerson(person); + }); + }); + }); + + it('should return linked vertices when using .fetch() with depth 1', function () { + return this.db.select().from('Person').fetch('out_Eat:1 out_Eat.in:1').all() + .then(function (people) { + //console.log('people: ' + require('util').inspect(people, {depth: 3})); + people.length.should.be.equal(2); + people.forEach(function (person) { + testPerson(person); + }); + }); + }); + + it('should return one linked vertex when using .fetch() with depth 2', function () { + return this.db.select().from('Person') + .limit(1) + .fetch('out_Eat:1 out_Eat.in:2').one() + .then(function (person) { + //console.log('\nperson: ' + require('util').inspect(person, {depth: 3})); + testPerson(person); + }); + }); + + // Relevant test + it('should return linked vertices when using .fetch() with depth 2', function () { + // OrientBD returns duplicates: #13:0 and #13:1 (edges) + // this.db.exec('select from Person', { fetchPlan: 'out_Eat:1 out_Eat.in:2' }).then(function (resultset) { + // console.log('results: ' + require('util').inspect(resultset.results) + '\n'); + // }); + + return this.db.select().from('Person').fetch('out_Eat:1 out_Eat.in:2').all() + .then(function (people) { + //console.log('\npeople: ' + require('util').inspect(people, {depth: 4})); + people.length.should.be.equal(2); + people.forEach(function (person) { + testPerson(person, false); + }); + }); + }); + + }); +}); diff --git a/test/bugs/188-subquery-with-vars.js b/test/bugs/188-subquery-with-vars.js new file mode 100644 index 0000000..6d92df3 --- /dev/null +++ b/test/bugs/188-subquery-with-vars.js @@ -0,0 +1,112 @@ +"use strict"; +var Bluebird = require('bluebird'); + +describe("Test sub-query + $parent.$current", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_188') + .bind(this) + .then(function () { + return this.db.class.create('VertexOne','V'); + }) + .then(function (vOneClass) { + return vOneClass.property.create({ + name: 'uuid', + type: 'Integer' + }); + }) + .then(function () { + return this.db.class.create('VertexTwo','V'); + }) + .then(function (vTwoClass) { + return vTwoClass.property.create({ + name: 'uuid', + type: 'Integer' + }); + }) + .then(function () { + return this.db.class.create('HAS_EDGE','E'); + }) + .then(function () { + return this.db + .create('VERTEX', 'VertexOne') + .set({ + uuid: 1 + }) + .one(); + }) + .then(function () { + return this.db + .create('VERTEX', 'VertexTwo') + .set({ + uuid: 2 + }) + .one(); + }) + .then(function () { + return this.db + .create('VERTEX', 'VertexTwo') + .set({ + uuid: 3 + }) + .one(); + }) + .then(function () { + return this.db + .create('EDGE', 'HAS_EDGE') + .from(function (s) { + s + .select() + .from('VertexOne') + .where({uuid: 1}); + }) + .to(function (s) { + s + .select() + .from('VertexTwo') + .where({uuid: 2}); + }) + .one(); + }) + .then(function () { + return this.db + .create('EDGE', 'HAS_EDGE') + .from(function (s) { + s + .select() + .from('VertexOne') + .where({uuid: 1}); + }) + .to(function (s) { + s + .select() + .from('VertexTwo') + .where({uuid: 3}); + }) + .one(); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_188'); + }); + + it('should deserialize a value properly', function () { + var deserializer = this.db.server.transport.connection.protocol.deserializer; // ugh! + deserializer.deserialize('$VTwo:[#-2:1,#-2:2]').should.eql({ + '@type': 'd', + '$VTwo': [ + new LIB.RID('#-2:1'), + new LIB.RID('#-2:2') + ] + }); + }); + + it("should test if request return a value", function () { + return this.db.query("SELECT expand($VTwo) FROM VertexOne LET $VTwo = (SELECT uuid FROM (SELECT expand(out('HAS_EDGE')) FROM $parent.$current)) WHERE uuid = 1") + .then(function (result) { + result.length.should.equal(2); + result.forEach(function (item) { + (item.uuid === 2 || item.uuid === 3).should.be.true; + }); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/189-embedded-document.js b/test/bugs/189-embedded-document.js new file mode 100644 index 0000000..5a0f3c5 --- /dev/null +++ b/test/bugs/189-embedded-document.js @@ -0,0 +1,197 @@ +"use strict"; +var Bluebird = require('bluebird'); + +describe("Bug #189: Error inserting new document with embedded document containing link type field", function () { + var rid; + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_189') + .bind(this) + .then(function () { + return Bluebird.all([ + this.db.class.create('Person', 'V'), + this.db.class.create('Address') + ]) + .spread(function (Person, Address) { + return Bluebird.all([ + Person.property.create([ + { + name: 'name', + type: 'string' + }, + { + name: 'referrer', + type: 'link' + }, + { + name: 'url', + type: 'string' + }, + { + name: 'primaryAddress', + type: 'embedded', + linkedType: 'Address' + }, + { + name: 'addresses', + type: 'embeddedset', + linkedType: 'Address' + } + ]), + Address.property.create([ + { + name: 'link', + type: 'link' + }, + { + name: 'url', + type: 'string' + }, + { + name: 'city', + type: 'string' + } + ]) + ]); + }); + }) + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_189'); + }); + + it('should insert a person with a primary address', function () { + var query = this.db + .create('VERTEX', 'Person') + .set({ + name: 'Bob', + referrer: new LIB.RID('#5:0'), + url: 'http://example.com/', + primaryAddress: { + '@type': 'document', + '@class': 'Address', + city: 'London', + link: new LIB.RID('#5:0'), + url: 'http://example.com/' + } + }) + return query.one() + .then(function (result) { + result.primaryAddress.city.should.equal('London'); + result.url.should.equal('http://example.com/'); + result.primaryAddress.url.should.equal('http://example.com/'); + }); + }); + + + it('should insert a person with a primary address and some other addresses', function () { + var query = this.db + .create('VERTEX', 'Person') + .set({ + name: 'Alice', + referrer: new LIB.RID('#5:0'), + url: 'http://example.com/', + primaryAddress: { + '@type': 'document', + '@class': 'Address', + city: 'London', + url: 'http://example.com/' + }, + addresses: [ + { + '@type': 'document', + '@class': 'Address', + city: 'London', + link: new LIB.RID('#5:0'), + url: 'http://example.com/' + }, + { + '@type': 'document', + '@class': 'Address', + city: 'Paris', + link: new LIB.RID('#5:1'), + url: 'http://example.com/' + } + ] + }) + return query.one() + .then(function (result) { + result.url.should.equal('http://example.com/'); + result.primaryAddress.url.should.equal('http://example.com/'); + result.primaryAddress.city.should.equal('London'); + result.addresses.length.should.equal(2); + result.addresses.forEach(function (address) { + address.url.should.equal('http://example.com/'); + (address.city === "London" || address.city === "Paris").should.be.true; + }); + }); + }); + + it('should insert some people using sql batch', function () { + var query = this.db + .let('bob', function (s) { + s + .create('VERTEX', 'Person') + .set({ + name: 'Bob', + referrer: new LIB.RID('#5:0'), + url: 'http://example.com/', + primaryAddress: { + '@type': 'document', + '@class': 'Address', + city: 'London', + link: new LIB.RID('#5:0'), + url: 'http://example.com/' + } + }); + }) + .let('alice', function (s) { + s + .create('VERTEX', 'Person') + .set({ + name: 'Alice', + referrer: new LIB.RID('#5:0'), + url: 'http://example.com/', + primaryAddress: { + '@type': 'document', + '@class': 'Address', + city: 'London', + url: 'http://example.com/' + }, + addresses: [ + { + '@type': 'document', + '@class': 'Address', + city: 'London', + link: new LIB.RID('#5:0'), + url: 'http://example.com/' + }, + { + '@type': 'document', + '@class': 'Address', + city: 'Paris', + link: new LIB.RID('#5:1'), + url: 'http://example.com/' + } + ] + }); + }) + .return('[$bob,$alice]') + .commit(); + + return query.all() + .spread(function (bob, alice) { + + bob.url.should.equal('http://example.com/'); + bob.primaryAddress.url.should.equal('http://example.com/'); + bob.primaryAddress.city.should.equal('London'); + alice.primaryAddress.city.should.equal('London'); + alice.url.should.equal('http://example.com/'); + alice.primaryAddress.url.should.equal('http://example.com/'); + alice.addresses.length.should.equal(2); + alice.addresses.forEach(function (address) { + address.url.should.equal('http://example.com/'); + (address.city === "London" || address.city === "Paris").should.be.true; + }); + }); + }); +}); diff --git a/test/bugs/195-connection-reset.js b/test/bugs/195-connection-reset.js new file mode 100644 index 0000000..be44332 --- /dev/null +++ b/test/bugs/195-connection-reset.js @@ -0,0 +1,29 @@ +var Bluebird = require('bluebird'); + +describe.skip("Bug #195: hang on connection reset", function () { + var rid; + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_195', 'plocal'); + }); + after(function () { + // return DELETE_TEST_DB('testdb_bug_195', 'plocal'); + }); + + it('should not hang on connection reset', function () { + var self = this; + this.timeout(10000); + return this.db.select().from('OUser').all() + .then(function () { + console.log('Quick, go restart orientdb'); + return Bluebird.delay(4000); + }) + .then(function () { + console.log('Assuming orientdb was restarted, trying to query again.'); + return self.db.select().from('OUser').all(); + }) + .then(function (users) { + console.log('end', users); + users.length.should.be.above(1); + }); + }); +}); diff --git a/test/bugs/203-param-date.js b/test/bugs/203-param-date.js new file mode 100644 index 0000000..b736d42 --- /dev/null +++ b/test/bugs/203-param-date.js @@ -0,0 +1,41 @@ +describe("Issue #203: support dates in parameterised queries", function () { + var date = new Date(Date.UTC(2015, 0, 17, 22, 5, 12)); + before(function () { + return CREATE_TEST_DB(this, 'testdb_issue_203') + .bind(this) + .then(function () { + return this.db.query('alter database timezone cet'); // something not UTC to avoid coincidences + }) + .then(function () { + return this.db.class.create('dates'); + }) + .then(function (item) { + this.class = item; + return this.class.property.create({ + name: 'schemaDate', + type: 'DateTime' + }); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_issue_203'); + }); + + it('should insert a date in schemaful mode', function () { + return this.db.insert().into('dates').set({ schemaDate: date }).one() + .then(function (result) { + result.should.have.property('@rid'); + result.should.have.property('schemaDate'); + result.schemaDate.should.eql(date); + }); + }); + + it('should insert a date in schemaless mode', function () { + return this.db.insert().into('dates').set({ newProp: date }).one() + .then(function (result) { + result.should.have.property('@rid'); + result.should.have.property('newProp'); + result.newProp.should.eql(date); + }); + }); +}); diff --git a/test/bugs/206-insert-plus.js b/test/bugs/206-insert-plus.js new file mode 100644 index 0000000..36a842f --- /dev/null +++ b/test/bugs/206-insert-plus.js @@ -0,0 +1,32 @@ +describe("Bug #206: Inserting string value '+' produces error ", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_206') + .bind(this) + .then(function () { + return this.db.class.create('SomeClass', 'V'); + }) + .then(function (item) { + this.class = item; + return this.class.property.create({ + name: 'val', + type: 'String' + }); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_206'); + }); + it('should insert a "+" sign at the start of a field', function () { + return this.db.insert().into('SomeClass').set({val: '+hello'}).one() + .then(function (result) { + result.val.should.equal('+hello'); + }); + }); + + it('should insert a "+" sign as the value of a field', function () { + return this.db.insert().into('SomeClass').set({val: '+'}).one() + .then(function (result) { + result.val.should.equal('+'); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/224-rest-vs-binary-protocol.js b/test/bugs/224-rest-vs-binary-protocol.js new file mode 100644 index 0000000..8001b05 --- /dev/null +++ b/test/bugs/224-rest-vs-binary-protocol.js @@ -0,0 +1,83 @@ +var Bluebird = require('bluebird'); + +describe("Bug #224: REST vs BINARY protocol", function () { + var rid; + before(function () { + var self = this; + return CREATE_TEST_DB(this, 'testdb_bug_224') + .then(function () { + return Bluebird.map( + [ + 'CREATE CLASS User extends V', + 'CREATE PROPERTY User.name STRING', + + 'CREATE CLASS Post extends V', + 'CREATE PROPERTY Post.message STRING', + 'CREATE PROPERTY Post.score INTEGER', + + 'CREATE CLASS WROTES extends E', + + + "CREATE VERTEX User SET name='Monica'", + + "CREATE VERTEX Post SET message='My 1st post', score = 1", + "CREATE VERTEX Post SET message='My 2nd post', score = 2", + + "CREATE EDGE WROTES FROM (SELECT FROM User) TO (SELECT FROM Post)", + ], + function (text) { + return self.db.query(text); + } + ); + }) + .then(function () { + return self.db.select('@rid').from('User').scalar(); + }) + .then(function (result) { + rid = result; + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_224'); + }); + + it('should retrieve some records', function () { + return this.db + .select() + .from('User') + .all() + .then(function (results) { + results.length.should.be.above(0); + }); + }); + + // skipping due to bug in versions of OrientDB <= 2.1-SNAPSHOT + it.skip('should work correctly', function () { + var query = this.db + .select('name') + .select('$DescendingPosts AS Posts') + .from(rid.toString()) + .let('DescendingPosts', function (s) { + s + .select('@rid, message, score') + .from(function (s) { + s + .select('expand(out("WROTES"))') + .from('$parent.$current'); + }) + .order({ + "score": 'DESC' + }); + }) + .fetch({Posts: 1}); + + return query + .one() + .then(function (data) { + data.name.should.equal('Monica'); + data.Posts.length.should.equal(2); + data.Posts[0].message.should.equal('My 2nd post'); + data.Posts[1].message.should.equal('My 1st post'); + }) + }); +}); diff --git a/test/bugs/231-field-filtering.js b/test/bugs/231-field-filtering.js new file mode 100644 index 0000000..42c2b01 --- /dev/null +++ b/test/bugs/231-field-filtering.js @@ -0,0 +1,98 @@ +var Bluebird = require('bluebird'); +var Statement = require('../../lib/db/statement'); + +describe("Bug #231: Field filtering does not handle substitutions", function () { + var rid, hasProtocolSupport = false; + + function ifSupportedIt (text, fn) { + it(text, function () { + if (hasProtocolSupport) { + return fn.call(this); + } + else { + console.log(' skipping, "'+text+'": operation not supported by OrientDB version'); + } + }); + } + + before(function () { + var self = this; + return CREATE_TEST_DB(this, 'testdb_bug_231') + .then(function () { + return Bluebird.map( + [ + 'CREATE CLASS Person EXTENDS V', + 'CREATE CLASS Car EXTENDS V', + 'CREATE CLASS Drives EXTENDS E', + + "CREATE VERTEX Person SET name='Fred'", + + "CREATE VERTEX Car SET make='Volvo', vin = '1234'", + "CREATE VERTEX Car SET make='Tesla', vin = '5678'", + + "CREATE EDGE Drives FROM (SELECT FROM Person) TO (SELECT FROM Car)" + ], + function (text) { + return self.db.query(text); + } + ); + }) + .then(function () { + return self.db.select('@rid').from('Person').scalar(); + }) + .then(function (result) { + rid = result; + hasProtocolSupport = self.db.server.transport.connection.protocolVersion >= 28; + }); + }); + + after(function () { + return DELETE_TEST_DB('testdb_bug_231'); + }); + + it('should substitute parameters in filters', function () { + var s = new Statement(); + s.select('out(:edge)[val=:value]').from('Person').addParams({edge: "E", value: 123}).toString().should.equal( + 'SELECT out("E")[val=123] FROM Person' + ); + }); + + + ifSupportedIt('should select using a normal query', function () { + return this.db.query("SELECT expand(out(\"Drives\")[vin=\"1234\"]) FROM Person WHERE name = \"Fred\"") + .spread(function (data) { + data.vin.should.equal('1234'); + }); + }); + + ifSupportedIt('should select using a prepared query', function () { + return this.db.query("select expand( out( :d )[vin=:v] ) from Person WHERE name = :name", { + params: { + d: 'Drives', + v: '1234', + name: "Fred" + } + }) + .spread(function (data) { + data.vin.should.equal('1234'); + }); + }); + + + ifSupportedIt('should select using the query builder', function () { + var query = this.db + .select("expand(out(:d)[vin=:v])") + .from('Person') + .where({name: 'Fred'}) + .addParams({ + d: 'Drives', + v: '1234' + }); + + return query + .one() + .then(function (data) { + data.vin.should.equal('1234'); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/238-hang-on-invalid-credentials.js b/test/bugs/238-hang-on-invalid-credentials.js new file mode 100644 index 0000000..6e04626 --- /dev/null +++ b/test/bugs/238-hang-on-invalid-credentials.js @@ -0,0 +1,148 @@ +describe("Bug #238: Request hangs when attempting connection with invalid credentials", function () { + var hasProtocolSupport = false, + self = this, + serverValid, serverInvalid; + + //////////////////////////////////////////// + function ifSupportedIt(text, fn) { + it(text, function () { + if (hasProtocolSupport) { + return fn.call(this); + } + else { + console.log(' skipping, "' + text + '": operation not supported by OrientDB version'); + } + }); + } + + function createTestDb(server, name, type) { + type = type || 'memory'; + return server.exists(name, type) + .then(function (exists) { + if (exists) { + return server.drop({ + name: name, + storage: type + }); + } + else { + return false; + } + }) + .then(function () { + return server.create({ + name: name, + type: 'graph', + storage: type + }); + }) + .then(function (db) { + self.db = db; + }); + } + + function deleteTestDb(server, name, type) { + type = type || 'memory'; + return server.exists(name, type) + .then(function (exists) { + if (exists) { + return server.drop({ + name: name, + storage: type + }); + } + else { + return undefined; + } + }) + .then(function () { + return undefined; + }); + } + + function newValidServer() { + return new LIB.Server({ + host: TEST_SERVER_CONFIG.host, + port: TEST_SERVER_CONFIG.port, + username: TEST_SERVER_CONFIG.username, + password: TEST_SERVER_CONFIG.password, + transport: 'binary', + useToken: true + }); + } + + //////////////////////////////////////////// + + before(function () { + serverValid = newValidServer(); + + return createTestDb(serverValid, 'testdb_bug_238') + .then(function () { + + serverInvalid = new LIB.Server({ + host: TEST_SERVER_CONFIG.host, + port: TEST_SERVER_CONFIG.port, + username: 'nonononono', + password: 'nopenopenopenopenope', + transport: 'binary', + useToken: true + }); + + hasProtocolSupport = self.db.server.transport.connection.protocolVersion >= 28; + }); + }); + after(function () { + return deleteTestDb(serverValid, 'testdb_bug_238'); + }); + + ifSupportedIt('should connect to the database with valid credentials', function () { + return serverValid.use('testdb_bug_238').open() + .then(function (db) { + db.name.should.equal('testdb_bug_238'); + db.token.should.be.an.instanceOf(Buffer); + db.token.length.should.be.above(0); + }); + }); + + ifSupportedIt('should fail to open a database with invalid server credentials', function () { + var db = serverInvalid.use('testdb_bug_238'); + + return db.open() + .then(function (data) { + throw new Error('should never happen.'); + }) + .catch(LIB.errors.RequestError, function (err) { + err.message.should.match(/password/i); + }); + }); + + ifSupportedIt('should open a database with valid database credentials', function () { + var db = serverValid.use({ + name: 'testdb_bug_238', + username: 'reader', + password: 'reader' + }); + + return db.open() + .then(function (db) { + db.token.length.should.be.above(0); + }); + }); + + ifSupportedIt('should fail to open a database with invalid database credentials', function () { + + var db = serverValid.use({ + name: 'testdb_bug_238', + username: 'nonononono', + password: 'nopenopenopenopenope' + }); + + return db.open() + .then(function (data) { + throw new Error('should never happen.'); + }) + .catch(LIB.errors.RequestError, function (err) { + err.message.should.match(/password/i); + }); + }); +}); diff --git a/test/bugs/25-property-create.js b/test/bugs/25-property-create.js new file mode 100644 index 0000000..29c084d --- /dev/null +++ b/test/bugs/25-property-create.js @@ -0,0 +1,31 @@ +describe("Bug #25: Create undefined in Myclass.property.create", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_25') + .bind(this) + .then(function () { + return this.db.class.create('Member', 'V'); + }) + .then(function (item) { + this.class = item; + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_25'); + }); + + it('Let me create a property immediately after creating a class', function () { + var values = { + name: 'name', + type: 'String', + mandatory: true, + max: 65 + }; + return this.class.property.create(values) + .then(function (prop) { + prop.should.have.property('name'); + prop.should.have.property('type'); + prop.should.have.property('mandatory'); + prop.should.have.property('max'); + }) + }); +}); diff --git a/test/bugs/252-save-embedded-map.js b/test/bugs/252-save-embedded-map.js new file mode 100644 index 0000000..7ea1134 --- /dev/null +++ b/test/bugs/252-save-embedded-map.js @@ -0,0 +1,68 @@ +describe("Bug #252: Unable to save plain EmbededMap", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_252') + .bind(this) + .then(function () { + return this.db.class.create('TestEmbeddedMap'); + }) + .then(function (TestEmbeddedMap) { + return TestEmbeddedMap.property.create([ + { + name: 'name', + type: 'string' + }, + { + name: 'map', + type: 'embeddedmap', + linkedType: 'string' + }, + { + name: 'list', + type: 'embeddedlist' + } + ]) + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_252'); + }); + + it('should insert a map into the database', function () { + return this.db + .insert() + .into('TestEmbeddedMap') + .set({ + name: 'abc', + map: { + k1: 'v1', + k2: 'v2' + } + }) + .one() + .then(function (res) { + res.map.k1.should.equal('v1'); + }); + }); + + describe('Bug #255: param is not working with embeddedlist', function () { + it('should allow params in embedded list', function () { + return this.db.query('INSERT INTO TestEmbeddedMap SET name = :name, list = :list', { + params: { + name: 'def', + list: [ + { + controller: 'home' + } + ] + } + }) + .bind(this) + .then(function () { + return this.db.select().from('TestEmbeddedMap').where({name: 'def'}).one(); + }) + .then(function (row) { + row.list[0].controller.should.equal('home'); + }); + }); + }); +}); diff --git a/test/bugs/26-number-strings.js b/test/bugs/26-number-strings.js new file mode 100644 index 0000000..fc34413 --- /dev/null +++ b/test/bugs/26-number-strings.js @@ -0,0 +1,22 @@ +describe("Bug #26: Issue while adding IP as a value to a Vertex", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_26') + .bind(this) + .then(function () { + return this.db.class.create('Host'); + }) + .then(function (item) { + this.class = item; + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_26'); + }); + + it('should insert an IP into the database, using a dynamic field', function () { + return this.db.insert().into('Host').set({ip: '127.0.0.1'}).one() + .then(function (host) { + host.ip.should.equal('127.0.0.1'); + }) + }) +}); \ No newline at end of file diff --git a/test/bugs/27-slow.js b/test/bugs/27-slow.js new file mode 100644 index 0000000..823aab4 --- /dev/null +++ b/test/bugs/27-slow.js @@ -0,0 +1,137 @@ +var Promise = require('bluebird'); + +describe("Bug #27: Slow compared to Restful API", function () { + this.timeout(10 * 10000); + var LIMIT = 5000; + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_27_slow', 'plocal') + .bind(this) + .then(function () { + return this.db.class.create('School', 'V'); + }) + .then(function (item) { + this.class = item; + return item.property.create([ + { + name: 'name', + type: 'String', + mandator: true + }, + { + name: 'address', + type: 'String' + } + ]) + }) + .then(function () { + var rows = [], + total = LIMIT, + i, row; + for (i = 0; i < total; i++) { + row = { + name: 'School ' + i, + address: (122 + i) + ' Fake Street' + }; + rows.push(row); + } + return this.class.create(rows); + }) + .then(function (results) { + results.length.should.equal(LIMIT); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_27_slow', 'plocal'); + }); + + it('should load a lot of records quickly, using the binary raw command interface', function () { + var start = Date.now(); + return this.db.send('command', { + database: 'testdb_bug_27_slow', + class: 'com.orientechnologies.orient.core.sql.query.OSQLSynchQuery', + limit: LIMIT * 2, + query: 'SELECT * FROM School', + mode: 's' + }) + .then(function (response) { + var stop = Date.now(); + response.results[0].content.length.should.equal(LIMIT); + console.log('Binary Protocol Took ', (stop - start) + 'ms,', Math.round((LIMIT / (stop - start)) * 1000), 'documents per second') + }) + }); + + it('should load a lot of records quickly, using the rest raw command interface', function () { + var start = Date.now(); + return REST_SERVER.send('command', { + database: 'testdb_bug_27_slow', + class: 'com.orientechnologies.orient.core.sql.query.OSQLSynchQuery', + limit: LIMIT * 2, + query: 'SELECT * FROM School', + mode: 's' + }) + .then(function (response) { + var stop = Date.now(); + response.results[0].content.length.should.equal(LIMIT); + console.log('Rest Protocol Took ', (stop - start) + 'ms,', Math.round((LIMIT / (stop - start)) * 1000), 'documents per second') + }) + }); + + it('should load a lot of records quickly', function () { + var start = Date.now(); + return this.db.select().from('School').all() + .then(function (results) { + var stop = Date.now(); + results.length.should.equal(LIMIT); + console.log('Binary DB Api Took ', (stop - start) + 'ms,', Math.round((LIMIT / (stop - start)) * 1000), 'documents per second') + }) + }); + + it('should load a lot of records, one at a time, using binary', function () { + var start = Date.now(); + var cluster = this.class.defaultClusterId, + promises = [], + i; + + for (i = 0; i < LIMIT; i++) { + promises.push(this.db.send('record-load', { + database: 'testdb_bug_27_slow', + cluster: cluster, + position: i + })); + } + + return Promise.all(promises) + .then(function (results) { + var stop = Date.now(); + results.length.should.equal(LIMIT); + console.log('Binary Record Load Took ', (stop - start) + 'ms,', Math.round((LIMIT / (stop - start)) * 1000), 'documents per second') + }) + }); + + // skip the following because orientdb hangs up the socket. + it.skip('should load a lot of records, one at a time, using rest', function () { + var start = Date.now(); + var cluster = this.class.defaultClusterId, + promises = [], + i; + + for (i = 0; i < LIMIT; i++) { + promises.push(REST_SERVER.send('record-load', { + database: 'testdb_bug_27_slow', + cluster: cluster, + position: i + })); + } + + return Promise.all(promises) + .then(function (results) { + var stop = Date.now(); + results.length.should.equal(LIMIT); + console.log('Rest Record Load Took ', (stop - start) + 'ms,', Math.round((LIMIT / (stop - start)) * 1000), 'documents per second') + }) + }); + + + + +}); \ No newline at end of file diff --git a/test/bugs/328-wrong-backslash-quoting.js b/test/bugs/328-wrong-backslash-quoting.js new file mode 100644 index 0000000..3cca293 --- /dev/null +++ b/test/bugs/328-wrong-backslash-quoting.js @@ -0,0 +1,27 @@ +describe("Bug #328: wrong serialization of fields with multiple backslash characters", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_328') + .bind(this) + .then(function () { + return this.db.class.create('TestSerializeBackslash'); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_328'); + }); + + it('should insert a document with correct quotes for backslashes', function () { + return this.db + .insert() + .into('TestSerializeBackslash') + .set({ + foo: 'kratke, , nadherne, proste bozi, chjo som sa ostrihal :\\\\', + bar: '>' + }) + .one() + .then(function (res) { + res.foo.should.equal('kratke, , nadherne, proste bozi, chjo som sa ostrihal :\\\\'); + }); + }); + +}); diff --git a/test/bugs/329-pullreq-do-not-escape-slash.js b/test/bugs/329-pullreq-do-not-escape-slash.js new file mode 100644 index 0000000..8a2e796 --- /dev/null +++ b/test/bugs/329-pullreq-do-not-escape-slash.js @@ -0,0 +1,22 @@ +describe('Pull Request #329: Not escaping leading forward slash "(/)"', function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_PR_329'); + }); + after(function () { + return DELETE_TEST_DB('testdb_PR_329'); + }); + it('should still enable inserting record with field like "/// TE /// ST \\\\\\"', function () { + return this.db.insert().into('v').set({val: '/// TE /// ST \\\\\\'}).one() + .then(function (result) { + result.val.should.equal('/// TE /// ST \\\\\\'); + }); + }); + + it('should still enable where clause with field like "/// TE /// ST \\\\\\"', function () { + return this.db.select().from('v').where({val: '/// TE /// ST \\\\\\'}).one() + .then(function (result) { + result.val.should.equal('/// TE /// ST \\\\\\'); + }); + }); + +}); \ No newline at end of file diff --git a/test/bugs/59-close-connection.js b/test/bugs/59-close-connection.js new file mode 100644 index 0000000..2b8b012 --- /dev/null +++ b/test/bugs/59-close-connection.js @@ -0,0 +1,32 @@ +describe("Bug #59: hang on closing server connection", function () { + var server; + before(function () { + return CREATE_TEST_DB(this, 'testdb_59_close_connection') + .bind(this) + .then(function () { + server = new LIB.Server({ + host: TEST_SERVER_CONFIG.host, + port: TEST_SERVER_CONFIG.port, + username: TEST_SERVER_CONFIG.username, + password: TEST_SERVER_CONFIG.password, + transport: 'binary' + }); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_59_close_connection'); + }); + it('should close the server connection correctly', function () { + + var db = server.use('testdb_59_close_connection'); + + return db.class.list() + .then(function (classes) { + classes.length.should.be.above(1); + return server.close(); + }) + .then(function (server) { + expect(server.transport.connection.socket).to.equal(null); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/65-array-index-create.js b/test/bugs/65-array-index-create.js new file mode 100644 index 0000000..8b040ef --- /dev/null +++ b/test/bugs/65-array-index-create.js @@ -0,0 +1,38 @@ +describe("Bug #65: Passing JSON array to db.index.create", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_65') + .bind(this) + .then(function () { + return this.db.class.create('Member', 'V'); + }) + .then(function (item) { + this.class = item; + return this.class.property.create([ + { + name: 'name', + type: 'String' + }, + { + name: 'altName', + type: 'String' + } + ]); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_65'); + }); + + it('Let me create multiple indices at once', function () { + return this.db.index.create([ + { + name: 'Member.name', + type: 'unique' + }, + { + name: 'Member.altName', + type: 'unique' + } + ]); + }); +}); diff --git a/test/bugs/69-order-by-date.js b/test/bugs/69-order-by-date.js new file mode 100644 index 0000000..1a70392 --- /dev/null +++ b/test/bugs/69-order-by-date.js @@ -0,0 +1,60 @@ +describe("Bug #69: Order by date", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_69') + .bind(this) + .then(function () { + return this.db.class.create('Member', 'V'); + }) + .then(function (item) { + this.class = item; + return this.class.property.create([ + { + name: 'name', + type: 'String' + }, + { + name: 'creation', + type: 'DateTime' + } + ]); + }) + .then(function () { + return this.class.create([ + { + name: 'a', + creation: '2001-01-01 00:00:01' + }, + { + name: 'b', + creation: '2001-01-02 12:00:01' + }, + { + name: 'c', + creation: '2009-01-01 00:12:01' + }, + { + name: 'd', + creation: '2014-09-01 00:01:01' + }, + { + name: 'e', + creation: '2014-09-01 00:24:01' + } + ]) + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_69'); + }); + + it('should order by date', function () { + var query = this.db.select().from('Member').order('creation desc'); + return query.all() + .map(function (result) { + return result.name; + }) + .then(function (results) { + results.should.eql(['e', 'd', 'c', 'b', 'a']); + }); + }); +}); diff --git a/test/bugs/79-insert-errors.js b/test/bugs/79-insert-errors.js new file mode 100644 index 0000000..7041b0c --- /dev/null +++ b/test/bugs/79-insert-errors.js @@ -0,0 +1,54 @@ +describe("Bug #79: Error when inserting", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_79') + .bind(this) + .then(function () { + return this.db.class.create('User'); + }) + .then(function (item) { + this.class = item; + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_79'); + }); + + it('Let me create a property immediately after creating a class', function () { + var data = { + "acceptedTerms":true, + "activitiesCount":0, + "appFirstUseDate":{"__type":"Date","iso":"2013-03-26T10:36:23.050Z"}, + "email":"REMOVED@hotmail.com", + "emailVerified":true, + "first_name":"Imogen", + "followerCount":0, + "followingCount":0, + "gender":2, + "goal":2, + "height_unit":1, + "height_val1":5, + "height_val2":3, + "homeEquipment":[4,3,6,105,107], + "last_name":".", + "level":3, + "numReferrals":0, + "postCount":0, + "subscribedToPush":true, + "timezone":"America/New_York", + "unsubscribedFromWorkoutEmails":true, + "username":"imogenxoxo", + "weight":93, + "weight_unit":1, + "createdAt":"2013-03-26T10:38:04.971Z", + "updatedAt":"2014-04-09T17:18:38.577Z", + "objectId":"l402K4JOu4", + "ACL":{"*":{"read":true},"l402K4JOu4":{"read":true,"write":true}}, + "sessionToken":"sue1t43xj80miwi4s3ky49ybo" + }; + + return this.db.insert().into('User').set(data).one() + .then(function (res) { + res.acceptedTerms.should.equal(true); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/80-base64.js b/test/bugs/80-base64.js new file mode 100644 index 0000000..107b560 --- /dev/null +++ b/test/bugs/80-base64.js @@ -0,0 +1,119 @@ +describe("Bug #80: Bad Base64 input character decimal 95", function () { + var rid; + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_80') + .bind(this) + .then(function () { + return this.db.class.create('User'); + }) + .then(function (item) { + this.class = item; + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_80'); + }); + + var input = { + "settings": { + "__type": "Pointer", + "className": "Settings", + "objectId": "xdOfM1NauR" + }, + "acceptedTerms": true, + "activitiesCount": 16, + "appFirstUseDate": { + "__type": "Date", + "iso": "2014-06-18T21:01:14.057Z" + }, + "birthday": { + "__type": "Date", + "iso": "1989-01-10T00:00:00.000Z" + }, + "email": "EMAIL_REMOVED@gmail.com", + "equipment": { + "1": [ + 1, + 2, + 3, + 6, + 7, + 10, + 100, + 101, + 105, + 107, + 108, + 109, + 5, + 11, + 23, + 8, + 13, + 22, + 4 + ] + }, + "feedOption": 2, + "followerCount": 0, + "followingCount": 5, + "followingFeedLastReadAt": { + "__type": "Date", + "iso": "2014-06-23T20:00:46.903Z" + }, + "gender": 2, + "goal": 4, + "height_unit": 1, + "height_val1": 5, + "height_val2": 1, + "lastPushNotificationPrompt": { + "__type": "Date", + "iso": "2014-06-22T16:11:51.991Z" + }, + "lastRatePrompt": { + "__type": "Date", + "iso": "2014-06-20T17:06:03.189Z" + }, + "lastVersionUsed": "3.0.0", + "level": 3, + "needsToSeePushNotificationPrompt": false, + "newFeedLastReadAt": { + "__type": "Date", + "iso": "2014-06-19T14:36:09.877Z" + }, + "numReferrals": 0, + "platform": 1, + "popularFeedLastReadAt": { + "__type": "Date", + "iso": "2014-06-20T17:06:13.174Z" + }, + "postCount": 8, + "seenRatePrompt": true, + "stream": "a", + "timezone": "America/Havana", + "unsubscribedFromWorkoutEmails": true, + "username": "USERNAME_REMOVED", + "weight": 120, + "weight_unit": 1, + "createdAt": "2014-06-18T21:01:59.471Z", + "updatedAt": "2014-06-23T20:01:27.888Z", + "objectId": "OBJID", + "ACL": { + "*": { + "read": true + }, + "OBJID": { + "read": true, + "write": true + } + }, + "sessionToken": "36DawJQJgQmmZorP1sRcFAAp3" + }; + + it('should insert the user', function () { + return this.db.insert().into('User').set(input).one() + .then(function (response) { + response.should.have.property('@rid'); + }); + }); +}); \ No newline at end of file diff --git a/test/bugs/82-emojis.js b/test/bugs/82-emojis.js new file mode 100644 index 0000000..a9dce62 --- /dev/null +++ b/test/bugs/82-emojis.js @@ -0,0 +1,49 @@ +describe("Bug #82: db.query errors when parsing emojis ", function () { + var rid; + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_82') + .bind(this) + .then(function () { + return this.db.class.create('Emoji'); + }) + .then(function (item) { + this.class = item; + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_82'); + }); + + it('should allow emojis in insert statements', function () { + return this.db.insert().into('Emoji').set({value: '😢😂😭'}).one() + .then(function (result) { + result.should.have.property('@rid'); + rid = result['@rid']; + }); + }); + it('should allow emojis in update statements', function () { + return this.db.update(rid).set({value: 'hello 😢😂😭', foo: 'bar'}).one(); + }); + + it('should allow emojis using db.query() directly', function () { + var query = 'UPDATE #5:0 SET bio="😢😂"'; + return this.db.query(query) + .bind(this) + .spread(function (result) { + result.should.eql('1'); + return this.db.query('SELECT * FROM #5:0'); + }) + .spread(function (result) { + result.bio.should.equal("😢😂"); + }); + }); + + describe('Bug #180: Emoji characters are not saved correctly', function () { + it('should insert some emojis', function () { + return this.db.insert().into('Emoji').set({value: "testing emoji 💪💦👌"}).one() + .then(function (result) { + result.value.should.equal("testing emoji 💪💦👌"); + }); + }); + }); +}); diff --git a/test/bugs/84-rid-isValid-array.js b/test/bugs/84-rid-isValid-array.js new file mode 100644 index 0000000..426b35e --- /dev/null +++ b/test/bugs/84-rid-isValid-array.js @@ -0,0 +1,6 @@ +describe("Bug #84: Bug in RecordID.isValid with array input", function () { + it('validate array input', function () { + var input = ['#1:23', '#4:56', '#6:79']; + LIB.RID.isValid(input).should.be.true; + }); +}); \ No newline at end of file diff --git a/test/bugs/99-set-with-string.js b/test/bugs/99-set-with-string.js new file mode 100644 index 0000000..acc6da8 --- /dev/null +++ b/test/bugs/99-set-with-string.js @@ -0,0 +1,11 @@ +var Statement = require('../../lib/db/statement'); + +describe("Bug #99: Error using .set() with string", function () { + + it('should allow strings to be specified without parentheses', function () { + var s = new Statement(); + s.update('foo').set('a = 123').toString().should.equal( + 'UPDATE foo SET a = 123' + ); + }); +}); \ No newline at end of file diff --git a/test/bugs/xxx-link-when-create-edge.js b/test/bugs/xxx-link-when-create-edge.js new file mode 100644 index 0000000..490b92d --- /dev/null +++ b/test/bugs/xxx-link-when-create-edge.js @@ -0,0 +1,81 @@ +var Bluebird = require('bluebird'); + +describe("Bug: Should create a link while inserting an edge", function () { + var first, second, third; + before(function () { + return CREATE_TEST_DB(this, 'testdb_bug_edge_link') + .bind(this) + .then(function () { + return Bluebird.all([ + this.db.class.create('Thing', 'V'), + this.db.class.create('Knows', 'E') + ]); + }) + .spread(function (Thing, Knows) { + return Bluebird.all([ + Thing.property.create([ + { + name: 'name', + type: 'string' + } + ]), + Knows.property.create([ + { + name: 'referrer', + type: 'link' + } + ]) + ]); + }) + .then(function () { + return this.db + .let('first', "CREATE VERTEX Thing SET name = 'first'") + .let('second', "CREATE VERTEX Thing SET name = 'second'") + .let('third', "CREATE VERTEX Thing SET name = 'third'") + .return(['$first', '$second', '$third']) + .commit() + .all() + .then(function (results) { + first = results[0]; + second = results[1]; + third = results[2]; + }); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_bug_edge_link'); + }); + it('should create a link whilst creating an edge', function () { + return this.db + .create('EDGE', 'Knows') + .from(first['@rid']) + .to(second['@rid']) + .set({ + referrer: third['@rid'] + }) + .one() + .then(function (result) { + result.referrer.equals(third['@rid']).should.be.true; + }) + }); + + it('should create a link whilst creating an edge in a transaction', function () { + return this.db + .let('fourth', "CREATE VERTEX Thing SET name = 'fourth'") + .let('fifth', "CREATE VERTEX Thing SET name = 'fifth'") + .let('sixth', "CREATE VERTEX Thing SET name = 'sixth'") + .let('knows', function (s) { + s + .create('EDGE', 'Knows') + .from('$fourth') + .to('$fifth') + .set('referrer = first($sixth)') + }) + .return(['$sixth', '$knows']) + .commit() + .all() + .spread(function (referrer, edge) { + edge.referrer.should.equal(referrer); + }); + }); +}); \ No newline at end of file diff --git a/test/core/bag-test.js b/test/core/bag-test.js new file mode 100644 index 0000000..af0cca0 --- /dev/null +++ b/test/core/bag-test.js @@ -0,0 +1,169 @@ +var utils = require('../../lib/utils'); + +describe("RID Bag", function () { + describe('Embedded Bag', function () { + before(function () { + var self = this; + return CREATE_TEST_DB(this, 'testdb_dbapi_rid_bag_embedded') + .bind(this) + .then(function () { + return this.db.class.create('Person', 'V'); + }) + .then(function (Person) { + this.Person = Person; + return this.db.class.create('Knows', 'E'); + }) + .then(function (Knows) { + this.Knows = Knows; + return this.Person.create({ + name: 'John Smith' + }); + }) + .then(function (subject) { + var limit = 10, + i; + this.subject = subject; + this.people = []; + for (i = 0; i < limit; i++) { + this.people.push({ + name: 'Friend ' + i + }); + } + return this.Person.create(this.people); + }) + .then(function () { + return this.db.edge + .from(this.subject['@rid']) + .to('SELECT * FROM Person WHERE name LIKE "Friend%"') + .create('Knows'); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_dbapi_rid_bag_embedded'); + }); + + beforeEach(function () { + return this.db + .select() + .from(this.subject['@rid']) + .fetch({'*': 2}) + .one() + .bind(this) + .then(function (record) { + this.bag = record.out_Knows; + }); + }); + + it('should load a bag', function () { + this.bag.should.be.an.instanceOf(LIB.Bag) + this.bag.type.should.equal(LIB.Bag.BAG_EMBEDDED); + expect(this.bag.uuid).to.equal(null); + this.bag.size.should.equal(10); + }); + + it('should iterate the contents in the bag', function () { + var size = this.bag.size, + i = 0, + item; + while((item = this.bag.next())) { + item.should.be.an.instanceOf(LIB.RID); + i++; + } + i.should.equal(10); + }); + + it('should return all the contents of the bag', function () { + var contents = this.bag.all(); + contents.length.should.equal(10); + contents.forEach(function (item) { + item.should.have.property('@rid'); + }); + }); + + it('should return the right JSON representation', function () { + var json = utils.jsonify(this.bag) + decoded = JSON.parse(json); + decoded.length.should.equal(10); + decoded.forEach(function (item) { + item.should.have.property('@rid'); + }); + }); + + describe('Optional RIDBags', function () { + before(function () { + this.db.server.transport.connection.protocol.deserializer.enableRIDBags = false; + }); + after(function () { + this.db.server.transport.connection.protocol.deserializer.enableRIDBags = true; + }); + it('should optionally disable RIDBags', function () { + Array.isArray(this.bag).should.be.true; + }); + }); + }); + + describe('Tree Bag', function () { + before(function () { + this.timeout(20000); + var self = this; + return CREATE_TEST_DB(this, 'testdb_dbapi_rid_bag_tree', 'plocal') + .bind(this) + .then(function () { + return this.db.class.create('Person', 'V'); + }) + .then(function (Person) { + this.Person = Person; + return this.db.class.create('Knows', 'E'); + }) + .then(function (Knows) { + this.Knows = Knows; + return this.Person.create({ + name: 'John Smith' + }); + }) + .then(function (subject) { + var limit = 120, + i; + this.subject = subject; + this.people = []; + for (i = 0; i < limit; i++) { + this.people.push({ + name: 'Friend ' + i + }); + } + return this.Person.create(this.people); + }) + .then(function () { + return this.db.edge + .from(this.subject['@rid']) + .to('SELECT * FROM Person WHERE name LIKE "Friend%"') + .create('Knows'); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_dbapi_rid_bag_tree', ''); + }); + + beforeEach(function () { + return this.db + .select() + .from(this.subject['@rid']) + .fetch({'*': 2}) + .one() + .bind(this) + .then(function (record) { + this.bag = record.out_Knows; + }); + }); + + it('should load a bag', function () { + this.bag.should.be.an.instanceOf(LIB.Bag) + this.bag.type.should.equal(LIB.Bag.BAG_TREE); + expect(this.bag.uuid).to.equal(null); + // > note: following behavior changes since protocol 19 + // old versions return the number of records, newer ones don't. + // newer versions must ask orient + expect(this.bag.size === -1 || this.bag.size === 120).to.be.true; + }); + }); +}); diff --git a/test/core/jwt.js b/test/core/jwt.js new file mode 100644 index 0000000..c2fb64c --- /dev/null +++ b/test/core/jwt.js @@ -0,0 +1,178 @@ +var Bluebird = require('bluebird'); +describe('JWT', function () { + var hasProtocolSupport = false, + server = null; + function ifSupportedIt (text, fn) { + it(text, function () { + if (hasProtocolSupport) { + return fn.call(this); + } + else { + console.log(' skipping, "'+text+'": operation not supported by OrientDB version'); + } + }); + } + before(function () { + server = new LIB.Server({ + host: TEST_SERVER_CONFIG.host, + port: TEST_SERVER_CONFIG.port, + username: TEST_SERVER_CONFIG.username, + password: TEST_SERVER_CONFIG.password, + transport: 'binary', + useToken: true + }); + return CREATE_TEST_DB(this, 'testdb_jwt') + .bind(this) + .then(function () { + hasProtocolSupport = this.db.server.transport.connection.protocolVersion >= 28; + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_jwt'); + }); + describe('JWT Server::connect()', function () { + var dbs; + before(function () { + return server.list() + .then(function (items) { + dbs = items; + }); + }); + ifSupportedIt('should connect to the server and get a token', function () { + server.token.should.be.an.instanceOf(Buffer); + server.token.length.should.be.above(0); + }); + ifSupportedIt('should retrieve a list of databases', function () { + dbs.length.should.be.above(0); + }); + }); + describe('JWT Server::use()', function () { + var db; + before(function () { + db = server.use({ + name: 'testdb_jwt', + username: 'admin', + password: 'admin' + }); + return db.open(); + }) + ifSupportedIt('should open a database and get a token', function () { + db.token.should.be.an.instanceOf(Buffer); + db.token.length.should.be.above(0); + }); + ifSupportedIt('should return a different token from the server token', function () { + db.token.toString().should.not.equal(server.token.toString()); + }); + ifSupportedIt('should execute commands using the token', function () { + return db.select().from('OUser').all() + .then(function (users) { + users.length.should.be.above(0); + }); + }); + }); + describe('JWT Database::query()', function () { + var db, admin, reader, writer; + before(function () { + if (hasProtocolSupport) { + db = server.use('testdb_jwt'); + return Bluebird.all([ + server.use({name: 'testdb_jwt', username: 'admin', password: 'admin'}).open(), + server.use({name: 'testdb_jwt', username: 'reader', password: 'reader'}).open(), + server.use({name: 'testdb_jwt', username: 'writer', password: 'writer'}).open() + ]) + .then(function (items) { + admin = items[0].token; + reader = items[1].token; + writer = items[2].token; + + admin.toString().should.not.equal(reader.toString()); + admin.toString().should.not.equal(writer.toString()); + writer.toString().should.not.equal(reader.toString()); + }); + } + }); + ifSupportedIt('should not allow the reader to create a vertex', function () { + return db.create('VERTEX', 'V').set({foo: 'bar'}).token(reader).one() + .then(function () { + throw new Error('No, this should not happen'); + }) + .catch(LIB.errors.RequestError, function (err) { + /permission/i.test(err.message).should.be.true; + }); + }); + ifSupportedIt('should allow the reader to read from a class', function () { + return db.select().from('OUser').token(reader).all() + .then(function (users) { + users.length.should.be.above(0); + }); + }); + ifSupportedIt('should allow the writer to create a vertex', function () { + return db.create('VERTEX', 'V').set({foo: 'bar'}).token(writer).one() + .then(function (item) { + item.foo.should.equal('bar'); + }); + }); + ifSupportedIt('should allow the writer to read from a class', function () { + return db.select().from('OUser').token(writer).all() + .then(function (users) { + users.length.should.be.above(0); + }); + }); + ifSupportedIt('should allow the admin to create a vertex', function () { + return db.create('VERTEX', 'V').set({foo: 'bar'}).token(admin).one() + .then(function (item) { + item.foo.should.equal('bar'); + }); + }); + ifSupportedIt('should allow the admin to read from a class', function () { + return db.select().from('OUser').token(admin).all() + .then(function (users) { + users.length.should.be.above(0); + }); + }); + + ifSupportedIt('should allow the default user to create a vertex', function () { + return db.create('VERTEX', 'V').set({foo: 'bar'}).one() + .then(function (item) { + item.foo.should.equal('bar'); + }); + }); + ifSupportedIt('should allow the default user to read from a class', function () { + return db.select().from('OUser').all() + .then(function (users) { + users.length.should.be.above(0); + }); + }); + + describe('Db::createUserContext()', function () { + var readerContext, adminContext; + before(function () { + if (hasProtocolSupport) { + readerContext = db.createUserContext(reader); + adminContext = db.createUserContext(admin); + } + }); + ifSupportedIt('should create a user context', function () { + return readerContext.select().from('OUser').all() + .then(function (users) { + users.length.should.be.above(1); + }); + }); + ifSupportedIt('should ensure that the token is used correctly', function () { + return readerContext.create('VERTEX', 'V').set({greeting: 'hello world'}).one() + .then(function () { + throw new Error('No, this should not happen'); + }) + .catch(LIB.errors.RequestError, function (err) { + /permission/i.test(err.message).should.be.true; + }); + }); + ifSupportedIt('should insert a row using the admin context', function () { + return adminContext.insert().into('OUser').set({name: 'foo', password: 'bar', status: 'active'}).one() + .then(function (data) { + data['@class'].should.equal('OUser'); + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/core/recordid.js b/test/core/recordid.js new file mode 100644 index 0000000..a64a76c --- /dev/null +++ b/test/core/recordid.js @@ -0,0 +1,28 @@ +describe('RecordID', function () { + var rid = LIB.RID('#1:23'); + + describe('RecordID::equals()', function () { + it('should equal an identical record', function () { + rid.equals(rid).should.be.true; + }); + it('should equal a string representation of the record', function () { + rid.equals("#1:23").should.be.true; + }); + it('should not equal a different record', function () { + rid.equals(LIB.RID("4:56")).should.be.false; + }); + it('should not equal a string representation a different record', function () { + rid.equals("4:56").should.be.false; + }); + it('should equal an identical record expressed as a POJO', function () { + rid.equals({cluster: 1, position: 23}).should.be.true; + }); + it('should not equal a different record expressed as a POJO', function () { + rid.equals({cluster: 4, position: 56}).should.be.false; + }); + it('should not equal nonsense', function () { + rid.equals(false).should.be.false; + rid.equals("blah").should.be.false; + }) + }); +}); \ No newline at end of file diff --git a/test/core/utils.js b/test/core/utils.js new file mode 100644 index 0000000..7486033 --- /dev/null +++ b/test/core/utils.js @@ -0,0 +1,51 @@ +'use strict'; + +var utils = require('../../lib/utils'); + +describe('utils.prepare', function () { + it("should prepare SQL statements", function () { + utils.prepare("select from index:foo").should.equal("select from index:foo"); + }); + it("should prepare SQL statements with parameters", function () { + utils.prepare("select from index:foo where key = :key", {key: 123}).should.equal("select from index:foo where key = 123"); + }); + it("should prepare SQL statements with date parameters", function () { + var date = new Date(Date.UTC(2015, 0, 5, 22, 7, 5, 435)); + utils.prepare("select from index:foo where date = :date", {date: date}).should.equal("select from index:foo where date = date(\"2015-01-05 22:07:05.435\", \"yyyy-MM-dd HH:mm:ss.SSS\", \"UTC\")"); + }); +}); + + +describe('utils.requiresParens()', function () { + it('should not require parentheses for string values', function () { + utils.requiresParens('"foo"').should.be.false; + }); + it('should not require parentheses for integer values', function () { + utils.requiresParens('2134').should.be.false; + }); + it('should not require parentheses for function calls', function () { + utils.requiresParens('foo("hello", "world")').should.be.false; + }); + it('should require parentheses for compound expressions with strings', function () { + utils.requiresParens('"foo" AND "bar"').should.be.true; + }); + it('should not require parentheses for pre-wrapped compound expressions with strings', function () { + utils.requiresParens('("foo" AND "bar")').should.be.false; + }); + it('should require parentheses for compound call expressions', function () { + utils.requiresParens('foo("hello", "world") AND wat').should.be.true; + utils.requiresParens('foo("hello", "world") AND bar("wat")').should.be.true; + }); + it('should not require parentheses for deeply nested function calls', function () { + utils.requiresParens("foo(1,2,3, bar(4,5,6))").should.be.false; + }); + it('should require parentheses for separately parenthesized expressions', function () { + utils.requiresParens("(foo(1,2,3) AND test) AND (a > 2)").should.be.true; + }); + it('should not require parentheses for strings with parenthesized expressions', function () { + utils.requiresParens("'foo (a b c) wat'").should.be.false; + }); + it('should ignore leading and trailing whitespace', function () { + utils.requiresParens(' ("foo" AND "bar") ').should.be.false; + }); +}); \ No newline at end of file diff --git a/test/db/class-test.js b/test/db/class-test.js index 4857d11..f482f0c 100644 --- a/test/db/class-test.js +++ b/test/db/class-test.js @@ -33,17 +33,39 @@ describe("Database API - Class", function () { return this.db.class.create('TestClass') .then(function (item) { item.name.should.equal('TestClass'); + item.should.have.property('superClass', null); + item.should.be.an.instanceOf(Class); + }); + }); + it('should create a class with the given name and a superClass', function () { + return this.db.class.create('TestClassExtended', 'V') + .then(function (item) { + item.name.should.equal('TestClassExtended'); + item.should.have.property('superClass', 'V'); item.should.be.an.instanceOf(Class); }); }); }); - describe('Db::class.delete()', function () { - it('should delete a class with the given name', function () { - return this.db.class.delete('TestClass'); + describe('Db::class.update()', function () { + it('should update a class with the given superClass', function () { + return this.db.class.update({ + name: 'TestClass', + superClass: 'V' + }) + .then(function (item) { + item.name.should.equal('TestClass'); + item.should.have.property('superClass', 'V'); + item.should.be.an.instanceOf(Class); + }); }); }); + describe('Db::class.drop()', function () { + it('should delete a class with the given name', function () { + return this.db.class.drop('TestClass'); + }); + }); describe('Instance functions', function () { before(function () { diff --git a/test/db/cluster-test.js b/test/db/cluster-test.js index 92b0f69..c80e3cd 100644 --- a/test/db/cluster-test.js +++ b/test/db/cluster-test.js @@ -11,7 +11,7 @@ describe("Database API - Cluster", function () { }); }); after(function () { - return TEST_SERVER.delete({ + return TEST_SERVER.drop({ name: 'testdb_dbapi_cluster', storage: 'memory' }); @@ -86,10 +86,10 @@ describe("Database API - Cluster", function () { }); }); - describe('Db::cluster.delete()', function () { + describe('Db::cluster.drop()', function () { it('should delete a cluster with the given name', function () { - return this.db.cluster.delete('mycluster'); + return this.db.cluster.drop('mycluster'); }); }); -}); \ No newline at end of file +}); diff --git a/test/db/db-test.js b/test/db/db-test.js index 7e07146..7a8f336 100644 --- a/test/db/db-test.js +++ b/test/db/db-test.js @@ -1,3 +1,5 @@ +var Promise = require('bluebird'); + describe("Database API", function () { before(function () { return TEST_SERVER.create({ @@ -11,7 +13,7 @@ describe("Database API", function () { }); }); after(function () { - return TEST_SERVER.delete({ + return TEST_SERVER.drop({ name: 'testdb_dbapi', storage: 'memory' }); @@ -72,4 +74,162 @@ describe("Database API", function () { }); }); }); -}); \ No newline at end of file + + describe('Db::registerTransformer()', function () { + function OUser (data) { + if (!(this instanceof OUser)) { + return new OUser(data); + } + var keys = Object.keys(data), + length = keys.length, + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + this[key] = data[key]; + } + } + + function ORole (data) { + if (!(this instanceof ORole)) { + return new ORole(data); + } + var keys = Object.keys(data), + length = keys.length, + key, i; + for (i = 0; i < length; i++) { + key = keys[i]; + this[key] = data[key]; + } + } + + + before(function () { + this.db.registerTransformer('OUser', OUser); + this.db.registerTransformer('ORole', ORole); + }); + + it('should register a transformation function for a class', function () { + return Promise.all([ + this.db.select().from('OUser').all(), + this.db.select().from('ORole').all() + ]) + .spread(function (users, roles) { + users.length.should.be.above(0); + users.forEach(function (user) { + user.should.be.an.instanceOf(OUser); + }); + roles.length.should.be.above(0); + roles.forEach(function (role) { + role.should.be.an.instanceOf(ORole); + }); + }); + }); + + it('should transform documents even when they are nested', function () { + return this.db.select().from('OUser').fetch({roles: 1}).all() + .then(function (users) { + users.length.should.be.above(0); + users.forEach(function (user) { + user.should.be.an.instanceOf(OUser); + user.roles.length.should.be.above(0); + user.roles.forEach(function (role) { + role.should.be.an.instanceOf(ORole); + }); + }); + }); + }); + + it('should still allow scalars', function () { + return this.db.select().from('OUser').limit(1).scalar() + .then(function (result) { + result.should.equal('admin'); + }); + }); + + it('should not transform when individual columns are selected', function () { + return this.db.select('name, status').from('OUser').limit(1).one() + .then(function (result) { + result.should.not.be.an.instanceOf(OUser); + }); + }); + }); + + describe('Db::on()', function () { + it('should emit a beginQuery event', function () { + var emitedObject; + this.db.on("beginQuery", function(obj) { + emitedObject = obj; + }); + + return this.db.select('name, status').from('OUser').limit(1).one() + .then(function () { + emitedObject.should.have.property("query"); + emitedObject.should.have.property("mode"); + emitedObject.should.have.property("fetchPlan"); + emitedObject.should.have.property("limit"); + emitedObject.should.have.property("params"); + emitedObject.query.should.equal("SELECT name, status FROM OUser LIMIT 1"); + }); + }); + + it('should emit a endQuery event with success', function () { + var emitedObject; + this.db.on("endQuery", function(obj) { + emitedObject = obj; + }); + + return this.db.select('name, status').from('OUser').limit(1).one() + .delay(10) // solves a strange race condition which happens about 1/20th of the time, needs further investigation. + .then(function () { + emitedObject.should.have.propertyByPath("perf", "query"); + emitedObject.should.have.property("err"); + emitedObject.should.have.property("result"); + emitedObject.perf.query.should.be.above(0); + (isNaN(emitedObject.err)).should.be.true; + emitedObject.result.should.be.ok; + }); + }); + + it('should emit a endQuery event with error', function () { + var emitedObject; + this.db.on("endQuery", function(obj) { + emitedObject = obj; + }); + + return this.db.select('name, status').from('Invalid').limit(1).one() + .catch(function (err) { + emitedObject.should.have.propertyByPath("perf", "query"); + emitedObject.should.have.property("err"); + emitedObject.should.have.property("result"); + emitedObject.perf.query.should.be.above(0); + emitedObject.err.should.be.ok; + (isNaN(emitedObject.result)).should.be.true; + }); + }); + + it('should create a runnable function with name arg as function name', function () { + var db = this.db; + + return db.createFn("runme1", function(str) { + return "this "+str+" work"; + }).then(function() { + return db.select('runme1("does") as testresult').from('OUser').limit(1).one(); + }).then(function(res) { + res.testresult.should.be.equal("this does work"); + }); + }); + + it('should create runnable function with function name as name', function () { + var db = this.db; + + return db.createFn(function runme2(str) { + return "this "+str+" work"; + }).then(function() { + return db.select('runme2("does") as testresult').from('OUser').limit(1).one(); + }).then(function(res) { + res.testresult.should.be.equal("this does work"); + }); + }); + + }); +}); diff --git a/test/db/index-test.js b/test/db/index-test.js index 8a37361..b85aa96 100644 --- a/test/db/index-test.js +++ b/test/db/index-test.js @@ -54,9 +54,9 @@ describe("Database API - Index", function () { }); }); - describe('Db::index.delete()', function () { + describe('Db::index.drop()', function () { it('should delete an index', function () { - return this.db.index.delete('TestClass.name'); + return this.db.index.drop('TestClass.name'); }); }); @@ -159,8 +159,8 @@ describe("Database API - Index", function () { describe('Db::index::delete()', function () { - it('should delete a rid', function () { - return this.index.delete(this.items[4]['@rid']); + it('should delete a key', function () { + return this.index.delete(this.items[4].name); }); }); }); diff --git a/test/db/property-test.js b/test/db/property-test.js index a342d19..1213828 100644 --- a/test/db/property-test.js +++ b/test/db/property-test.js @@ -40,7 +40,7 @@ describe("Database API - Class - Property", function () { }) .then(function (item) { item.name.should.equal('customprop'); - item.max.should.eql(20); + item.max.should.eql('20'); }); }); it('should create an array of properties', function () { @@ -91,7 +91,7 @@ describe("Database API - Class - Property", function () { }) .then(function (item) { item.name.should.equal('myprop2'); - item.max.should.eql(20); + item.max.should.eql('20'); }); }); }); @@ -103,11 +103,11 @@ describe("Database API - Class - Property", function () { }); - describe('Db::class.property.delete()', function () { - it('should delete a property with the given name', function () { - return this.class.property.delete('myprop'); + describe('Db::class.property.drop()', function () { + it('should drop a property with the given name', function () { + return this.class.property.drop('myprop'); }); }); -}); \ No newline at end of file +}); diff --git a/test/db/query-test.js b/test/db/query-test.js index a777b10..291c87f 100644 --- a/test/db/query-test.js +++ b/test/db/query-test.js @@ -261,6 +261,13 @@ describe("Database API - Query", function () { user.name.should.equal('reader'); }); }); + it('should select a record by its RID', function () { + return this.db.select().from('OUser').where({'@rid': new LIB.RID('#5:0')}).one() + .then(function (user) { + expect(typeof user).to.equal('object'); + user.name.should.equal('admin'); + }); + }); it('should select a user with a fetch plan', function () { return this.db.select().from('OUser').where({name: 'reader'}).fetch({roles: 3}).one() .then(function (user) { @@ -300,8 +307,65 @@ describe("Database API - Query", function () { it('should update a user', function () { return this.db.update('OUser').set({foo: 'bar'}).where({name: 'reader'}).limit(1).scalar() .then(function (count) { - count.should.eql(1); + count.should.eql('1'); + }); + }); + }); + describe('Db::query()', function() { + it('should execute an insert query', function () { + return this.db.query('insert into OUser (name, password, status) values (:name, :password, :status)', + { + params: { + name: 'Samson', + password: 'mypassword', + status: 'active' + } + } + ).then(function (response){ + response[0].name.should.equal('Samson'); + }); + }); + it('should exec a raw select command', function () { + return this.db.exec('select from OUser where name=:name', { + params: { + name: 'Samson' + } + }) + .then(function (result){ + Array.isArray(result.results[0].content).should.be.true; + result.results[0].content.length.should.be.above(0); + }); + }); + it('should execute a script command', function () { + return this.db.exec('123456;', { + language: 'javascript', + class: 's' + }) + .then(function (response) { + response.results.length.should.equal(1); + }); + }); + it('should execute a select query string', function () { + return this.db.query('select from OUser where name=:name', { + params: { + name: 'Samson' + }, + limit: 1 + }) + .then(function (result){ + Array.isArray(result).should.be.true; + result.length.should.be.above(0); + (result[0]['@class']).should.eql('OUser'); + }); + }); + it('should execute a delete query', function () { + return this.db.query('delete from OUser where name=:name', { + params: { + name: 'Samson' + } + }).then(function (response){ + response[0].should.eql('1'); }); }); }); -}); \ No newline at end of file +}); diff --git a/test/db/statement-test.js b/test/db/statement-test.js index 17d8b8b..9c20088 100644 --- a/test/db/statement-test.js +++ b/test/db/statement-test.js @@ -12,6 +12,142 @@ describe("Database API - Statement", function () { this.statement = new Statement(this.db); }); + describe('Statement::let()', function () { + + it('should let a variable in a select() query', function () { + this.statement + .select('$thing') + .from('OUser') + .let('thing', '$current.thing') + .where('1=1') + .buildStatement() + .should + .equal('SELECT $thing FROM OUser LET thing = $current.thing WHERE 1=1'); + }); + + it('should let a variable equal a subexpression', function () { + var sub = (new Statement(this.db)).select('name').from('OUser').where({status: 'ACTIVE'}); + this.statement + .let('names', sub) + .buildStatement() + .should + .equal('LET names = (SELECT name FROM OUser WHERE status = "ACTIVE")'); + }); + it('should let a variable equal a subexpression, more than once', function () { + var sub1 = (new Statement(this.db)).select('name').from('OUser').where({status: 'ACTIVE'}), + sub2 = (new Statement(this.db)).select('status').from('OUser'); + this.statement + .let('names', sub1) + .let('statuses', sub2) + .buildStatement() + .should + .equal('LET names = (SELECT name FROM OUser WHERE status = "ACTIVE"),statuses = (SELECT status FROM OUser)'); + }); + it('should let a variable equal a subexpression, more than once, using locks', function () { + var sub1 = (new Statement(this.db)).select('name').from('OUser').where({status: 'ACTIVE'}), + sub2 = (new Statement(this.db)).select('status').from('OUser').lock('record'); + this.statement + .let('names', sub1) + .let('statuses', sub2) + .buildStatement() + .should + .equal('LET names = (SELECT name FROM OUser WHERE status = "ACTIVE"),statuses = (SELECT status FROM OUser LOCK record)'); + }); + + it('should allow RIDs in LET expressions', function () { + var rec1 = { + '@rid': new LIB.RID({ + cluster: 23, + position: 1234567 + }) + }; + var rec2 = { + '@rid': new LIB.RID({ + cluster: 23, + position: 98765432 + }) + }; + this.statement + .let('foo', function (statement) { + return statement.select().from('Foo'); + }) + .let('edge', function (statement) { + return statement + .create('edge', 'E') + .from('$foo') + .to(rec1['@rid']); + }) + .let('updated', function (statement) { + return statement.update(rec2['@rid']).set({foo: 'bar'}); + }) + .commit() + .return('$edge') + .buildStatement() + .should.equal('BEGIN\n\ + LET foo = SELECT * FROM Foo\n\ + LET edge = CREATE edge E FROM $foo TO #23:1234567\n\ + LET updated = UPDATE #23:98765432 SET foo = "bar"\n\ + \n\ +COMMIT \n\ + RETURN $edge'); + }); + }); + + describe('Statement::commit() and Statement::return()', function () { + it('should generate an empty transaction', function () { + this.statement + .commit() + .buildStatement() + .should + .equal('BEGIN\n \nCOMMIT \n'); + }); + it('should generate an empty transaction, with retries', function () { + this.statement + .commit(100) + .buildStatement() + .should + .equal('BEGIN\n \nCOMMIT RETRY 100 \n'); + }); + it('should generate an update transaction', function () { + this.statement + .update('OUser') + .set({name: 'name'}) + .commit() + .toString() + .should + .equal('BEGIN\n UPDATE OUser SET name = "name" \nCOMMIT \n'); + }); + it('should generate an update transaction, with retries', function () { + this.statement + .update('OUser') + .set({name: 'name'}) + .commit(100) + .toString() + .should + .equal('BEGIN\n UPDATE OUser SET name = "name" \nCOMMIT RETRY 100 \n'); + }); + it('should generate an update transaction, with returns', function () { + var sub = (new Statement(this.db)).update('OUser').set({name: 'name'}); + this.statement + .let('names', sub) + .commit() + .return('$names') + .toString() + .should + .equal('BEGIN\n LET names = UPDATE OUser SET name = "name"\n \nCOMMIT \n RETURN $names'); + }); + it('should generate an update transaction, with returns and a return clause before while', function () { + var sub = (new Statement(this.db)).update('OUser').set({name: 'name'}).return('COUNT'); + this.statement + .let('names', sub) + .commit() + .return('$names') + .toString() + .should + .equal('BEGIN\n LET names = UPDATE OUser SET name = "name" RETURN COUNT\n \nCOMMIT \n RETURN $names'); + }); + }); + describe('Statement::select()', function () { it('should select all the columns by default', function () { this.statement.select(); @@ -27,6 +163,55 @@ describe("Database API - Statement", function () { }); }); + describe('Statement::traverse()', function () { + it('should traverse all the edges by default', function () { + this.statement.traverse(); + this.statement.buildStatement().should.equal('TRAVERSE *'); + }); + + it('should traverse a single edge type', function () { + this.statement.traverse('out("Thing")'); + this.statement.buildStatement().should.equal('TRAVERSE out("Thing")'); + }); + + it('should traverse multiple edge types', function () { + this.statement.traverse('in("Thing")', 'out("Thing")'); + this.statement.buildStatement().should.equal('TRAVERSE in("Thing"), out("Thing")'); + }); + + it('should traverse in depth first', function () { + this.statement.traverse().strategy('DEPTH_FIRST').from('Abc'); + this.statement.buildStatement().should.equal('TRAVERSE * FROM Abc STRATEGY DEPTH_FIRST'); + }); + + it('should traverse in breadth first', function () { + this.statement.traverse().strategy('BREADTH_FIRST').from('#23:4'); + this.statement.buildStatement().should.equal('TRAVERSE * FROM #23:4 STRATEGY BREADTH_FIRST'); + }); + + it('should traverse with no strategy spec', function () { + this.statement.traverse().strategy('XYZ'); + this.statement.buildStatement().should.equal('TRAVERSE *'); + }); + + it('should traverse in breadth first and with limit', function () { + this.statement.traverse().strategy('BREADTH_FIRST').limit(2).from('Xyz'); + this.statement.buildStatement().should.equal('TRAVERSE * FROM Xyz LIMIT 2 STRATEGY BREADTH_FIRST'); + }); + }); + + describe('Statement::while()', function () { + it('should add a while clause to traverses', function () { + this.statement.traverse().from('OUser').while('$depth < 1'); + this.statement.buildStatement().should.equal("TRAVERSE * FROM OUser WHILE $depth < 1"); + }); + + it('should add multiple while clauses to traverses', function () { + this.statement.traverse().from('OUser').while('$depth < 1').and('1=1'); + this.statement.buildStatement().should.equal("TRAVERSE * FROM OUser WHILE $depth < 1 AND 1=1"); + }); + }); + describe('Statement::insert()', function () { it('should insert a record', function () { this.statement.insert().into('OUser').set({foo: 'bar', greeting: 'hello world'}); @@ -39,6 +224,27 @@ describe("Database API - Statement", function () { this.statement.update('#1:1').set({foo: 'bar', greeting: 'hello world'}); this.statement.buildStatement().should.equal('UPDATE #1:1 SET foo = :paramfoo0, greeting = :paramgreeting1'); }); + + it('should update a record with a nested statement', function () { + this.statement.update('#1:1').set({ + foo: function (s) { + s.select().from('OUser'); + } + }); + this.statement.buildStatement().should.equal('UPDATE #1:1 SET foo = (SELECT * FROM OUser)'); + }); + }); + + describe('Statement::delete()', function () { + it('should delete a record', function () { + this.statement.delete().from('OUser').where({foo: 'bar', greeting: 'hello world'}); + this.statement.buildStatement().should.equal('DELETE FROM OUser WHERE (foo = :paramfoo0 AND greeting = :paramgreeting1)'); + }); + + it('should delete an edge', function () { + this.statement.delete('EDGE', 'foo').from(LIB.RID('#1:23')).to(LIB.RID('#4:56')); + this.statement.buildStatement().should.equal('DELETE EDGE foo FROM #1:23 TO #4:56'); + }); }); describe('Statement::from()', function () { @@ -50,10 +256,101 @@ describe("Database API - Statement", function () { this.statement.select().from(new LIB.RID('#4:4')); this.statement.buildStatement().should.equal('SELECT * FROM #4:4'); }); - it('should select from a subexpression', function () { + it('should select from a subexpression with parentheses', function () { + this.statement.select().from('(SELECT * FROM OUser)'); + this.statement.buildStatement().should.equal('SELECT * FROM (SELECT * FROM OUser)'); + }); + it('should select from a subexpression without parentheses', function () { this.statement.select().from('SELECT * FROM OUser'); this.statement.buildStatement().should.equal('SELECT * FROM (SELECT * FROM OUser)'); }); + it('should select from a subquery', function () { + this.statement.select().from((new Statement(this.db).select().from('OUser'))); + this.statement.buildStatement().should.equal('SELECT * FROM (SELECT * FROM OUser)'); + }); + it('should select from a subquery, using a function', function () { + this.statement.select().from(function (s) { + s.select().from('OUser'); + }); + this.statement.buildStatement().should.equal('SELECT * FROM (SELECT * FROM OUser)'); + }); + }); + + describe('Statement::to()', function () { + it('should create an edge', function () { + this.statement.create('EDGE', 'E').from('#5:0').to('#5:1'); + this.statement.buildStatement().should.equal('CREATE EDGE E FROM #5:0 TO #5:1'); + }); + it('should create an edge from a record id to a record id', function () { + this.statement.create('EDGE', 'E').from(LIB.RID('#5:0')).to(LIB.RID('#22:310540')); + this.statement.buildStatement().should.equal('CREATE EDGE E FROM #5:0 TO #22:310540'); + }); + it('should create an edge using a subexpression with parentheses', function () { + this.statement.create('EDGE', 'E').to('(SELECT * FROM OUser)').from(LIB.RID('#1:23')); + this.statement.buildStatement().should.equal('CREATE EDGE E FROM #1:23 TO (SELECT * FROM OUser)'); + }); + it('should create an edge using a subexpression without parentheses', function () { + this.statement.create('EDGE', 'E').to('SELECT * FROM OUser').from(LIB.RID('#1:23')); + this.statement.buildStatement().should.equal('CREATE EDGE E FROM #1:23 TO (SELECT * FROM OUser)'); + }); + it('should create an edge using a subquery', function () { + this.statement.create('EDGE', 'E').to((new Statement(this.db).select().from('OUser'))).from(LIB.RID('#1:23')); + this.statement.buildStatement().should.equal('CREATE EDGE E FROM #1:23 TO (SELECT * FROM OUser)'); + }); + }); + + describe('Statement::retry()', function () { + it('should create an edge with retry', function () { + this.statement.create('EDGE', 'E').from('#5:0').to('#5:1').retry(5); + this.statement.buildStatement().should.equal('CREATE EDGE E FROM #5:0 TO #5:1 RETRY 5'); + }); + }); + + describe('Statement::wait()', function () { + it('should create an edge with retry and wait', function () { + this.statement.create('EDGE', 'E').from('#5:0').to('#5:1').retry(5).wait(100); + this.statement.buildStatement().should.equal('CREATE EDGE E FROM #5:0 TO #5:1 RETRY 5 WAIT 100'); + }); + }); + + describe('Statement::return()', function () { + it('should build a return clause', function () { + this.statement.update('#1:1').set({foo: 'bar', greeting: 'hello world'}).return('AFTER'); + this.statement.buildStatement().should.equal('UPDATE #1:1 SET foo = :paramfoo0, greeting = :paramgreeting1 RETURN AFTER'); + }); + it('should build a return clause with object parameters', function () { + this.statement.update('#1:1').set({foo: 'bar', greeting: 'hello world'}).return({rid: '@rid'}); + this.statement.buildStatement().should.equal('UPDATE #1:1 SET foo = :paramfoo0, greeting = :paramgreeting1 RETURN {"rid":@rid}'); + }); + it('should build a return clause with array parameters', function () { + this.statement.update('#1:1').set({foo: 'bar', greeting: 'hello world'}).return(['@rid', '@class']); + this.statement.buildStatement().should.equal('UPDATE #1:1 SET foo = :paramfoo0, greeting = :paramgreeting1 RETURN [@rid,@class]'); + }); + it('should build a return clause before the where clause', function () { + this.statement.delete().from('OUser').return('BEFORE').where({foo: 'bar', greeting: 'hello world'}); + this.statement.buildStatement().should.equal('DELETE FROM OUser RETURN BEFORE WHERE (foo = :paramfoo0 AND greeting = :paramgreeting1)'); + }); + it('should build a return clause after the insert query', function () { + this.statement.insert().into('OUser').set({foo: 'bar', greeting: 'hello world'}).return('AFTER'); + this.statement.buildStatement().should.equal('INSERT INTO OUser SET foo = :paramfoo0, greeting = :paramgreeting1 RETURN AFTER'); + }); + }); + + describe('Statement::skip() and Statement::limit()', function () { + it('should build a statement with a skip clause', function () { + this.statement.select().from('OUser').skip(2); + this.statement.buildStatement().should.equal('SELECT * FROM OUser SKIP 2'); + }); + + it('should build a statement with a limit clause', function () { + this.statement.select().from('OUser').limit(2); + this.statement.buildStatement().should.equal('SELECT * FROM OUser LIMIT 2'); + }); + + it('should build a statement with skip and limit clauses', function () { + this.statement.select().from('OUser').skip(1).limit(2); + this.statement.buildStatement().should.equal('SELECT * FROM OUser LIMIT 2 SKIP 1'); + }); }); describe('Statement::where(), Statement::and(), Statement::or()', function () { @@ -68,6 +365,13 @@ describe("Database API - Statement", function () { }); this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE (name = :paramname0 AND foo = :paramfoo1)'); }); + it('should generate IS NULL in a where clause', function () { + this.statement.select().from('OUser').where({ + name: 'root', + foo: null + }); + this.statement.toString().should.equal('SELECT * FROM OUser WHERE (name = "root" AND foo IS NULL)'); + }); it('should build a chained where clause', function () { this.statement.select().from('OUser').where('1=1').where('2=2'); this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE 1=1 AND 2=2'); @@ -89,4 +393,160 @@ describe("Database API - Statement", function () { this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE ((1=1) OR 2=2 OR 3=3 OR 4=4) AND 5=5'); }); }); + + describe('Statement::containsText()', function () { + it('should build a where clause with a map of values', function () { + this.statement.select().from('OUser').containsText({ + name: 'root', + foo: 'bar' + }); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE (name CONTAINSTEXT :paramname0 AND foo CONTAINSTEXT :paramfoo1)'); + }); + }); + + describe('Statement::lock()', function () { + it('should lock a record', function () { + this.statement.update('OUser').lock('record'); + this.statement.buildStatement().should.equal('UPDATE OUser LOCK record'); + }); + it('should lock a record with an expression', function () { + this.statement.update('OUser').where('1=1').lock('record'); + this.statement.buildStatement().should.equal('UPDATE OUser WHERE 1=1 LOCK record'); + }); + }); + + describe('Statement::upsert()', function () { + it('should upsert a record', function () { + this.statement.update('OUser').set("foo = 'bar'").upsert().where('1 = 1'); + this.statement.buildStatement().should.equal("UPDATE OUser SET foo = 'bar' UPSERT WHERE 1 = 1"); + }); + it('should upsert a record, with a where clause', function () { + this.statement.update('OUser').set("foo = 'bar'").upsert('1 = 1'); + this.statement.buildStatement().should.equal("UPDATE OUser SET foo = 'bar' UPSERT WHERE 1 = 1"); + }); + }); + + + describe('Statement::lucene()', function () { + it('should accept a string query', function () { + this.statement.select().from('OUser').lucene('name', '(name:"admin")'); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE name LUCENE "(name:\\"admin\\")"'); + }); + + it('should accept a naked string query', function () { + this.statement.select().from('OUser').lucene('name', 'admin'); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE name LUCENE "admin"'); + }); + + it('should accept a query object', function () { + this.statement.select().from('OUser').lucene({ + name: 'admin', + status: 'ACTIVE' + }); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE name LUCENE "admin" AND status LUCENE "ACTIVE"'); + }); + + it('should accept multiple parameters', function () { + this.statement.select().from('OUser').lucene('name', 'status', '(name:"admin" AND status:"ACTIVE")'); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE [name,status] LUCENE "(name:\\"admin\\" AND status:\\"ACTIVE\\")"'); + }); + }); + + + describe('Statement::near()', function () { + it('should accept plain values', function () { + this.statement.select().from('OUser').near('latitude', 'longitude', 1, 2); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE [latitude,longitude] NEAR [1,2]'); + }); + it('should accept plain values with a max distance', function () { + this.statement.select().from('OUser').near('latitude', 'longitude', 1, 2, 100); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE [latitude,longitude,$spatial] NEAR [1,2,{"maxDistance":100}]'); + }); + it('should accept an object of values', function () { + this.statement.select().from('OUser').near({latitude: 1, longitude: 2}); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE [latitude,longitude] NEAR [1,2]'); + }); + it('should accept an object of values, with a max distance', function () { + this.statement.select().from('OUser').near({latitude: 1, longitude: 2}, 100); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE [latitude,longitude,$spatial] NEAR [1,2,{"maxDistance":100}]'); + }); + }); + + describe('Statement::within()', function () { + it('should build a within query', function () { + this.statement.select().from('OUser').within('latitude', 'longitude', [[1, 2], [3, 4]]); + this.statement.buildStatement().should.equal('SELECT * FROM OUser WHERE [latitude,longitude] WITHIN [[1,2],[3,4]]'); + }); + }); + + describe('Statement::increment()', function () { + it('should increment a field using the default value', function () { + this.statement.update('#1:1').increment('foo'); + this.statement.toString().should.equal('UPDATE #1:1 INCREMENT foo = 1'); + }); + + it('should increment a field using the specified positive value', function () { + this.statement.update('#1:1').increment('foo', 100); + this.statement.toString().should.equal('UPDATE #1:1 INCREMENT foo = 100'); + }); + + + it('should increment a field using the specified negative value', function () { + this.statement.update('#1:1').increment('foo', -100); + this.statement.toString().should.equal('UPDATE #1:1 INCREMENT foo = -100'); + }); + }); + + describe('Statement::add()', function () { + it('should add a string value to a property', function () { + this.statement.update('#1:1').add('foo', 'bar'); + this.statement.toString().should.equal('UPDATE #1:1 ADD foo = "bar"'); + }); + + it('should add a numerical value to a property', function () { + this.statement.update('#1:1').add('foo', 123); + this.statement.toString().should.equal('UPDATE #1:1 ADD foo = 123'); + }); + + it('should add multiple values to a property', function () { + this.statement.update('#1:1').add('foo', 123, 'bar'); + this.statement.toString().should.equal('UPDATE #1:1 ADD foo = 123, foo = "bar"'); + }); + }); + + describe('Statement::remove()', function () { + it('should remove a string value from a property', function () { + this.statement.update('#1:1').remove('foo', 'bar'); + this.statement.toString().should.equal('UPDATE #1:1 REMOVE foo = "bar"'); + }); + + it('should remove a numerical value from a property', function () { + this.statement.update('#1:1').remove('foo', 123); + this.statement.toString().should.equal('UPDATE #1:1 REMOVE foo = 123'); + }); + + it('should remove multiple values from a property', function () { + this.statement.update('#1:1').remove('foo', 123, 'bar'); + this.statement.toString().should.equal('UPDATE #1:1 REMOVE foo = 123, foo = "bar"'); + }); + }); + + describe('Statement::put()', function () { + it('should build a put query', function () { + this.statement.update('#1:1') + .put('fooMap', { + foo: 'fooVal', + greeting: 'hello world' + }) + .put('barMap', { + bar: 'barVal', + name: 'mario' + }); + this.statement.buildStatement().should.equal('UPDATE #1:1 PUT ' + + 'fooMap = "foo", :paramfooMapfoo0, ' + + 'fooMap = "greeting", :paramfooMapgreeting1, ' + + 'barMap = "bar", :parambarMapbar2, ' + + 'barMap = "name", :parambarMapname3'); + }); + }); }); \ No newline at end of file diff --git a/test/db/transaction-test.js b/test/db/transaction-test.js new file mode 100644 index 0000000..22c59cd --- /dev/null +++ b/test/db/transaction-test.js @@ -0,0 +1,285 @@ +var Transaction = require('../../lib/db/transaction'), + Promise = require('bluebird'); + +describe("Database API - Transaction", function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_dbapi_tx') + .bind(this) + .then(function () { + return this.db.class.create('TestClass', 'V'); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_dbapi_tx'); + }); + describe("Db::begin()", function () { + it('should return a new transaction instance', function () { + var tx = this.db.begin(); + tx.should.be.an.instanceOf(Transaction); + tx.id.should.be.above(0); + }); + }); + + describe('Db::commit()', function () { + before(function () { + return this.db.record.create([ + { + '@class': 'TestClass', + name: 'item1' + }, + { + '@class': 'TestClass', + name: 'item2' + } + ]) + .bind(this) + .spread(function (first, second) { + this.first = first; + this.second = second; + }); + }); + it('should perform a single action', function () { + this.tx = this.db.begin(); + this.tx.create({ + '@class': 'TestClass', + name: 'item3' + }); + return this.tx.commit() + .then(function (results) { + results.created.length.should.equal(1); + results.updated.length.should.equal(0); + results.deleted.length.should.equal(0); + }); + }); + it('should perform multiple actions', function () { + this.tx = this.db.begin(); + this.first.wat = 'wat'; + this.tx + .create({ + '@class': 'TestClass', + name: 'item4' + }) + .delete(this.second) + .update(this.first); + + return this.tx.commit() + .then(function (results) { + results.created.length.should.equal(1); + results.updated.length.should.equal(1); + results.deleted.length.should.equal(1); + }); + }); + }); + + describe("Db::transaction.create()", function () { + it('should create a single record', function () { + this.tx = this.db.begin(); + return this.tx + .create({ + '@class': 'TestClass', + name: 'item1' + }) + .commit() + .bind(this) + .then(function (results) { + results.created.length.should.equal(1); + results.updated.length.should.equal(0); + results.deleted.length.should.equal(0); + }); + }); + + it('should create multiple records', function () { + this.tx = this.db.begin(); + return this.tx + .create({ + '@class': 'TestClass', + name: 'item1' + }) + .create({ + '@class': 'TestClass', + name: 'item2' + }) + .create({ + '@class': 'TestClass', + name: 'item3' + }) + .commit() + .then(function (results) { + results.created.length.should.equal(3); + results.updated.length.should.equal(0); + results.deleted.length.should.equal(0); + }); + }); + }); + describe("Db::transaction.update()", function () { + beforeEach(function () { + this.tx = this.db.begin(); + return this.db.record.create([ + { + '@class': 'TestClass', + name: 'updateMe1' + }, + { + '@class': 'TestClass', + name: 'updateMe2' + } + ]) + .bind(this) + .spread(function (first, second) { + this.first = first; + this.second = second; + }); + }); + it('should update a single record', function () { + this.first.foo = 'foo'; + return this.tx + .update(this.first) + .commit() + .then(function (results) { + results.created.length.should.equal(0); + results.updated.length.should.equal(1); + results.deleted.length.should.equal(0); + }); + }); + it('should update multiple records', function () { + this.first.foo = 'foo'; + this.second.baz = 'baz'; + return this.tx + .update(this.first) + .update(this.second) + .commit() + .then(function (results) { + results.created.length.should.equal(0); + results.updated.length.should.equal(2); + results.deleted.length.should.equal(0); + }); + }); + }); + describe("Db::transaction.delete()", function () { + beforeEach(function () { + this.tx = this.db.begin(); + return this.db.record.create([ + { + '@class': 'TestClass', + name: 'deleteMe1' + }, + { + '@class': 'TestClass', + name: 'deleteMe2' + } + ]) + .bind(this) + .spread(function (first, second) { + this.first = first; + this.second = second; + }); + }); + it('should delete a single record', function () { + return this.tx + .delete(this.first) + .commit() + .then(function (results) { + results.created.length.should.equal(0); + results.updated.length.should.equal(0); + results.deleted.length.should.equal(1); + }); + }); + it('should delete multiple records', function () { + return this.tx + .delete(this.first) + .delete(this.second) + .commit() + .then(function (results) { + results.created.length.should.equal(0); + results.updated.length.should.equal(0); + results.deleted.length.should.equal(2); + }); + }); + }); +}); + +describe('Transactional Queries', function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_dbapi_tx_queries') + .bind(this) + .then(function () { + return Promise.all([ + this.db.class.create('TestVertex', 'V'), + this.db.class.create('TestEdge', 'E') + ]); + }); + }); + after(function () { + return DELETE_TEST_DB('testdb_dbapi_tx_queries'); + }); + + it('should execute a simple transaction, using a raw query', function () { + return this.db.query('begin\nupdate OUser set someField = true\ncommit', { + class: 's' + }) + .spread(function (result) { + result.should.be.above(2); + }); + }); + it('should execute a simple transaction, using the query builder', function () { + return this.db + .update('OUser') + .set({newField: true}) + .commit() + .all() + .spread(function (result) { + result.should.be.above(2); + }); + }); + + it('should execute a complex transaction, using a raw query', function () { + return this.db.query('begin\nlet vert = create vertex TestVertex set name = "thing"\nlet user = select from OUser where name = "admin"\nlet edge = create edge TestEdge from $vert to $user\ncommit retry 100\nreturn $edge', { + class: 's' + }) + .spread(function (result) { + result['@class'].should.equal('TestEdge'); + }); + }); + it('should execute a complex transaction, using the query builder', function () { + return this.db + .let('vert', 'create vertex TestVertex set name="wat"') + .let('user', 'select from OUser where name="reader"') + .let('edge', 'create edge TestEdge from $vert to $user') + .commit(100) + .return('$edge') + .one() + .then(function (result) { + result['@class'].should.equal('TestEdge'); + }); + }); + it('should execute a complex transaction, using the query builder for let statements', function () { + return this.db + .let('vert', function (s) { + return s + .create('vertex', 'TestVertex') + .set({ + name: "foo" + }); + }) + .let('user', function (s) { + return s + .select() + .from('OUser') + .where({ + name: 'reader' + }); + }) + .let('edge', function (s) { + return s + .create('edge', 'TestEdge') + .from('$vert') + .to('$user') + }) + .commit(100) + .return('$edge') + .one() + .then(function (result) { + result['@class'].should.equal('TestEdge'); + }); + }); +}); \ No newline at end of file diff --git a/test/db/vertex-test.js b/test/db/vertex-test.js index a1e2e36..1a2a46b 100644 --- a/test/db/vertex-test.js +++ b/test/db/vertex-test.js @@ -1,6 +1,7 @@ var Class = require('../../lib/db/class'); describe("Database API - Vertex", function () { + var created1, created2; before(function () { return CREATE_TEST_DB(this, 'testdb_dbapi_vertex'); }); @@ -14,7 +15,7 @@ describe("Database API - Vertex", function () { .bind(this) .then(function (vertex) { vertex['@rid'].should.be.an.instanceOf(LIB.RID); - this.created1 = vertex; + created1 = vertex; }); }); it('should create a vertex with some attributes', function () { @@ -28,20 +29,20 @@ describe("Database API - Vertex", function () { vertex['@rid'].should.be.an.instanceOf(LIB.RID); vertex.key1.should.equal('val1'); vertex.key2.should.equal('val2'); - this.created2 = vertex; + created2 = vertex; }); }); }); describe("Db::vertex.delete()", function () { it('should delete a vertex', function () { - return this.db.vertex.delete(this.created1) + return this.db.vertex.delete(created1) .bind(this) .then(function (count) { count.should.equal(1); }); }); it('should delete a vertex with properties', function () { - return this.db.vertex.delete(this.created2) + return this.db.vertex.delete(created2) .bind(this) .then(function (count) { count.should.equal(1); diff --git a/test/fixtures/oriento.opts b/test/fixtures/oriento.opts index f6dfd83..f0b16d7 100644 --- a/test/fixtures/oriento.opts +++ b/test/fixtures/oriento.opts @@ -1,3 +1,3 @@ ---server=localhost +--host=localhost --port=2424 ---password=3BA5DB89CC6206DBF835B36B70FF8A0EDCEFA617A229F0D44D1D726ABA04216A +--password=root diff --git a/test/index.js b/test/index.js index 7e5c741..5cc2d67 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,8 @@ // test bootstrap -var Promise = require('bluebird'); +var Promise = require('bluebird'), + path = require('path'); + Promise.longStackTraces(); global.expect = require('expect.js'), @@ -9,21 +11,46 @@ global.should = require('should'); global.TEST_SERVER_CONFIG = require('./test-server.json'); -global.LIB = require('../lib'); +global.LIB_ROOT = path.resolve(__dirname, '..', 'lib'); + +global.LIB = require(LIB_ROOT); + -global.TEST_SERVER = new LIB.Server(TEST_SERVER_CONFIG); +global.TEST_SERVER = new LIB.Server({ + host: TEST_SERVER_CONFIG.host, + port: TEST_SERVER_CONFIG.port, + username: TEST_SERVER_CONFIG.username, + password: TEST_SERVER_CONFIG.password, + transport: 'binary' +}); -// Uncomment the following line to enable debug logging +global.REST_SERVER = new LIB.Server({ + host: TEST_SERVER_CONFIG.host, + port: TEST_SERVER_CONFIG.httpPort, + username: TEST_SERVER_CONFIG.username, + password: TEST_SERVER_CONFIG.password, + transport: 'rest' +}); + +// Uncomment the following lines to enable debug logging // global.TEST_SERVER.logger.debug = console.log.bind(console, '[ORIENTDB]'); +// global.REST_SERVER.logger.debug = console.log.bind(console, '[ORIENTDB]'); + + +global.CREATE_TEST_DB = createTestDb.bind(null, TEST_SERVER); +global.DELETE_TEST_DB = deleteTestDb.bind(null, TEST_SERVER); +global.CREATE_REST_DB = createTestDb.bind(null, REST_SERVER); +global.DELETE_REST_DB = deleteTestDb.bind(null, REST_SERVER); -global.CREATE_TEST_DB = function (context, name) { - return TEST_SERVER.exists(name, 'memory') +function createTestDb(server, context, name, type) { + type = type || 'memory'; + return server.exists(name, type) .then(function (exists) { if (exists) { - return TEST_SERVER.delete({ + return server.drop({ name: name, - storage: 'memory' + storage: type }); } else { @@ -31,25 +58,26 @@ global.CREATE_TEST_DB = function (context, name) { } }) .then(function () { - return TEST_SERVER.create({ + return server.create({ name: name, type: 'graph', - storage: 'memory' + storage: type }); }) .then(function (db) { context.db = db; return undefined; }); -}; +} -global.DELETE_TEST_DB = function (name) { - return TEST_SERVER.exists(name, 'memory') +function deleteTestDb (server, name, type) { + type = type || 'memory'; + return server.exists(name, type) .then(function (exists) { if (exists) { - return TEST_SERVER.delete({ + return server.drop({ name: name, - storage: 'memory' + storage: type }); } else { @@ -59,4 +87,4 @@ global.DELETE_TEST_DB = function (name) { .then(function () { return undefined; }); -}; \ No newline at end of file +} diff --git a/test/migration/manager-test.js b/test/migration/manager-test.js index 80c737e..82058b2 100644 --- a/test/migration/manager-test.js +++ b/test/migration/manager-test.js @@ -66,6 +66,10 @@ describe("Migration Manager", function () { return this.manager.list() .then(function (migrations) { migrations.length.should.equal(2); + migrations.should.eql([ + 'm20140318_014253_my_test_migration', + 'm20140318_014300_my_second_test_migration' + ]); }); }) }); diff --git a/test/server/db-test.js b/test/server/db-test.js new file mode 100644 index 0000000..4312cac --- /dev/null +++ b/test/server/db-test.js @@ -0,0 +1,55 @@ +describe("Database commands", function () { + this.timeout(30000); + before(function () { + return CREATE_TEST_DB(this, 'test_db_commands', 'plocal'); + }); + after(function () { + return DELETE_TEST_DB('test_db_commands'); + }); + describe('Server::freeze()', function () { + it("should freeze", function () { + return TEST_SERVER.freeze("test_db_commands", "plocal") + .then(function (response) { + response.should.be.true; + }); + }); + it("should allow only read-only operations", function(){ + return this.db.record.create({ + '@class': 'OUser', + name: 'testuser1', + password: 'testpassword1', + status: 'ACTIVE' + }) + .bind(this) + .then(function (record) { + throw new Error('Should never happen!'); + }) + .catch(LIB.errors.Request, function (e) { + return true; + }); + }); + }); + describe('Server::release()', function () { + it("should release", function () { + return TEST_SERVER.release("test_db_commands", "plocal") + .then(function (response) { + response.should.be.true; + }); + }); + it("should allow record creation", function(){ + return this.db.record.create({ + '@class': 'OUser', + name: 'testuser2', + password: 'testpassword2', + status: 'ACTIVE' + }) + .bind(this) + .then(function (record) { + return true; + }) + .catch(LIB.errors.Request, function (e) { + throw new Error('Should never happen!'); + }); + }); + }); +}); diff --git a/test/server/server-test.js b/test/server/server-test.js index 04e35ff..28873ec 100644 --- a/test/server/server-test.js +++ b/test/server/server-test.js @@ -1,96 +1,85 @@ var errors = LIB.errors; - - describe("Server", function () { - describe('Server::connect()', function () { - it("should negotiate a connection", function () { - return TEST_SERVER.connect() - .then(function (server) { - server.sessionId.should.be.above(-1); + describe('Server::create()', function () { + it("should create a new database", function () { + return TEST_SERVER.create({ + name: 'testdb_server', + type: 'graph', + storage: 'memory' + }) + .then(function (db) { + db.name.should.equal('testdb_server'); }); }); }); - describe('Server::send()', function () { - it("should handle errors correctly", function () { - return TEST_SERVER.send('db-open', { - name: 'not_an_existing_database', - type: 'graph', - username: 'admin', - password: 'admin' - }) + describe('Server::freeze()', function () { + it("should freeze", function () { + return TEST_SERVER.freeze("testdb_server") .then(function (response) { - throw new Error('Should Not Happen!'); - }) - .catch(errors.Request, function (e) { - e.type.should.equal('com.orientechnologies.orient.core.exception.OConfigurationException'); - return true; + response.should.be.true; }); - }) - }); -}); -describe('Server::create()', function () { - it("should create a new database", function () { - return TEST_SERVER.create({ - name: 'testdb_server', - type: 'graph', - storage: 'memory' - }) - .then(function (db) { - db.name.should.equal('testdb_server'); }); }); -}); -describe('Server::list()', function () { - it("should list the existing databases", function () { - return TEST_SERVER.list() - .then(function (dbs) { - dbs.length.should.be.above(0); - dbs.forEach(function (db) { - db.should.be.an.instanceOf(LIB.Db); + describe('Server::release()', function () { + it("should release", function () { + return TEST_SERVER.release("testdb_server") + .then(function (response) { + response.should.be.true; }); }); }); -}); -describe('Server::exists()', function () { - it("should confirm an existing database exists", function () { - return TEST_SERVER.exists('testdb_server') - .then(function (exists) { - exists.should.be.true; + describe('Server::list()', function () { + it("should list the existing databases", function () { + return TEST_SERVER.list() + .then(function (dbs) { + dbs.length.should.be.above(0); + dbs.forEach(function (db) { + db.should.be.an.instanceOf(LIB.Db); + }); + }); }); }); - it("should confirm a missing database does not exist", function () { - return TEST_SERVER.exists('a_missing_database') - .then(function (exists) { - exists.should.be.false; + describe('Server::exists()', function () { + it("should confirm an existing database exists", function () { + return TEST_SERVER.exists('testdb_server') + .then(function (exists) { + exists.should.be.true; + }); + }); + it("should confirm a missing database does not exist", function () { + return TEST_SERVER.exists('a_missing_database') + .then(function (exists) { + exists.should.be.false; + }); }); }); -}); -describe('Server::delete()', function () { - it("should delete a database", function () { - return TEST_SERVER.delete({ - name: 'testdb_server', - type: 'graph', - storage: 'memory' - }) - .then(function (response) { - response.should.be.true; + describe('Server::delete()', function () { + it("should delete a database", function () { + return TEST_SERVER.drop({ + name: 'testdb_server', + type: 'graph', + storage: 'memory' + }) + .then(function (response) { + response.should.be.true; + }); }); }); -}); -describe('Server::config.list', function () { - it("should list the server config", function () { - return TEST_SERVER.config.list() - .then(function (config) { - config.should.have.property('db.pool.min'); + describe('Server::config.list', function () { + it("should list the server config", function () { + return TEST_SERVER.config.list() + .then(function (config) { + config.should.have.property('db.pool.min'); + }); }); }); -}); -describe('Server::config.get', function () { - it("should get a server config key", function () { - return TEST_SERVER.config.get('db.pool.min') - .then(function (value) { - value.should.have.type('string'); + describe('Server::config.get', function () { + it("should get a server config key", function () { + return TEST_SERVER.config.get('db.pool.min') + .then(function (value) { + value.should.have.type('string'); + }); }); }); -}); \ No newline at end of file +}); diff --git a/test/test-server.json b/test/test-server.json index e88d2c2..6d36843 100644 --- a/test/test-server.json +++ b/test/test-server.json @@ -1,6 +1,7 @@ { "host": "localhost", "port": 2424, + "httpPort": 2480, "username": "root", - "password": "3BA5DB89CC6206DBF835B36B70FF8A0EDCEFA617A229F0D44D1D726ABA04216A" + "password": "root" } \ No newline at end of file diff --git a/test/transport/binary/binary-transport-test.js b/test/transport/binary/binary-transport-test.js new file mode 100644 index 0000000..e12832f --- /dev/null +++ b/test/transport/binary/binary-transport-test.js @@ -0,0 +1,28 @@ +var errors = LIB.errors; +describe("Binary Transport", function () { + describe('BinaryTransport::connect()', function () { + it("should negotiate a connection", function () { + return TEST_SERVER.transport.connect() + .then(function (server) { + server.sessionId.should.be.above(-1); + }); + }); + }); + describe('BinaryTransport::send()', function () { + it("should handle errors correctly", function () { + return TEST_SERVER.transport.send('db-open', { + name: 'not_an_existing_database', + type: 'graph', + username: 'admin', + password: 'admin' + }) + .then(function (response) { + throw new Error('Should Not Happen!'); + }) + .catch(errors.Request, function (e) { + e.type.should.equal('com.orientechnologies.orient.core.exception.OConfigurationException'); + return true; + }); + }) + }); +}); \ No newline at end of file diff --git a/test/transport/binary/protocol19/deserializer-test.js b/test/transport/binary/protocol19/deserializer-test.js new file mode 100644 index 0000000..728cefd --- /dev/null +++ b/test/transport/binary/protocol19/deserializer-test.js @@ -0,0 +1,305 @@ +var deserializer = require(LIB_ROOT + '/transport/binary/protocol19/deserializer'); +var serializer = require(LIB_ROOT + '/transport/binary/protocol19/serializer'); + +describe("Deserializer", function () { + it('should go fast!', function () { + var limit = 100000, + input = 'OUser@foo:123,baz:"bazz\\"za",int: 1234,true:true,false:false,null:null,date:123456a,rid:#12:10', + size = input.length * limit, + start = Date.now(); + + for (var i = 0; i < limit; i++) { + deserializer.deserialize(input); + } + + var stop = Date.now(), + total = (stop - start) / 1000; + + console.log('Done in ' + total + 's, ', (limit / total).toFixed(3), 'documents / sec', (((size / total) / 1024) / 1024).toFixed(3), ' Mb / sec') + }); + + it('should go fast, using simple string keys', function () { + var record = {}; + for (var k = 0; k < 15; k++) { + record["name" + k] = "Luca" + k; + } + + var limit = 100000, + input = serializer.serializeDocument(record), + size = input.length * limit, + start = Date.now(); + + for (var i = 0; i < limit; i++) { + deserializer.deserialize(input); + } + + var stop = Date.now(), + total = (stop - start) / 1000; + + console.log('Done in ' + total + 's, ', (limit / total).toFixed(3), 'documents / sec', (((size / total) / 1024) / 1024).toFixed(3), ' Mb / sec') + }); + + + it('should go fast, using more complex keys', function () { + var record = {}; + record["name"] = "john"; + record["surname"] = "wood"; + record["age"] = 20; + record["born"] = new Date(); + record["money"] = 10.0; + record["address"] = "somewhere bla bla"; + record["active"] = true; + record["timestamp"] = 20; + record["other1"] = "other1"; + record["other2"] = 2; + record["other3"] = 3; + + var limit = 100000, + input = serializer.serializeDocument(record), + size = input.length * limit, + start = Date.now(); + + for (var i = 0; i < limit; i++) { + deserializer.deserialize(input); + } + + var stop = Date.now(), + total = (stop - start) / 1000; + + console.log('Done in ' + total + 's, ', (limit / total).toFixed(3), 'documents / sec', (((size / total) / 1024) / 1024).toFixed(3), ' Mb / sec') + }); + + + + + describe('deserialize()', function () { + it('should parse a very simple record', function () { + var input = 'OUser@foo:123,baz:"bazx\\"za",int:1234,true:true,false:false,null:null,date:123456a,rid:#12:10,array:[1,2,3,4,5],twice:"\\"127.0.0.1\\""'; + var parsed = deserializer.deserialize(input); + parsed.should.eql({ + '@type': 'd', + '@class': 'OUser', + foo: 123, + baz: 'bazx"za', + int: 1234, + true: true, + false: false, + null: null, + date: new Date(123456), + rid: new LIB.RID('#12:10'), + array: [1, 2, 3, 4, 5], + twice: '"127.0.0.1"' + }); + }); + }); + describe('eatString()', function () { + it('should eat a string', function () { + var input = 'this is a string"'; + var parsed = deserializer.eatString(input); + parsed[0].should.equal('this is a string'); + parsed[1].length.should.equal(0); + }); + it('should eat a string which contains escaped double quotes', function () { + var input = 'this \\"is\\" a string"'; + var parsed = deserializer.eatString(input); + parsed[0].should.equal('this "is" a string'); + parsed[1].length.should.equal(0); + }); + }); + describe('eatNumber()', function () { + it('should eat an integer', function () { + var input = '1234,'; + var parsed = deserializer.eatNumber(input); + parsed[0].should.equal(1234); + parsed[1].length.should.equal(1); + }); + it('should eat a number with a decimal point', function () { + var input = '1234.567,'; + var parsed = deserializer.eatNumber(input); + parsed[0].should.equal(1234.567); + parsed[1].length.should.equal(1); + }); + it('should eat a float', function () { + var input = '1234f'; + var parsed = deserializer.eatNumber(input); + parsed[0].should.equal(1234); + parsed[1].length.should.equal(0); + }); + it('should eat a float in scientific notation', function () { + var input = '1.234e-04f'; + var parsed = deserializer.eatNumber(input); + parsed[0].should.equal(1.234e-04); + parsed[1].length.should.equal(0); + }); + it('should eat a date', function () { + var input = '1a'; + var parsed = deserializer.eatNumber(input); + parsed[0].should.eql(new Date(1)); + parsed[1].length.should.equal(0); + }); + }); + describe('eatRID()', function () { + it('should eat a record id', function () { + var input = '12:10'; + var parsed = deserializer.eatRID(input); + parsed[0].toString().should.equal('#12:10'); + parsed[1].length.should.equal(0); + }); + it('should eat a record id, with a trailing comma', function () { + var input = '12:10,'; + var parsed = deserializer.eatRID(input); + parsed[0].toString().should.equal('#12:10'); + parsed[1].length.should.equal(1); + }); + }); + describe('eatArray()', function () { + it('should eat an array', function () { + var input = '1,2,3]'; + var parsed = deserializer.eatArray(input); + parsed[0].should.eql([1,2,3]); + parsed[1].length.should.equal(0); + }); + it('should eat an empty array', function () { + var input = ']'; + var parsed = deserializer.eatArray(input); + parsed[0].should.eql([]); + parsed[1].length.should.equal(0); + }); + it('should eat an empty array, with a trailing comma', function () { + var input = '],'; + var parsed = deserializer.eatArray(input); + parsed[0].should.eql([]); + parsed[1].length.should.equal(1); + }); + }); + describe('eatSet()', function () { + it('should eat a set', function () { + var input = '1,2,3>'; + var parsed = deserializer.eatSet(input); + parsed[0].should.eql([1,2,3]); + parsed[1].length.should.equal(0); + }); + it('should eat an empty set', function () { + var input = '>'; + var parsed = deserializer.eatSet(input); + parsed[0].should.eql([]); + parsed[1].length.should.equal(0); + }); + it('should eat an empty set, with a trailing comma', function () { + var input = '>,'; + var parsed = deserializer.eatSet(input); + parsed[0].should.eql([]); + parsed[1].length.should.equal(1); + }); + }); + describe('eatMap()', function () { + it('should eat a map', function () { + var input = '"key":"value","key2":2,"null": null}'; + var parsed = deserializer.eatMap(input); + parsed[0].should.eql({key: 'value', key2: 2, null: null}); + parsed[1].length.should.equal(0); + }); + it('should eat an empty map', function () { + var input = '}'; + var parsed = deserializer.eatMap(input); + parsed[0].should.eql({}); + parsed[1].length.should.equal(0); + }); + it('should eat an empty map, with a trailing comma', function () { + var input = '},'; + var parsed = deserializer.eatMap(input); + parsed[0].should.eql({}); + parsed[1].length.should.equal(1); + }); + }); + describe('eatRecord()', function () { + it('should eat a record', function () { + var input = 'key:"value",key2:2,null:)'; + var parsed = deserializer.eatRecord(input); + parsed[0].should.eql({'@type': 'd', key: 'value', key2: 2, null: null}); + parsed[1].length.should.equal(0); + }); + it('should eat an empty record', function () { + var input = ')'; + var parsed = deserializer.eatRecord(input); + parsed[0].should.eql({'@type': 'd'}); + parsed[1].length.should.equal(0); + }); + it('should eat an empty record with a class name', function () { + var input = 'foo@)'; + var parsed = deserializer.eatRecord(input); + parsed[0].should.eql({ + '@type': 'd', + '@class': 'foo' + }); + parsed[1].length.should.equal(0); + }); + it('should eat an empty record, with a trailing comma', function () { + var input = '),'; + var parsed = deserializer.eatRecord(input); + parsed[0].should.eql({'@type': 'd'}); + parsed[1].length.should.equal(1); + }); + }); + describe('eatBag()', function () { + it('should eat a RID bag', function () { + var input = 'AQAAAAoACwAAAAAAAAACAAsAAAAAAAAAAQALAAAAAAAAAAoACwAAAAAAAAAJAAsAAAAAAAAACAALAAAAAAAAAAcACwAAAAAAAAAGAAsAAAAAAAAABQALAAAAAAAAAAQACwAAAAAAAAAD;'; + var parsed = deserializer.eatBag(input); + parsed[0].should.be.an.instanceOf(LIB.Bag); + parsed[0].size.should.equal(10); + parsed[1].length.should.equal(0); + }); + it('should eat a RID bag with a trailing comma', function () { + var input = 'AQAAAAoACwAAAAAAAAACAAsAAAAAAAAAAQALAAAAAAAAAAoACwAAAAAAAAAJAAsAAAAAAAAACAALAAAAAAAAAAcACwAAAAAAAAAGAAsAAAAAAAAABQALAAAAAAAAAAQACwAAAAAAAAAD;,'; + var parsed = deserializer.eatBag(input); + parsed[0].should.be.an.instanceOf(LIB.Bag); + parsed[0].size.should.equal(10); + parsed[1].length.should.equal(1); + }); + }); + describe('eatBinary()', function () { + it('should eat a binary field', function () { + var input = new Buffer('Hello World', 'utf8'); + var parsed = deserializer.eatBinary(input.toString('base64') + '_'); + parsed[0].should.be.instanceOf(Buffer); + parsed[0].should.eql(input); + parsed[1].length.should.equal(0); + }); + it('should eat an empty binary field', function () { + var input = new Buffer('', 'utf8'); + var parsed = deserializer.eatBinary(input.toString('base64') + '_'); + parsed[0].should.be.instanceOf(Buffer); + parsed[0].should.eql(input); + parsed[1].length.should.equal(0); + }); + }); + describe('eatKey', function () { + it('should eat an unquoted key', function () { + var input = 'mykey:123'; + var parsed = deserializer.eatKey(input); + parsed[0].should.equal('mykey'); + parsed[1].length.should.equal(3); + }); + + it('should eat a quoted key', function () { + var input = '"mykey":123'; + var parsed = deserializer.eatKey(input); + parsed[0].should.equal('mykey'); + parsed[1].length.should.equal(3); + }); + }); + describe('eatValue', function () { + it('should eat a null value', function () { + var input = ','; + var parsed = deserializer.eatValue(input); + expect(parsed[0]).to.equal(null); + parsed[1].length.should.equal(1); + }); + it('should eat a string value', function () { + var input = '"foo bar"'; + var parsed = deserializer.eatValue(input); + parsed[0].should.equal('foo bar'); + parsed[1].length.should.equal(0); + }); + }); +}); diff --git a/test/protocol/operation-test.js b/test/transport/binary/protocol19/operation-test.js similarity index 98% rename from test/protocol/operation-test.js rename to test/transport/binary/protocol19/operation-test.js index 836e75a..0449e86 100644 --- a/test/protocol/operation-test.js +++ b/test/transport/binary/protocol19/operation-test.js @@ -1,4 +1,4 @@ -var Operation = LIB.protocol.Operation; +var Operation = require(LIB_ROOT + '/transport/binary/protocol19/operation'); describe('Operation', function () { diff --git a/test/protocol/operations/config-operations-test.js b/test/transport/binary/protocol19/operations/config-operations-test.js similarity index 100% rename from test/protocol/operations/config-operations-test.js rename to test/transport/binary/protocol19/operations/config-operations-test.js diff --git a/test/protocol/operations/db-operations-test.js b/test/transport/binary/protocol19/operations/db-operations-test.js similarity index 96% rename from test/protocol/operations/db-operations-test.js rename to test/transport/binary/protocol19/operations/db-operations-test.js index bac8b3b..ea49e2d 100644 --- a/test/protocol/operations/db-operations-test.js +++ b/test/transport/binary/protocol19/operations/db-operations-test.js @@ -1,5 +1,4 @@ -var path = require('path'), - dbSessionId = -1, +var dbSessionId = -1, dataCluster = -1, dataSegment = -1, serverCluster = {}, @@ -295,16 +294,6 @@ describe("Database Operations", function () { }); }); }); - describe('datasegment-add', function () { - it("should add a data segment", function () { - return TEST_SERVER.send('datasegment-add', { - sessionId: dbSessionId, - location: '/tmp', - name: 'test_segment' - }); - }); - }); - describe('db-close', function () { it("should close a database", function () { return TEST_SERVER.send('db-close', { diff --git a/test/transport/rest/rest-transport-test.js b/test/transport/rest/rest-transport-test.js new file mode 100644 index 0000000..a7bd76f --- /dev/null +++ b/test/transport/rest/rest-transport-test.js @@ -0,0 +1,121 @@ +var errors = LIB.errors; +var Promise = require('bluebird'); + +describe("Rest Transport", function () { + describe('RestTransport::send()', function () { + it("should handle errors correctly", function () { + return REST_SERVER.transport.send('db-open', { + name: 'not_an_existing_database', + type: 'graph', + username: 'admin', + password: 'admin' + }) + .then(function (response) { + throw new Error('Should Not Happen!'); + }) + .catch(errors.Request, function (e) { + e.message.should.equal('Authorization Error'); + return true; + }); + }) + }); + + describe('REST Operations', function () { + before(function () { + return CREATE_TEST_DB(this, 'testdb_rest'); + }); + after(function () { + return DELETE_TEST_DB('testdb_rest'); + }); + + + describe('Record Load', function () { + it('should load a record', function () { + var params = { + sessionId: -1, + database: 'testdb_rest', + cluster: 5, + position: 0 + }; + return Promise.all([REST_SERVER.send('record-load', params), this.db.send('record-load', params)]) + .spread(function (fromRest, fromBinary) { + fromRest = fromRest.records[0]; + fromBinary = fromBinary.records[0]; + fromRest['@rid'].should.eql(fromBinary['@rid']); + fromRest['@version'].should.equal(fromBinary['@version']); + fromRest['@type'].should.equal(fromBinary['@type']); + fromRest.name.should.equal(fromBinary.name); + fromRest.status.should.equal(fromBinary.status); + fromRest.roles.length.should.equal(fromBinary.roles.length); + }); + }); + + it('should load a record with a fetch plan', function () { + var params = { + sessionId: -1, + database: 'testdb_rest', + cluster: 5, + position: 0, + fetchPlan: 'roles:1' + }; + return Promise.all([REST_SERVER.send('record-load', params), this.db.send('record-load', params)]) + .spread(function (fromRest, fromBinary) { + fromRest.records[0]['@rid'].should.eql(fromBinary.records[0]['@rid']); + fromRest.records[0]['@version'].should.equal(fromBinary.records[0]['@version']); + fromRest.records[0]['@type'].should.equal(fromBinary.records[0]['@type']); + fromRest.records[0].name.should.equal(fromBinary.records[0].name); + fromRest.records[0].status.should.equal(fromBinary.records[0].status); + fromRest.records[0].roles.length.should.equal(fromBinary.records[0].roles.length); + fromRest.records[0].roles[0]['@rid'].should.eql(fromBinary.records[0].roles[0]); + fromRest.records[0].roles[0]['@rid'].should.eql(fromBinary.records[1]['@rid']); + fromRest.records[0].roles[0].name.should.eql(fromBinary.records[1].name); + }); + }); + }); + + + describe('Db Open', function () { + it('should open the database', function () { + var params = { + sessionId: -1, + name: 'testdb_rest', + type: 'graph', + username: 'admin', + password: 'admin' + }; + return Promise.all([REST_SERVER.send('db-open', params), TEST_SERVER.send('db-open', params)]) + .spread(function (fromRest, fromBinary) { + fromRest.release.should.equal(fromBinary.release); + fromRest.totalClusters.should.equal(fromBinary.totalClusters); + fromRest.clusters.length.should.equal(fromBinary.clusters.length); + fromRest.clusters.map(pluck('name')).sort().should.eql(fromBinary.clusters.map(pluck('name')).sort()); + }); + }) + }); + + + describe('Command', function () { + it('should execute a query', function () { + var config = { + database: 'testdb_rest', + class: 'com.orientechnologies.orient.core.sql.query.OSQLSynchQuery', + limit: 2, + query: 'SELECT * FROM OUser', + mode: 's' + }; + return Promise.all([REST_SERVER.send('command', config), this.db.send('command', config)]) + .spread(function (fromRest, fromBinary) { + fromRest.results.length.should.equal(fromBinary.results.length); + fromRest.results[0].content.length.should.equal(fromBinary.results[0].content.length); + }); + }); + }); + + + function pluck (key) { + return function (item) { + return item[key]; + } + } + }); +}); \ No newline at end of file