├── .nvmrc ├── .gitignore ├── docs ├── extra.css ├── getting_started.md ├── index.md └── api_docs.md ├── mkdocs.yml ├── .jslintrc ├── Makefile ├── index.js ├── lib ├── utils.js ├── client.js └── collection.js ├── package.json ├── circle.yml ├── test ├── testutils.js ├── test_query_events.js ├── test_constraints.js ├── test_client.js ├── test_writes.js └── test_reads.js └── readme.markdown /.nvmrc: -------------------------------------------------------------------------------- 1 | 4.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .tern-port 3 | -------------------------------------------------------------------------------- /docs/extra.css: -------------------------------------------------------------------------------- 1 | h2 { 2 | margin-top: 2em; 3 | } 4 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Todo. See [BedquiltDB Guide](http://bedquiltdb.readthedocs.io/en/latest/guide/). 4 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Node-Bedquilt Documentation 2 | theme: readthedocs 3 | pages: 4 | - [index.md, Home] 5 | - [getting_started.md, Getting Started] 6 | - [api_docs.md, API Docs] 7 | -------------------------------------------------------------------------------- /.jslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals" : { 3 | /* MOCHA */ 4 | "describe" : false, 5 | "it" : false, 6 | "before" : false, 7 | "beforeEach" : false, 8 | "after" : false, 9 | "afterEach" : false 10 | } 11 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # node-bedquilt makefile 2 | MOCHA = ./node_modules/mocha/bin/mocha 3 | 4 | all: docs test 5 | 6 | test: 7 | $(MOCHA) 8 | 9 | docs: 10 | echo 'Nope' 11 | #documentation -f md > docs/api_docs.md 12 | 13 | .PHONY: all test docs 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true*/ 2 | /*global require exports*/ 3 | "use strict"; 4 | 5 | var BedquiltClient = require('./lib/client.js'); 6 | var BedquiltCollection = require('./lib/collection.js'); 7 | 8 | exports.BedquiltClient = BedquiltClient; 9 | exports.BedquiltCollection = BedquiltCollection; 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Node-Bedquilt Documentation 2 | 3 | Node-Bedquilt is the nodejs driver for [BedquiltDB](http://bedquiltdb.github.io). 4 | 5 | 6 | ## Installation 7 | 8 | PyBedquilt can be installed via `npm`: 9 | 10 | ``` 11 | $ npm install node-bedquilt --save 12 | ``` 13 | 14 | 15 | ## Sections 16 | 17 | ### [Getting Started](getting_started.md) 18 | 19 | ### [API Documentation](api_docs.md) 20 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true*/ 2 | /*jshint esnext: true*/ 3 | /*global require exports*/ 4 | 5 | /** Various result projection functions 6 | */ 7 | var project = { 8 | column: () => { 9 | return (result) => { 10 | return result.rows.map((row) => { 11 | return row.bq_result; 12 | }); 13 | }; 14 | }, 15 | single: () => { 16 | return (result) => { 17 | if (result.rows.length === 0) { 18 | return null; 19 | } else { 20 | return result.rows[0].bq_result; 21 | } 22 | }; 23 | } 24 | }; 25 | 26 | module.exports.project = project; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bedquilt", 3 | "version": "2.0.0", 4 | "description": "A nodejs driver for BedquiltDB (http://bedquiltdb.github.io)", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/BedquiltDB/node-bedquilt.git" 8 | }, 9 | "homepage": "http://github.com/BedquiltDB/node-bedquilt", 10 | "scripts": { 11 | "test": "make test" 12 | }, 13 | "main": "index.js", 14 | "author": "Shane Kilkelly ", 15 | "license": "MIT", 16 | "keywords": [ 17 | "bedquilt", 18 | "bedquiltdb", 19 | "json", 20 | "nosql", 21 | "postgresql" 22 | ], 23 | "devDependencies": { 24 | "mocha": "^2.2.4", 25 | "should": "^6.0.1", 26 | "async": "^0.9.0" 27 | }, 28 | "dependencies": { 29 | "async": "^0.9.0", 30 | "pg": "^4.3.0" 31 | }, 32 | "engines": { 33 | "node": ">=4.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.2.6 4 | timezone: 5 | UTC 6 | services: 7 | - postgresql 8 | 9 | dependencies: 10 | pre: 11 | - sudo apt-get update 12 | - sudo apt-get install build-essential 13 | - sudo apt-get install libpq-dev 14 | - sudo apt-get install postgresql-server-dev-all 15 | - sudo apt-get install postgresql-common 16 | - sudo apt-get install postgresql-plpython3-9.5 17 | override: 18 | - npm install 19 | 20 | database: 21 | override: 22 | - createdb bedquilt_test 23 | - cd /tmp; rm -rf bedquilt-core; git clone git@github.com:BedquiltDB/bedquilt-core.git -b $CIRCLE_BRANCH; cd bedquilt-core; sudo make install-head; 24 | - sudo psql -d bedquilt_test -c "create extension if not exists pgcrypto;create extension if not exists plpython3u; drop extension if exists bedquilt; create extension bedquilt;" 25 | 26 | test: 27 | override: 28 | - make test -------------------------------------------------------------------------------- /test/testutils.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | /* jshint esnext: true */ 3 | "use strict"; 4 | 5 | let pg = require('pg'); 6 | let BedquiltClient = require('../index.js').BedquiltClient; 7 | let connectionString = 'postgres://localhost/bedquilt_test'; 8 | 9 | let cleanDatabase = (callback) => { 10 | pg.connect(connectionString, (err, client, done) => { 11 | let query = ` 12 | drop schema public cascade; 13 | create schema public; 14 | create extension pgcrypto; 15 | create extension bedquilt; 16 | `; 17 | client.query(query, [], (err, result) => { 18 | if(err) { 19 | throw err; 20 | } else { 21 | done(); 22 | return callback(); 23 | } 24 | }); 25 | }); 26 | }; 27 | 28 | let connect = (callback) => { 29 | BedquiltClient.connect(connectionString, callback); 30 | }; 31 | 32 | exports.connectionString = connectionString; 33 | exports.cleanDatabase = cleanDatabase; 34 | exports.connect = connect; 35 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | node-bedquilt 2 | ============= 3 | 4 | The nodejs driver for [BedquiltDB](http://bedquiltdb.github.io) 5 | 6 | Requires `node` >= 4.0.0 7 | 8 | [![CircleCI](https://circleci.com/gh/BedquiltDB/node-bedquilt.svg?style=svg)](https://circleci.com/gh/BedquiltDB/node-bedquilt) 9 | 10 | 11 | ## Install 12 | 13 | ``` 14 | $ npm install bedquilt --save 15 | ``` 16 | 17 | ## Getting Started 18 | 19 | - First, [set up BedquiltDB](http://bedquiltdb.readthedocs.org/en/latest/getting_started/) 20 | - Install the node-bedquilt package: `npm install bedquilt` 21 | - Connect to the BedquiltDB server: 22 | ```javascript 23 | var BedquiltClient = require('bedquilt').BedquiltClient; 24 | 25 | BedquiltClient.connect('postgres://localhost/test', function(err, client) { 26 | 27 | var people = client.collection('people'); 28 | 29 | people.insert({name: "Sarah", age: 45}, function(err, result) { 30 | console.log('Added Sarah to the people collection with _id: ' + result); 31 | }); 32 | 33 | people.find({age: {'$gte': 50, '$lte': 70}}, function(err, result) { 34 | console.log('Users between 50 and 70: ' + result.length) 35 | }); 36 | 37 | // streaming 38 | cursor = people.find({city: {'$in': ['Edinburgh', 'Glasgow']}}) 39 | cursor.on('row', function(person) { 40 | console.log('Someone from Scotland: ' + person.name) 41 | }); 42 | cursor.on('end', function() { 43 | console.log('done'); 44 | }); 45 | 46 | }); 47 | ``` 48 | 49 | ## Documentation 50 | 51 | See the [node-bedquilt documentation](http://node-bedquilt.readthedocs.org/en/latest). 52 | 53 | 54 | ## Tests 55 | 56 | Run `make test` to run the test suite. Requires a local instance of PostgreSQL, with BedquiltDB installed and 57 | a `bedquilt_test` database set up. 58 | -------------------------------------------------------------------------------- /test/test_query_events.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true*/ 2 | /*jshint esnext: true*/ 3 | /*global require, describe, it, before, beforeEach, after, afterEach */ 4 | "use strict"; 5 | 6 | let should = require("should"); 7 | let testutils = require('./testutils.js'); 8 | let Async = require('async'); 9 | 10 | let populate = (callback) => { 11 | testutils.connect((err, client) => { 12 | let things = client.collection('things'); 13 | Async.series( 14 | [{name: 'sarah', age: 22}, 15 | {name: 'mike', age: 20}, 16 | {name: 'irene', age: 40}, 17 | {name: 'mary', age: 16}, 18 | {name: 'brian', age: 31}, 19 | {name: 'dave', age: 22}, 20 | {name: 'kate', age: 25}, 21 | {name: 'alice', age: 57}].map( 22 | (doc) => 23 | (next) => things.save(doc, next) 24 | ), 25 | (err, results) => 26 | callback() 27 | ); 28 | }); 29 | }; 30 | 31 | describe('BedquiltQuery events', () => { 32 | 33 | describe('#find() without callback', () => { 34 | beforeEach(testutils.cleanDatabase); 35 | afterEach(testutils.cleanDatabase); 36 | 37 | it('should skip two documents', (done) => { 38 | populate(() => { 39 | testutils.connect((err, client) => { 40 | let names = []; 41 | let things = client.collection('things'); 42 | let query = things.find({}, (err, result) => {}); 43 | 44 | query.on('row', (row) => { 45 | names.push(row.name); 46 | }); 47 | 48 | query.on('end', (result) => { 49 | should.deepEqual(names, [ 50 | 'sarah', 51 | 'mike', 52 | 'irene', 53 | 'mary', 54 | 'brian', 55 | 'dave', 56 | 'kate', 57 | 'alice' 58 | ]); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/test_constraints.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true*/ 2 | /*jshint esnext: true*/ 3 | /*global require, describe, it, before, beforeEach, after, afterEach */ 4 | "use strict"; 5 | 6 | let should = require("should"); 7 | let testutils = require('./testutils.js'); 8 | let Async = require('async'); 9 | 10 | describe('BedquiltCollection constraint ops', () => { 11 | 12 | describe('BedquiltCollection#addConstraints()', () => { 13 | beforeEach(testutils.cleanDatabase); 14 | afterEach(testutils.cleanDatabase); 15 | 16 | it('should add a simple constraint', (done) => { 17 | testutils.connect((err, client) => { 18 | let things = client.collection('things'); 19 | let constraints = { 20 | name: {$required: 1, 21 | $notnull: 1} 22 | }; 23 | things.addConstraints(constraints, (err, result) => { 24 | should.equal(result, true); 25 | things.save({wat: 1}, (err, result) => { 26 | should.notEqual(err, null); 27 | should.equal(result, null); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | }); 33 | 34 | it('should work with a more complex constraint', (done) => { 35 | testutils.connect((err, client) => { 36 | let people = client.collection('people'); 37 | let spec = { 38 | name: { 39 | $required: 1, 40 | $notnull: 1, 41 | $type: 'string' 42 | }, 43 | age: { 44 | $required: 1, 45 | $type: 'number' 46 | }, 47 | addresses: { 48 | $required: 1, 49 | $type: 'array' 50 | }, 51 | 'address.0.city': { 52 | $required: 1, 53 | $notnull: 1, 54 | $type: 'string' 55 | } 56 | }; 57 | people.addConstraints(spec, (err, result) => { 58 | should.equal(result, true); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | 64 | }); 65 | 66 | describe('BedquiltCollection#listConstraints()', () => { 67 | beforeEach(testutils.cleanDatabase); 68 | afterEach(testutils.cleanDatabase); 69 | 70 | it('should list all constraints', (done) => { 71 | testutils.connect((err, client) => { 72 | let things = client.collection('things'); 73 | let constraints = { 74 | name: {$required: 1, 75 | $notnull: 1} 76 | }; 77 | things.listConstraints((err, result) => { 78 | should.deepEqual(result, []); 79 | things.addConstraints(constraints, (err, result) => { 80 | should.equal(result, true); 81 | things.listConstraints((err, result) => { 82 | should.deepEqual(result, ['name:notnull', 'name:required']); 83 | done(); 84 | }); 85 | }); 86 | }); 87 | }); 88 | }); 89 | }); 90 | 91 | describe('BedquiltCollection#removeConstraints()', () => { 92 | beforeEach(testutils.cleanDatabase); 93 | afterEach(testutils.cleanDatabase); 94 | 95 | it('should remove constraints', (done) => { 96 | testutils.connect((err, client) => { 97 | let things = client.collection('things'); 98 | let constraints = { 99 | name: {$required: 1, 100 | $notnull: 1} 101 | }; 102 | things.addConstraints(constraints, (err, result) => { 103 | should.equal(result, true); 104 | things.save({wat: 1}, (err, result) => { 105 | should.notEqual(err, null); 106 | should.equal(result, null); 107 | things.removeConstraints(constraints, (err, result) => { 108 | should.equal(result, true); 109 | things.insert({wat: 1}, (err, result) => { 110 | should.equal(err, null); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | }); 116 | }); 117 | }); 118 | }); 119 | 120 | }); 121 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true*/ 2 | /*jshint esnext: true*/ 3 | /*global require exports module */ 4 | "use strict"; 5 | 6 | const pg = require('pg'); 7 | const BedquiltCollection = require('./collection.js'); 8 | const project = require('./utils.js').project; 9 | const util = require('util'); 10 | const EventEmitter = require('events'); 11 | 12 | const cache = {}; 13 | const MINIMUM_SERVER_VERSION = '2.0.0'; 14 | 15 | function BedquiltClient(connectionString) { 16 | this.connectionString = connectionString; 17 | 18 | this._pg = pg; 19 | 20 | this.collection = (collectionName) => { 21 | return new BedquiltCollection(this, collectionName); 22 | }; 23 | 24 | this._query = (queryString, params, projection, callback) => { 25 | var bqQuery = new BedquiltQuery(); 26 | pg.connect(this.connectionString, (err, client, done) => { 27 | if(err) { 28 | return callback(err, null); 29 | } 30 | bqQuery.init( 31 | client.query(queryString, params, (err, result) => { 32 | done(); 33 | if(err) { 34 | return callback(err, null); 35 | } 36 | if (callback) { 37 | return callback(null, projection(result)); 38 | } else { 39 | return null; 40 | } 41 | } 42 | )); 43 | return null; 44 | }); 45 | return bqQuery; 46 | }; 47 | 48 | this.listCollections = (callback) => { 49 | return this._query( 50 | 'select bq_list_collections() as bq_result;', 51 | [], 52 | project.column(), 53 | callback 54 | ); 55 | }; 56 | 57 | this.createCollection = (collectionName, callback) => { 58 | return this._query( 59 | 'select bq_create_collection($1::text) as bq_result;', 60 | [collectionName], 61 | project.single(), 62 | callback 63 | ); 64 | }; 65 | 66 | this.deleteCollection = (collectionName, callback) => { 67 | return this._query( 68 | 'select bq_delete_collection($1::text) as bq_result;', 69 | [collectionName], 70 | project.single(), 71 | callback 72 | ); 73 | }; 74 | 75 | this.collectionExists = (collectionName, callback) => { 76 | return this._query( 77 | 'select bq_collection_exists($1::text) as bq_result;', 78 | [collectionName], 79 | project.single(), 80 | callback 81 | ); 82 | }; 83 | } 84 | 85 | BedquiltClient.connect = (connectionString, callback) => { 86 | let previous = cache[connectionString]; 87 | let client = null; 88 | if (previous) { 89 | client = previous.client; 90 | if (previous.error) { 91 | throw previous.error; 92 | } 93 | return callback(null, client); 94 | } else { 95 | // build a new client and cache it 96 | client = new BedquiltClient(connectionString); 97 | cache[connectionString] = {client: client, error: null}; 98 | 99 | // check bedquilt version on db server 100 | pg.connect(connectionString, (err, db, done) => { 101 | let q = "select bq_util_assert_minimum_version($1::text)"; 102 | if (err) { 103 | return callback(err, null); 104 | } 105 | db.query(q, [MINIMUM_SERVER_VERSION], (err, result) => { 106 | done(); 107 | if (err) { 108 | // update cached value with the error and throw 109 | cache[connectionString].error = err; 110 | throw err; 111 | } else { 112 | // finally invoke the callback with client instance 113 | return callback(null, client); 114 | } 115 | }); 116 | return null; 117 | }); 118 | } 119 | return null; 120 | }; 121 | 122 | function BedquiltQuery() { 123 | 124 | this.init = (baseQuery) => { 125 | baseQuery.on('row', (row) => { 126 | this.emit('row', row.bq_result); 127 | }); 128 | 129 | baseQuery.on('error', (error) => { 130 | this.emit('error', error); 131 | }); 132 | 133 | baseQuery.on('end', (result) => { 134 | this.emit('end', project.column('bq_result')(result)); 135 | }); 136 | }; 137 | 138 | EventEmitter.call(this); 139 | } 140 | util.inherits(BedquiltQuery, EventEmitter); 141 | 142 | 143 | module.exports = BedquiltClient; 144 | -------------------------------------------------------------------------------- /test/test_client.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true*/ 2 | /*jshint esnext: true*/ 3 | /*global require, describe, it, before, beforeEach, after, afterEach */ 4 | "use strict"; 5 | 6 | let should = require("should"); 7 | let BedquiltClient = require('../index.js').BedquiltClient; 8 | let testutils = require('./testutils.js'); 9 | let Async = require('async'); 10 | 11 | describe('BedquiltClient', () => { 12 | 13 | describe('BedquiltClient#connect()', () => { 14 | beforeEach(testutils.cleanDatabase); 15 | afterEach(testutils.cleanDatabase); 16 | 17 | it('should connect', (done) => { 18 | BedquiltClient.connect( 19 | testutils.connectionString, 20 | (err, client) => { 21 | should.equal(err, null); 22 | should.notEqual(client, null); 23 | should.equal(client.connectionString, testutils.connectionString); 24 | done(); 25 | }); 26 | }); 27 | 28 | }); 29 | 30 | describe('BedquiltClient#query', () => { 31 | beforeEach(testutils.cleanDatabase); 32 | afterEach(testutils.cleanDatabase); 33 | 34 | it('should allow us to query', (done) => { 35 | testutils.connect((err, client) => { 36 | client._query('select 1 as num', [], 37 | (r) => { return r; }, 38 | (err, result) => { 39 | should.equal(err, null); 40 | should.equal(result.rows[0]['num'], 1); 41 | done(); 42 | }); 43 | }); 44 | }); 45 | 46 | }); 47 | 48 | describe('BedquiltClient#createCollection', () => { 49 | beforeEach(testutils.cleanDatabase); 50 | afterEach(testutils.cleanDatabase); 51 | 52 | it('should create a collection', (done) => { 53 | testutils.connect((err, client) => { 54 | client.createCollection('stuff', (err, created) => { 55 | should.equal(err, null); 56 | should.equal(created, true); 57 | done(); 58 | }); 59 | }); 60 | }); 61 | }); 62 | 63 | 64 | describe('BedquiltClient#deleteCollection', () => { 65 | beforeEach(testutils.cleanDatabase); 66 | afterEach(testutils.cleanDatabase); 67 | 68 | it('should not delete a collection which does not exist', (done) => { 69 | testutils.connect((err, client) => { 70 | client.deleteCollection('stuff', (err, deleted) => { 71 | should.equal(err, null); 72 | should.equal(deleted, false); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | 78 | it("should delete a collection", (done) => { 79 | testutils.connect((err, client) => { 80 | client.createCollection('stuff', (err, created) => { 81 | client.deleteCollection('stuff', (err, deleted) => { 82 | should.equal(err, null); 83 | should.equal(deleted, true); 84 | client.deleteCollection('stuff', (err, deleted) => { 85 | should.equal(deleted, false); 86 | done(); 87 | }); 88 | }); 89 | }); 90 | }); 91 | }); 92 | }); 93 | 94 | describe('BedquiltClient#collectionExists', () => { 95 | beforeEach(testutils.cleanDatabase); 96 | afterEach(testutils.cleanDatabase); 97 | 98 | it('should return false when the collection does not exist', (done) => { 99 | testutils.connect((err, client) => { 100 | client.collectionExists('stuff', (err, result) => { 101 | should.equal(err, null); 102 | should.equal(result, false); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | 108 | it('should return true when the collection exists', (done) => { 109 | testutils.connect((err, client) => { 110 | client.createCollection('stuff', (err, result) => { 111 | client.collectionExists('stuff', (err, result) => { 112 | should.equal(err, null); 113 | should.equal(result, true); 114 | done(); 115 | }); 116 | }); 117 | }); 118 | }); 119 | }); 120 | 121 | describe('BedquiltClien#listCollections', () => { 122 | beforeEach(testutils.cleanDatabase); 123 | afterEach(testutils.cleanDatabase); 124 | 125 | it('should return 0 when there are no collections', (done) => { 126 | // with no collections 127 | testutils.connect((err, client) => { 128 | client.listCollections((err, result) => { 129 | should.equal(err, null); 130 | should.equal(result.length, 0); 131 | done(); 132 | }); 133 | }); 134 | }); 135 | 136 | it('should return 1 when there is one collection', (done) => { 137 | testutils.connect((err, client) => { 138 | client.listCollections((err, result) => { 139 | should.equal(err, null); 140 | should.equal(result.length, 0); 141 | client.createCollection('things', (err, created) => { 142 | should.equal(err, null); 143 | should.equal(created, true); 144 | client.listCollections((err, collections) => { 145 | should.equal(collections.length, 1); 146 | should.equal(collections[0], 'things'); 147 | done(); 148 | }); 149 | }); 150 | }); 151 | }); 152 | }); 153 | 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /lib/collection.js: -------------------------------------------------------------------------------- 1 | /*jslint node: true*/ 2 | /*jshint esnext: true*/ 3 | /*global require exports*/ 4 | "use strict"; 5 | 6 | const project = require('./utils.js').project; 7 | 8 | /** 9 | * Create a BedquiltCollection instance 10 | * @constructor 11 | * @param {BedquiltClient} client The BedquiltClient instance to use 12 | * @param {string} collectionName Name of collection 13 | */ 14 | function BedquiltCollection(client, collectionName) { 15 | this.client = client; 16 | this.collectionName = collectionName; 17 | 18 | this.distinct = function(keyPath, callback) { 19 | return this.client._query( 20 | "select bq_distinct($1::text, $2::text) as bq_result;", 21 | [this.collectionName, keyPath], 22 | project.column(), 23 | callback 24 | ); 25 | }; 26 | 27 | this.count = function(queryDoc, callback) { 28 | return this.client._query( 29 | "select bq_count($1::text, $2::jsonb) as bq_result;", 30 | [this.collectionName, queryDoc], 31 | project.single(), 32 | callback 33 | ); 34 | }; 35 | 36 | this.insert = function(doc, callback) { 37 | return this.client._query( 38 | "select bq_insert($1::text, $2::jsonb) as bq_result;", 39 | [this.collectionName, doc], 40 | project.single(), 41 | callback 42 | ); 43 | }; 44 | 45 | this.save = function(doc, callback) { 46 | return this.client._query( 47 | "select bq_save($1::text, $2::jsonb) as bq_result;", 48 | [this.collectionName, doc], 49 | project.single(), 50 | callback 51 | ); 52 | }; 53 | 54 | this.find = function(queryDoc, options, callback) { 55 | var _query, _opts, _cb; 56 | var skip = 0; 57 | var limit = null; 58 | var sort = null; 59 | if (arguments.length === 1) { 60 | _query = arguments[0]; 61 | _opts = {}; 62 | } else if (arguments.length === 2) { 63 | if (typeof arguments[1] === 'function') { 64 | _query = arguments[0]; 65 | _opts = {}; 66 | _cb = arguments[1]; 67 | } else { 68 | _query = arguments[0]; 69 | _opts = arguments[1]; 70 | } 71 | } else if (arguments.length === 3) { 72 | _query = arguments[0]; 73 | _opts = arguments[1]; 74 | _cb = arguments[2]; 75 | } else { 76 | throw new Error("Wrong number of arguments passed to collection.find"); 77 | } 78 | if (_opts.skip) { 79 | skip = _opts.skip; 80 | } 81 | if (_opts.limit) { 82 | limit = _opts.limit; 83 | } 84 | if (_opts.sort) { 85 | sort = JSON.stringify(_opts.sort); 86 | } 87 | return this.client._query( 88 | "select bq_find($1::text, $2::jsonb, $3::integer, $4::integer, $5::jsonb) as bq_result;", 89 | [this.collectionName, _query, skip, limit, sort], 90 | project.column(), 91 | _cb 92 | ); 93 | }; 94 | 95 | this.findOne = function(queryDoc, options, callback) { 96 | var _query, _opts, _cb; 97 | var skip = 0; 98 | var sort = null; 99 | if (arguments.length === 1) { 100 | _query = arguments[0]; 101 | _opts = {}; 102 | } else if (arguments.length === 2) { 103 | if (typeof arguments[1] === 'function') { 104 | _query = arguments[0]; 105 | _opts = {}; 106 | _cb = arguments[1]; 107 | } else { 108 | _query = arguments[0]; 109 | _opts = arguments[1]; 110 | } 111 | } else if (arguments.length === 3) { 112 | _query = arguments[0]; 113 | _opts = arguments[1]; 114 | _cb = arguments[2]; 115 | } else { 116 | throw new Error("Wrong number of arguments passed to collection.find"); 117 | } 118 | if (_opts.skip) { 119 | skip = _opts.skip; 120 | } 121 | if (_opts.sort) { 122 | sort = JSON.stringify(_opts.sort); 123 | } 124 | return this.client._query( 125 | "select bq_find_one($1::text, $2::jsonb, $3::integer, $4::jsonb) as bq_result;", 126 | [this.collectionName, _query, skip, sort], 127 | project.single(), 128 | _cb 129 | ); 130 | }; 131 | 132 | this.findOneById = function(_id, callback) { 133 | return this.client._query( 134 | "select bq_find_one_by_id($1::text, $2::text) as bq_result;", 135 | [this.collectionName, _id], 136 | project.single(), 137 | callback 138 | ); 139 | }; 140 | 141 | this.findManyByIds = function(ids, callback) { 142 | return this.client._query( 143 | "select bq_find_many_by_ids($1::text, $2::jsonb) as bq_result;", 144 | [this.collectionName, JSON.stringify(ids)], 145 | project.column(), 146 | callback 147 | ); 148 | }; 149 | 150 | this.remove = function(queryDoc, callback) { 151 | return this.client._query( 152 | "select bq_remove($1::text, $2::jsonb) as bq_result;", 153 | [this.collectionName, queryDoc], 154 | project.single(), 155 | callback 156 | ); 157 | }; 158 | 159 | this.removeOne = function(queryDoc, callback) { 160 | return this.client._query( 161 | "select bq_remove_one($1::text, $2::jsonb) as bq_result;", 162 | [this.collectionName, queryDoc], 163 | project.single(), 164 | callback 165 | ); 166 | }; 167 | 168 | this.removeOneById = function(_id, callback) { 169 | return this.client._query( 170 | "select bq_remove_one_by_id($1::text, $2::text) as bq_result;", 171 | [this.collectionName, _id], 172 | project.single(), 173 | callback 174 | ); 175 | }; 176 | 177 | this.removeManyByIds = function(ids, callback) { 178 | return this.client._query( 179 | "select bq_remove_many_by_ids($1::text, $2::jsonb) as bq_result;", 180 | [this.collectionName, JSON.stringify(ids)], 181 | project.single(), 182 | callback 183 | ); 184 | }; 185 | 186 | this.addConstraints = function(specDoc, callback) { 187 | return this.client._query( 188 | "select bq_add_constraints($1::text, $2::jsonb) as bq_result;", 189 | [this.collectionName, specDoc], 190 | project.single(), 191 | callback 192 | ); 193 | }; 194 | 195 | this.removeConstraints = function(specDoc, callback) { 196 | return this.client._query( 197 | "select bq_remove_constraints($1::text, $2::jsonb) as bq_result;", 198 | [this.collectionName, specDoc], 199 | project.single(), 200 | callback 201 | ); 202 | }; 203 | 204 | this.listConstraints = function(callback) { 205 | return this.client._query( 206 | "select bq_list_constraints($1::text) as bq_result;", 207 | [this.collectionName], 208 | project.column(), 209 | callback 210 | ); 211 | }; 212 | } 213 | 214 | module.exports = BedquiltCollection; 215 | -------------------------------------------------------------------------------- /test/test_writes.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true*/ 2 | /*jshint esnext: true*/ 3 | /*global require, describe, it, before, beforeEach, after, afterEach */ 4 | "use strict"; 5 | 6 | let should = require("should"); 7 | let testutils = require('./testutils.js'); 8 | let Async = require('async'); 9 | 10 | describe('BedquiltCollection write ops', () => { 11 | 12 | describe('BedquiltCollection#remove()', () => { 13 | beforeEach(testutils.cleanDatabase); 14 | afterEach(testutils.cleanDatabase); 15 | 16 | it('should do nothing on empty collection', (done) => { 17 | testutils.connect((err, client) => { 18 | let things = client.collection('things'); 19 | things.remove({tag: 'aa'}, (err, result) => { 20 | should.equal(result, 0); 21 | done(); 22 | }); 23 | }); 24 | }); 25 | 26 | it('should remove documents from collection', (done) => { 27 | testutils.connect((err, client) => { 28 | let things = client.collection('things'); 29 | Async.series( 30 | [{tag: 'aa'}, 31 | {tag: 'bb'}, 32 | {tag: 'aa'}].map( 33 | (doc) => 34 | (next) => things.insert(doc, next) 35 | ), 36 | (err, results) => { 37 | things.remove({tag: 'aa'}, (err, result) => { 38 | should.equal(result, 2); 39 | things.count({}, (err, result) => { 40 | should.equal(1, result); 41 | done(); 42 | }); 43 | }); 44 | } 45 | ); 46 | }); 47 | }); 48 | }); 49 | 50 | describe('BedquiltCollection#removeOne()', () => { 51 | beforeEach(testutils.cleanDatabase); 52 | afterEach(testutils.cleanDatabase); 53 | 54 | it('should do nothing on empty collection', (done) => { 55 | testutils.connect((err, client) => { 56 | let things = client.collection('things'); 57 | things.removeOne({tag: 'aa'}, (err, result) => { 58 | should.equal(result, 0); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | 64 | it('should remove documents from collection', (done) => { 65 | testutils.connect((err, client) => { 66 | let things = client.collection('things'); 67 | Async.series( 68 | [{tag: 'aa'}, 69 | {tag: 'bb'}, 70 | {tag: 'aa'}].map( 71 | (doc) => 72 | (next) => things.insert(doc, next) 73 | ), 74 | (err, results) => { 75 | things.removeOne({tag: 'aa'}, (err, result) => { 76 | should.equal(result, 1); 77 | things.count({}, (err, result) => { 78 | should.equal(2, result); 79 | things.count({tag: 'aa'}, (err, result) => { 80 | should.equal(1, result); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | } 86 | ); 87 | }); 88 | }); 89 | }); 90 | 91 | 92 | describe('BedquiltCollection#removeOneById()', () => { 93 | beforeEach(testutils.cleanDatabase); 94 | afterEach(testutils.cleanDatabase); 95 | 96 | it('should do nothing on empty collection', (done) => { 97 | testutils.connect((err, client) => { 98 | let things = client.collection('things'); 99 | things.removeOneById('one', (err, result) => { 100 | should.equal(result, 0); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | 106 | it('should remove documents from collection', (done) => { 107 | testutils.connect((err, client) => { 108 | let things = client.collection('things'); 109 | 110 | Async.series( 111 | [{_id: 'one', tag: 'aa'}, 112 | {_id: 'two', tag: 'bb'}, 113 | {_id: 'three', tag: 'aa'}].map( 114 | (doc) => 115 | (next) => things.insert(doc, next) 116 | ), 117 | (err, results) => { 118 | things.removeOneById('one', (err, result) => { 119 | should.equal(result, 1); 120 | things.count({}, (err, result) => { 121 | should.equal(2, result); 122 | things.count({tag: 'aa'}, (err, result) => { 123 | should.equal(1, result); 124 | done(); 125 | }); 126 | }); 127 | }); 128 | } 129 | ); 130 | }); 131 | }); 132 | }); 133 | 134 | describe('BedquiltCollection#removeManyByIds()', () => { 135 | beforeEach(testutils.cleanDatabase); 136 | afterEach(testutils.cleanDatabase); 137 | 138 | it('should do nothing on empty collection', (done) => { 139 | testutils.connect((err, client) => { 140 | let things = client.collection('things'); 141 | things.removeManyByIds(['one', 'three'], (err, result) => { 142 | should.equal(result, 0); 143 | done(); 144 | }); 145 | }); 146 | }); 147 | 148 | it('should remove documents from collection', (done) => { 149 | testutils.connect((err, client) => { 150 | let things = client.collection('things'); 151 | 152 | Async.series( 153 | [{_id: 'one', tag: 'aa'}, 154 | {_id: 'two', tag: 'bb'}, 155 | {_id: 'three', tag: 'aa'}].map( 156 | (doc) => 157 | (next) => things.insert(doc, next) 158 | ), 159 | (err, results) => { 160 | things.removeManyByIds(['one', 'three'], (err, result) => { 161 | should.equal(err, null); 162 | should.equal(result, 2); 163 | things.count({}, (err, result) => { 164 | should.equal(1, result); 165 | things.count({tag: 'aa'}, (err, result) => { 166 | should.equal(0, result); 167 | done(); 168 | }); 169 | }); 170 | }); 171 | } 172 | ); 173 | }); 174 | }); 175 | }); 176 | 177 | 178 | describe('BedquiltCollection#insert()', () => { 179 | beforeEach(testutils.cleanDatabase); 180 | afterEach(testutils.cleanDatabase); 181 | 182 | it('should return _id of document', (done) => { 183 | testutils.connect((err, client) => { 184 | let things = client.collection('things'); 185 | let doc = { 186 | _id: 'spanner', 187 | description: 'A small spanner' 188 | }; 189 | things.insert(doc, (err, result) => { 190 | should.equal(null, err); 191 | should.equal('spanner', result); 192 | things.count({}, (err, result) => { 193 | should.equal(null, err); 194 | should.equal(1, result); 195 | done(); 196 | }); 197 | }); 198 | }); 199 | }); 200 | }); 201 | 202 | describe('BedquiltCollection#save()', () => { 203 | beforeEach(testutils.cleanDatabase); 204 | afterEach(testutils.cleanDatabase); 205 | 206 | it('should return _id of document', (done) => { 207 | testutils.connect((err, client) => { 208 | let things = client.collection('things'); 209 | let doc = { 210 | _id: 'spanner', 211 | description: 'A small spanner' 212 | }; 213 | things.save(doc, (err, result) => { 214 | should.equal(null, err); 215 | should.equal('spanner', result); 216 | things.count({}, (err, result) => { 217 | should.equal(null, err); 218 | should.equal(1, result); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | }); 224 | 225 | it('should update document in place', (done) => { 226 | testutils.connect((err, client) => { 227 | let things = client.collection('things'); 228 | let doc = { 229 | _id: 'spanner', 230 | description: 'A small spanner' 231 | }; 232 | things.save(doc, (err, result) => { 233 | let _id = result; 234 | things.findOneById(_id, (err, result) => { 235 | should.notEqual(result.tag, 'aaa'); 236 | result.tag = 'aaa'; 237 | things.save(result, (err, result) => { 238 | should.equal(err, null); 239 | should.equal('spanner', result); 240 | things.findOneById(result, function(err, spanner) { 241 | should.equal(spanner.tag, 'aaa'); 242 | done(); 243 | }); 244 | }); 245 | }); 246 | }); 247 | }); 248 | }); 249 | }); 250 | 251 | }); 252 | -------------------------------------------------------------------------------- /docs/api_docs.md: -------------------------------------------------------------------------------- 1 | # API Documentation 2 | 3 | This document describes the `node-bedquilt` api. 4 | See the [BedquiltDB Guide](https://bedquiltdb.readthedocs.org/en/latest/guide) for more comprehensive documentation of the BedquiltDB system. 5 | 6 | ---- 7 | 8 | ## `BedquiltClient` 9 | 10 | A client class which communicates with the PostgreSQL/Bedquilt server. 11 | 12 | ### `#connect` 13 | 14 | Params: `connectionString::String`, `callback::Function` 15 | 16 | A static method used to connect to the Bedquilt server. 17 | The callback takes two parameters `err` and `client`, the latter of which 18 | is an instance of BedquiltCollection. 19 | 20 | Example: 21 | ```javascript 22 | var BedquiltClient = require('bedquilt').BedquiltClient; 23 | 24 | BedquiltClient.connect('postgres://localhost/test', function(err, client) { 25 | var people = client.collection('people'); 26 | people.insert({name: "Sarah", age: 45}, function(err, result) { 27 | console.log('Added Sarah to the people collection with _id: ' + result); 28 | }); 29 | }); 30 | ``` 31 | 32 | ---- 33 | 34 | ### `#collection` 35 | 36 | Params: `collectionName::String` 37 | 38 | Returns: instance of `BedquiltCollection` 39 | 40 | Use to get an instance of `BedquiltCollection`, which can be used to read and write to a collection. 41 | 42 | ---- 43 | 44 | 45 | ### `#listCollections` 46 | 47 | Params: `callback::Function` 48 | 49 | Gets a list of the names of all collections on the database. The callback function should take two parameters, 50 | `err` and `result` where result is an Array of strings. 51 | 52 | --- 53 | 54 | 55 | ### `#createCollection` 56 | 57 | Params: `collectionName::String`, `callback::Function` 58 | 59 | Creates a collection on the database. 60 | The callback function should take two parameters, `err` and `result` where result is a boolean indicating whether 61 | the collection was created. This value will be false if the named collection already existed on the server. 62 | 63 | ---- 64 | 65 | 66 | ### `#deleteCollection` 67 | 68 | Params: `collectionName::String`, `callback::Function` 69 | 70 | Deletes a collection on the database. 71 | The callback function should take two parameters, `err` and `result` where result is a boolean indicating whether 72 | the collection was deleted. This value will be false if the named collection did not exist on the server. 73 | 74 | ---- 75 | 76 | 77 | ## `BedquiltCollection` 78 | 79 | A class representing a connection to a collection on the database server. 80 | 81 | ---- 82 | 83 | 84 | ### `#count` 85 | 86 | Params: `queryDoc::Object`, `callback::Function` 87 | 88 | Get a count of documents in the collection. 89 | The callback function should take two parameters, `err` and `result`, where result is a Number. 90 | 91 | ---- 92 | 93 | 94 | ### `#insert` 95 | 96 | Params: `doc::Object`, `callback::Function` 97 | 98 | Insert a document into the collection. If the document has an `_id` field, then that will be used as the primary key for the document in the collection. If not, then a random string will be generated on the server and set as the `_id` field. The callback function should take two parameters, `err` and `result`, where result is the `_id` value of the inserted document. 99 | 100 | ---- 101 | 102 | 103 | ### `#save` 104 | 105 | Params: `doc::Object`, `callback::Function` 106 | 107 | Save a document to the collection. If the document has an `_id` field, and it matches an existing document 108 | in the collection then the supplied document will replace (overwrite) the existing one. 109 | Othewise, this method behaves like the `insert` method. 110 | The callback function should take two parameters, `err` and `result`, where result is the `_id` field of the saved document. 111 | 112 | ---- 113 | 114 | 115 | ### `#find` 116 | 117 | Params: `queryDoc::Object`, `options::Object (optional)`, `callback::Function` 118 | 119 | Find documents in the collection, matching the supplied query document. 120 | The callback function should take two parameters, `err` and `result`, where result is an Array of Objects. 121 | The options object may contain the following fields: 122 | 123 | - `sort`: Array of sort specs, example: `{sort: [{lastUpdate: 1, name: -1}]` 124 | - `skip`: Number of documents to skip, default 0 125 | - `limit`: Number of documents to limit result set to, default `null` 126 | 127 | The `find` method returns a `BedquiltQuery` object, which is an event emitter, emitting 128 | the following events: 129 | 130 | - `row`: fired when a row is retrieved from the database 131 | - `error`: fired when an error is encountered 132 | - `end`: fired when the query has finished 133 | 134 | This means you can omit the `callback` parameter to find, and subscribe to `row` events 135 | from the returned query object in order to access rows as they are returned from 136 | the database. Example: 137 | 138 | ```javascript 139 | query = things.find({'type': 'tool'}) 140 | query.on('row', (row) => { 141 | console.log('>> got a row'); 142 | console.log(row); // -> {_id: 'abcd', ...'}; 143 | }); 144 | query.on('end', (result) => { 145 | console.log('>> done'); 146 | }); 147 | ``` 148 | 149 | See `BedquiltQuery` below for more details. 150 | 151 | 152 | ---- 153 | 154 | 155 | ### `#findOne` 156 | 157 | Params: `queryDoc::Object`, `options::Object (optional)` `callback::Function` 158 | 159 | Find a single document in the collection, matching the supplied query document. 160 | The callback function should take two parameters, `err` and `result`, where result is an Object. 161 | 162 | The options object may contain the following fields: 163 | 164 | - `sort`: Array of sort specs, example: `{sort: [{lastUpdate: 1, name: -1}]` 165 | - `skip`: Number of documents to skip, default 0 166 | 167 | ---- 168 | 169 | 170 | 171 | ### `#findOneById` 172 | 173 | Params: `id::String`, `callback::Function` 174 | 175 | Find a single document in the collection, which has an `_id` field matching the supplied id string. 176 | The callback function should take two parameters, `err` and `result`, where result is an Object. 177 | 178 | 179 | ### `#findManyByIds` 180 | 181 | Params: `ids::Array[String]`, `callback::Function` 182 | 183 | Find many documents, by their `_id` fields. 184 | The callback function should take two parameters, `err` and `result`, where result is an Array of Objects. 185 | 186 | ---- 187 | 188 | 189 | 190 | ### `#findManyByIds` 191 | 192 | Params: `ids::Array[String]`, `callback::Function` 193 | 194 | Find several documents in the collection, which has an `_id` field in the supplied array of ids. 195 | The callback function should take two parameters, `err` and `result`, where result is an Array of Objects. 196 | 197 | ---- 198 | 199 | 200 | 201 | 202 | ### `#distinct` 203 | 204 | Params: `keyPath::String`, `callback::Function` 205 | 206 | Get a list of the distinct values present in a collection for the specified key. 207 | The keyPath string may be either the name of a top-level key in the collection, 208 | or a dotted path to a nested key. 209 | 210 | Example: 211 | ``` 212 | coll.distinct('address.city', (err, result) => { 213 | console.log(result); // ['Edinburgh', 'London', ...] 214 | }) 215 | ``` 216 | 217 | 218 | 219 | ### `#remove` 220 | 221 | Params: `queryDoc::Object`, `callback::Function` 222 | 223 | Remove documents from the collection, matching the supplied query document. 224 | The callback function should take two parameters, `err` and `result`, where result is a Number indicating 225 | the number of documents removed. 226 | 227 | ---- 228 | 229 | 230 | ### `#removeOne` 231 | 232 | Params: `queryDoc::Object`, `callback::Function` 233 | 234 | Remove a single document from the collection, matching the supplied query document. 235 | The callback function should take two parameters, `err` and `result`, where result is a Number indicating the number of documents removed. 236 | 237 | ---- 238 | 239 | 240 | ### `#removeOneById` 241 | 242 | Params: `id::String`, `callback::Function` 243 | 244 | Remove a single document from the collection, which has an `_id` field matching the supplied id string. 245 | The callback function should take two parameters, `err` and `result`, where result is a Number indicating the number of documents removed, either `0` or `1`. 246 | 247 | ---- 248 | 249 | 250 | ### `#removeManyByIds` 251 | 252 | Params: `ids::Array[String]`, `callback::Function` 253 | 254 | Remove many documents from a collection, by their `_id` fields. 255 | The callback function should take two parameters, `err` and `result`, where result is a Number indicating the number of documents removed, either `0` or `1`. 256 | 257 | 258 | 259 | ## `BedquiltQuery` 260 | 261 | An event-emitter representing a query in progress. Returned from all query operations, 262 | but only useful when returned from the `BedquiltCollection#find` and `BedquiltCollection#findManyByIds` methods. 263 | 264 | ### Events 265 | 266 | #### 'row': (row) 267 | 268 | Emitted when a row is retrieved from the database. Each row is a single json document. 269 | 270 | #### 'error': (error) 271 | 272 | Emitted when an error is encountered 273 | 274 | #### 'end': (result) 275 | 276 | Emitted when all rows have been retrieved. The result is an array of values, equivalent 277 | to the result that would be passed to a query callback. 278 | 279 | Example: 280 | 281 | ```javascript 282 | query = things.find({'type': 'tool'}) 283 | query.on('row', (row) => { 284 | console.log('>> got a row'); 285 | console.log(row); // -> {_id: 'abcd', ...'}; 286 | }); 287 | query.on('end', (result) => { 288 | console.log('>> done'); 289 | }); 290 | ``` 291 | 292 | ---- 293 | -------------------------------------------------------------------------------- /test/test_reads.js: -------------------------------------------------------------------------------- 1 | /*jshint node: true*/ 2 | /*jshint esnext: true*/ 3 | /*global require, describe, it, before, beforeEach, after, afterEach */ 4 | "use strict"; 5 | 6 | let should = require("should"); 7 | let testutils = require('./testutils.js'); 8 | let Async = require('async'); 9 | 10 | 11 | describe('BedquiltCollection find ops', () => { 12 | 13 | describe('BedquiltCollection#findOne() with skip and sort', () => { 14 | beforeEach(testutils.cleanDatabase); 15 | afterEach(testutils.cleanDatabase); 16 | 17 | let populate = (callback) => { 18 | testutils.connect((err, client) => { 19 | let things = client.collection('things'); 20 | Async.series( 21 | [{name: 'sarah', age: 22}, 22 | {name: 'mike', age: 20}, 23 | {name: 'irene', age: 40}, 24 | {name: 'mary', age: 16}, 25 | {name: 'brian', age: 31}, 26 | {name: 'dave', age: 22}, 27 | {name: 'kate', age: 25}, 28 | {name: 'alice', age: 57}].map( 29 | (doc) => 30 | (next) => things.save(doc, next) 31 | ), 32 | (err, results) => 33 | callback() 34 | ); 35 | }); 36 | }; 37 | let names = function(docs) { 38 | return docs.map((doc) => { return doc.name; }); 39 | }; 40 | let ages = function(docs) { 41 | return docs.map((doc) => { return doc.age; }); 42 | }; 43 | 44 | it('should skip no documents', (done) => { 45 | populate(() => { 46 | testutils.connect((err, client) => { 47 | let things = client.collection('things'); 48 | things.findOne({}, {sort: [{age: 1}]}, (err, result) => { 49 | should.equal(err, null); 50 | should.deepEqual(result.name, 'mary'); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | }); 56 | 57 | it('should skip one document', (done) => { 58 | populate(() => { 59 | testutils.connect((err, client) => { 60 | let things = client.collection('things'); 61 | things.findOne({}, {skip: 1, sort: [{age: 1}]}, (err, result) => { 62 | should.equal(err, null); 63 | should.deepEqual(result.name, 'mike'); 64 | done(); 65 | }); 66 | }); 67 | }); 68 | }); 69 | 70 | it('should skip one document, age descending', (done) => { 71 | populate(() => { 72 | testutils.connect((err, client) => { 73 | let things = client.collection('things'); 74 | things.findOne({}, {skip: 1, sort: [{age: -1}]}, (err, result) => { 75 | should.equal(err, null); 76 | should.deepEqual(result.name, 'irene'); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | }); 82 | 83 | it('should skip four documents', (done) => { 84 | populate(() => { 85 | testutils.connect((err, client) => { 86 | let things = client.collection('things'); 87 | things.findOne({}, {skip: 4, sort: [{age: 1}]}, (err, result) => { 88 | should.equal(err, null); 89 | should.deepEqual(result.name, 'kate'); 90 | done(); 91 | }); 92 | }); 93 | }); 94 | }); 95 | 96 | }); 97 | 98 | describe('BedquiltCollection#find() with skip, limit and sort', () => { 99 | beforeEach(testutils.cleanDatabase); 100 | afterEach(testutils.cleanDatabase); 101 | 102 | let populate = (callback) => { 103 | testutils.connect((err, client) => { 104 | let things = client.collection('things'); 105 | Async.series( 106 | [{name: 'sarah', age: 22}, 107 | {name: 'mike', age: 20}, 108 | {name: 'irene', age: 40}, 109 | {name: 'mary', age: 16}, 110 | {name: 'brian', age: 31}, 111 | {name: 'dave', age: 22}, 112 | {name: 'kate', age: 25}, 113 | {name: 'alice', age: 57}].map( 114 | (doc) => 115 | (next) => things.save(doc, next) 116 | ), 117 | (err, results) => 118 | callback() 119 | ); 120 | }); 121 | }; 122 | let names = function(docs) { 123 | return docs.map((doc) => { return doc.name; }); 124 | }; 125 | let ages = function(docs) { 126 | return docs.map((doc) => { return doc.age; }); 127 | }; 128 | 129 | describe('$created and $updated sorts', () => { 130 | it('should sort by $created', (done) => { 131 | populate(() => { 132 | testutils.connect((err, client) => { 133 | let things = client.collection('things'); 134 | things.find({}, {limit: 2, sort: [{'$created': -1}]}, (err, result) => { 135 | should.equal(err, null); 136 | should.deepEqual(names(result), ['alice', 'kate']); 137 | things.find({}, {limit: 2, sort: [{'$created': 1}]}, (err, result) => { 138 | should.equal(err, null); 139 | should.deepEqual(names(result), ['sarah', 'mike']); 140 | done(); 141 | }); 142 | }); 143 | }); 144 | }); 145 | }); 146 | 147 | it('should sort by $updated', (done) => { 148 | populate(() => { 149 | testutils.connect((err, client) => { 150 | let things = client.collection('things'); 151 | things.find({}, {limit: 2, sort: [{'$updated': -1}]}, (err, result) => { 152 | should.equal(err, null); 153 | should.deepEqual(names(result), ['alice', 'kate']); 154 | things.find({}, {limit: 2, sort: [{'$updated': 1}]}, (err, result) => { 155 | should.equal(err, null); 156 | should.deepEqual(names(result), ['sarah', 'mike']); 157 | done(); 158 | }); 159 | }); 160 | }); 161 | }); 162 | }); 163 | }); 164 | 165 | it('should skip two documents', (done) => { 166 | populate(() => { 167 | testutils.connect((err, client) => { 168 | let things = client.collection('things'); 169 | things.find({}, {skip: 2}, (err, result) => { 170 | should.equal(err, null); 171 | should.equal(result.length, 6); 172 | should.deepEqual(names(result), ['irene', 'mary', 'brian', 173 | 'dave', 'kate', 'alice']); 174 | done(); 175 | }); 176 | }); 177 | }); 178 | }); 179 | 180 | it('should skip two documents and limit to three', (done) => { 181 | populate(() => { 182 | testutils.connect((err, client) => { 183 | let things = client.collection('things'); 184 | things.find({}, {skip: 2, limit: 3}, (err, result) => { 185 | should.equal(err, null); 186 | should.equal(result.length, 3); 187 | should.deepEqual(names(result), ['irene', 'mary', 'brian']); 188 | done(); 189 | }); 190 | }); 191 | }); 192 | }); 193 | 194 | it('should limit to four documents', (done) => { 195 | populate(() => { 196 | testutils.connect((err, client) => { 197 | let things = client.collection('things'); 198 | things.find({}, {limit: 4}, (err, result) => { 199 | should.equal(err, null); 200 | should.equal(result.length, 4); 201 | should.deepEqual(names(result), ['sarah', 'mike', 'irene', 'mary']); 202 | done(); 203 | }); 204 | }); 205 | }); 206 | }); 207 | 208 | describe('with sort', () => { 209 | 210 | it('should order correctly ascending', (done) => { 211 | populate(() => { 212 | testutils.connect((err, client) => { 213 | let things = client.collection('things'); 214 | things.find({}, {sort: [{'age': 1}]}, (err, result) => { 215 | should.equal(err, null); 216 | should.equal(result.length, 8); 217 | let a = ages(result); 218 | let sorted = a.slice().sort(); 219 | should.deepEqual(a, sorted); 220 | done(); 221 | }); 222 | }); 223 | }); 224 | }); 225 | 226 | it('should order correctly ascending', (done) => { 227 | populate(() => { 228 | testutils.connect((err, client) => { 229 | let things = client.collection('things'); 230 | things.find({}, {sort: [{'age': -1}]}, (err, result) => { 231 | should.equal(err, null); 232 | should.equal(result.length, 8); 233 | let a = ages(result); 234 | let sorted = a.slice().sort().reverse(); 235 | should.deepEqual(a, sorted); 236 | done(); 237 | }); 238 | }); 239 | }); 240 | }); 241 | 242 | it('should order correctly ascending with skip and limit', (done) => { 243 | populate(() => { 244 | testutils.connect((err, client) => { 245 | let things = client.collection('things'); 246 | let opts = {skip: 2, limit: 3, sort: [{age: 1}]}; 247 | things.find({}, opts, (err, result) => { 248 | should.equal(err, null); 249 | should.equal(result.length, 3); 250 | should.deepEqual(ages(result), [22, 22, 25]); 251 | should.deepEqual(names(result), ['sarah', 'dave', 'kate']); 252 | done(); 253 | }); 254 | }); 255 | }); 256 | }); 257 | 258 | it('should order correctly descending with skip and limit', (done) => { 259 | populate(() => { 260 | testutils.connect((err, client) => { 261 | let things = client.collection('things'); 262 | let opts = {skip: 2, limit: 3, sort: [{age: -1}]}; 263 | things.find({}, opts, (err, result) => { 264 | should.equal(err, null); 265 | should.equal(result.length, 3); 266 | should.deepEqual(ages(result), [31, 25, 22]); 267 | should.deepEqual(names(result), ['brian', 'kate', 'sarah']); 268 | done(); 269 | }); 270 | }); 271 | }); 272 | }); 273 | 274 | // TODO: multisort 275 | it('should order by age and then by name', (done) => { 276 | populate(() => { 277 | testutils.connect((err, client) => { 278 | let things = client.collection('things'); 279 | let opts = {limit: 5, sort: [{age: 1}, {name: 1}]}; 280 | things.find({}, opts, (err, result) => { 281 | should.equal(err, null); 282 | should.equal(result.length, 5); 283 | should.deepEqual(ages(result), [16, 20, 22, 22, 25]); 284 | should.deepEqual( 285 | names(result), 286 | ['mary', 287 | 'mike', 288 | 'dave', 289 | 'sarah', 290 | 'kate']); 291 | done(); 292 | }); 293 | }); 294 | }); 295 | }); 296 | 297 | it('should order by age and then by name descending', (done) => { 298 | populate(() => { 299 | testutils.connect((err, client) => { 300 | let things = client.collection('things'); 301 | let opts = {limit: 5, sort: [{age: 1}, {name: -1}]}; 302 | things.find({}, opts, (err, result) => { 303 | should.equal(err, null); 304 | should.equal(result.length, 5); 305 | should.deepEqual(ages(result), [16, 20, 22, 22, 25]); 306 | should.deepEqual( 307 | names(result), 308 | ['mary', 309 | 'mike', 310 | 'sarah', 311 | 'dave', 312 | 'kate'] 313 | ); 314 | done(); 315 | }); 316 | }); 317 | }); 318 | }); 319 | 320 | }); 321 | }); 322 | 323 | describe('BedquiltCollection#findOneById()', () => { 324 | beforeEach(testutils.cleanDatabase); 325 | afterEach(testutils.cleanDatabase); 326 | let populate = (callback) => { 327 | testutils.connect((err, client) => { 328 | let things = client.collection('things'); 329 | Async.series( 330 | [{_id: 'sarah', age: 22}, 331 | {_id: 'mike', age: 20}, 332 | {_id: 'irene', age: 40}].map( 333 | (doc) => 334 | (next) => things.save(doc, next) 335 | ), 336 | (err, results) => callback() 337 | ); 338 | }); 339 | }; 340 | 341 | it('should return null when no match', (done) => { 342 | populate(() => { 343 | testutils.connect((err, client) => { 344 | let things = client.collection('things'); 345 | things.findOneById('nope', (err, doc) => { 346 | should.equal(err, null); 347 | should.equal(doc, null); 348 | done(); 349 | }); 350 | }); 351 | }); 352 | }); 353 | 354 | it('should return the right doc', (done) => { 355 | populate(() => { 356 | testutils.connect((err, client) => { 357 | let things = client.collection('things'); 358 | things.findOneById('mike', (err, doc) => { 359 | should.equal(err, null); 360 | should.deepEqual(doc, {_id: 'mike', age: 20}); 361 | done(); 362 | }); 363 | }); 364 | }); 365 | }); 366 | }); 367 | 368 | describe('BedquiltCollection#findOne()', () => { 369 | beforeEach(testutils.cleanDatabase); 370 | afterEach(testutils.cleanDatabase); 371 | let populate = (callback) => { 372 | testutils.connect((err, client) => { 373 | let things = client.collection('things'); 374 | Async.series( 375 | [{_id: 'sarah', age: 22}, 376 | {_id: "mike o'reilly", age: 20}, 377 | {_id: 'irene', age: 40}].map( 378 | (doc) => 379 | (next) => things.save(doc, next) 380 | ), 381 | (err, results) => callback() 382 | ); 383 | }); 384 | }; 385 | 386 | it('should return null when no match', (done) => { 387 | populate(() => { 388 | testutils.connect((err, client) => { 389 | let things = client.collection('things'); 390 | things.findOne({age: 99}, (err, doc) => { 391 | should.equal(err, null); 392 | should.equal(doc, null); 393 | done(); 394 | }); 395 | }); 396 | }); 397 | }); 398 | 399 | it('should return the right doc', (done) => { 400 | populate(() => { 401 | testutils.connect((err, client) => { 402 | let things = client.collection('things'); 403 | things.findOne({age: 20}, (err, doc) => { 404 | should.equal(err, null); 405 | should.deepEqual(doc, {_id: "mike o'reilly", age: 20}); 406 | done(); 407 | }); 408 | }); 409 | }); 410 | }); 411 | 412 | it('should be ok with quotes', (done) => { 413 | populate(() => { 414 | testutils.connect((err, client) => { 415 | let things = client.collection('things'); 416 | things.findOne({_id: "mike o'reilly"}, (err, doc) => { 417 | should.equal(err, null); 418 | should.deepEqual(doc, {_id: "mike o'reilly", age: 20}); 419 | done(); 420 | }); 421 | }); 422 | }); 423 | }); 424 | }); 425 | 426 | describe('BedquiltCollection#findManyByIds()', () => { 427 | beforeEach(testutils.cleanDatabase); 428 | afterEach(testutils.cleanDatabase); 429 | let populate = (callback) => { 430 | testutils.connect((err, client) => { 431 | let things = client.collection('things'); 432 | Async.series( 433 | [{_id: 'sarah', age: 22}, 434 | {_id: 'mike', age: 20}, 435 | {_id: 'irene', age: 40}, 436 | {_id: 'mary', age: 16}, 437 | {_id: 'brian', age: 31}, 438 | {_id: 'dave', age: 22}, 439 | {_id: 'kate', age: 25}, 440 | {_id: 'alice', age: 57}].map( 441 | (doc) => 442 | (next) => things.save(doc, next) 443 | ), 444 | (err, results) => 445 | callback() 446 | ); 447 | }); 448 | }; 449 | 450 | it('should return empty results when no match', (done) => { 451 | populate(() => { 452 | testutils.connect((err, client) => { 453 | let things = client.collection('things'); 454 | things.findManyByIds(['bad-one', 'bad-two'], (err, docs) => { 455 | should.equal(err, null); 456 | should.deepEqual(docs, []); 457 | done(); 458 | }); 459 | }); 460 | }); 461 | }); 462 | 463 | it('should return the right docs', (done) => { 464 | populate(() => { 465 | testutils.connect((err, client) => { 466 | let things = client.collection('things'); 467 | let ids = ['bad-one', 'kate', 'mary', 'alice']; 468 | things.findManyByIds(ids, (err, docs) => { 469 | should.equal(err, null); 470 | should.deepEqual( 471 | docs.map((doc) => doc._id), 472 | ['mary', 'kate', 'alice'] 473 | ); 474 | done(); 475 | }); 476 | }); 477 | }); 478 | }); 479 | }); 480 | 481 | describe('BedquiltCollection#count()', () => { 482 | beforeEach(testutils.cleanDatabase); 483 | afterEach(testutils.cleanDatabase); 484 | 485 | it('should return zero on non-existant collection', (done) => { 486 | testutils.connect((err, client) => { 487 | let things = client.collection('things'); 488 | things.count({}, (err, result) => { 489 | should.equal(err, null); 490 | should.equal(result, 0); 491 | done(); 492 | }); 493 | }); 494 | }); 495 | 496 | it('sould return count of documents in collection', (done) => { 497 | testutils.connect((err, client) => { 498 | let things = client.collection('things'); 499 | Async.series( 500 | [{}, 501 | {}, 502 | {} 503 | ].map((doc) => (next) => things.save(doc, next)), 504 | function(err, results) { 505 | things.count({}, (err, result) => { 506 | should.equal(3, result); 507 | done(); 508 | }); 509 | } 510 | ); 511 | }); 512 | }); 513 | }); 514 | 515 | describe('BedquiltCollection#distinct()', () => { 516 | beforeEach(testutils.cleanDatabase); 517 | afterEach(testutils.cleanDatabase); 518 | 519 | it('should return empty list for non-existant collection', (done) => { 520 | testutils.connect((err, client) => { 521 | let things = client.collection('things'); 522 | things.distinct('name', (err, result) => { 523 | should.equal(err, null); 524 | should.deepEqual(result, []); 525 | done(); 526 | }); 527 | }); 528 | }); 529 | 530 | it('sould return distinct values', (done) => { 531 | testutils.connect((err, client) => { 532 | let things = client.collection('things'); 533 | Async.series( 534 | [ 535 | {name: 'aa'}, 536 | {name: 'bb'}, 537 | {name: 'aa'} 538 | ].map((doc) => (next) => things.save(doc, next)), 539 | function(err, results) { 540 | things.distinct('name', (err, result) => { 541 | should.equal(err, null); 542 | should.deepEqual(['aa', 'bb'], result.sort()); 543 | done(); 544 | }); 545 | } 546 | ); 547 | }); 548 | }); 549 | }); 550 | 551 | describe('BedquiltCollection#find()', () => { 552 | beforeEach(testutils.cleanDatabase); 553 | afterEach(testutils.cleanDatabase); 554 | 555 | describe('on non-existant collection', () => { 556 | it('should return empty list', (done) => { 557 | testutils.connect((err, client) => { 558 | let things = client.collection('things'); 559 | things.find({}, (err, result) => { 560 | should.equal(err, null); 561 | should.equal(0, result.length); 562 | done(); 563 | }); 564 | }); 565 | }); 566 | }); 567 | 568 | describe('on empty collection', () => { 569 | it('should return empty list', (done) => { 570 | testutils.connect((err, client) => { 571 | let things = client.collection('things'); 572 | things.find({}, (err, result) => { 573 | should.equal(err, null); 574 | should.equal(0, result.length); 575 | done(); 576 | }); 577 | }); 578 | }); 579 | }); 580 | 581 | describe('with docs', () => { 582 | beforeEach((done) => { 583 | testutils.cleanDatabase((err, result) => { 584 | if(err) { 585 | throw err; 586 | } 587 | testutils.connect((err, client) => { 588 | let things = client.collection('things'); 589 | Async.series( 590 | [{_id: 'one', tag: 'aa'}, 591 | {_id: 'two', tag: 'bb'}, 592 | {_id: 'three', tag: 'cc'}, 593 | {_id: 'four', tag: 'dd'}, 594 | {_id: 'five', tag: 'aa'}].map( 595 | (doc) => 596 | (next) => things.save(doc, next) 597 | ), 598 | (err, results) => 599 | done() 600 | ); 601 | }); 602 | }); 603 | }); 604 | 605 | it('should return a query object', (done) => { 606 | testutils.connect((err, client) => { 607 | let things = client.collection('things'); 608 | let query = things.find({}); 609 | let counter = 0; 610 | query.on('row', (row) => { 611 | counter++; 612 | }); 613 | query.on('end', (result) => { 614 | should.equal(counter, 5); 615 | should.equal(result.length, 5); 616 | done(); 617 | }); 618 | }); 619 | }); 620 | 621 | it('should return entire collection', (done) => { 622 | testutils.connect((err, client) => { 623 | let things = client.collection('things'); 624 | things.find({}, (err, result) => { 625 | should.equal(err, null); 626 | should.equal(5, result.length); 627 | should.deepEqual({_id: 'one', tag: 'aa'}, result[0]); 628 | done(); 629 | }); 630 | }); 631 | }); 632 | 633 | it('should return one document when matching _id', (done) => { 634 | testutils.connect((err, client) => { 635 | let things =client.collection('things'); 636 | things.find({_id: 'two'}, (err, result) => { 637 | should.equal(err, null); 638 | should.equal(result.length, 1); 639 | should.deepEqual(result[0], {_id: 'two', tag: 'bb'}); 640 | done(); 641 | }); 642 | }); 643 | }); 644 | 645 | it('should return two documents when they match', (done) => { 646 | testutils.connect((err, client) => { 647 | let things = client.collection('things'); 648 | things.find({tag: 'aa'}, (err, result) => { 649 | should.equal(err, null); 650 | should.equal(result.length, 2); 651 | should.deepEqual([ 652 | {_id: 'one', tag: 'aa'}, 653 | {_id: 'five', tag: 'aa'} 654 | ], result); 655 | done(); 656 | }); 657 | }); 658 | }); 659 | 660 | describe('with advanced queries', () => { 661 | it('should get the right docs', (done) => { 662 | testutils.connect((err, client) => { 663 | let things = client.collection('things'); 664 | things.find({tag: {$eq: 'aa'}}, (err, result) => { 665 | should.equal(err, null); 666 | should.equal(result.length, 2); 667 | should.deepEqual([ 668 | {_id: 'one', tag: 'aa'}, 669 | {_id: 'five', tag: 'aa'} 670 | ], result); 671 | done(); 672 | }); 673 | }); 674 | }); 675 | }); 676 | 677 | describe('with skip and limit', () => { 678 | it('should return the right docs', (done) => { 679 | testutils.connect((err, client) => { 680 | let things = client.collection('things'); 681 | things.find({}, {skip: 1, limit: 2}, (err, result) => { 682 | should.equal(err, null); 683 | should.equal(result.length, 2); 684 | should.deepEqual( 685 | result, 686 | [{_id: 'two', tag: 'bb'}, 687 | {_id: 'three', tag: 'cc'}] 688 | ); 689 | done(); 690 | }); 691 | }); 692 | }); 693 | }); 694 | 695 | }); 696 | }); 697 | 698 | }); 699 | --------------------------------------------------------------------------------