├── .gitignore ├── Makefile ├── README.md ├── docs ├── chain_parse.txt ├── databases.txt ├── index.html ├── intro.md ├── notes.txt └── scraps.txt ├── index.js ├── lib ├── chain.js ├── drivers │ ├── couchdb.js │ ├── memory.js │ ├── mongodb.js │ └── mysql.js ├── select.js ├── selector_parser.js └── states.js ├── package.json └── test ├── core_test.js ├── memory_test.js ├── mongodb_test.js └── mysql_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | .DS_Store 4 | node_modules/* 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test docs 2 | 3 | test: 4 | @node_modules/whiskey/bin/whiskey --tests "test/memory_test.js test/mongodb_test.js test/mysql_test.js test/core_test.js" 5 | 6 | testmongo: 7 | @node_modules/whiskey/bin/whiskey --tests "test/mongodb_test.js" --verbosity 2 8 | 9 | testmysql: 10 | @node_modules/whiskey/bin/whiskey --tests "test/mysql_test.js" --verbosity 2 11 | 12 | testmemory: 13 | @node_modules/whiskey/bin/whiskey --tests "test/memory_test.js" --verbosity 2 14 | 15 | docs: 16 | @dox --title select lib/select.js -i docs/intro.md > docs/index.html 17 | 18 | .PHONY: test docs 19 | .SILENT: docs 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # select 2 | 3 | `select` is a database library. 4 | 5 | Supported databases: 6 | 7 | * MySQL 8 | * MongoDB 9 | * Memory 10 | * CouchDB (partial) 11 | * Planned: PostgreSQL, SQLite 12 | * Researching: Riak, Redis, selector language 13 | 14 | ## Installation 15 | 16 | (Note that this module is not supported, and the old npm repository has been transferred to npm user zenorocha) 17 | 18 | ## Examples 19 | 20 | // Set the database connection string 21 | select.db = 'mysql://root@localhost/select-test'; 22 | 23 | // Find some items and iterate 24 | select('users') 25 | .find({ name: 'Alex' }) 26 | .limit(3) 27 | .offset(8) 28 | .each(function(index) { 29 | console.log(this); 30 | }); 31 | 32 | // Update an attribute 33 | select('users') 34 | .find(17) 35 | .attr({ name: 'Yuka' }); 36 | 37 | // Create 38 | select('users') 39 | .add({ 40 | name: 'Bob', 41 | email: 'bob@example.com' 42 | }); 43 | 44 | // Delete 45 | select('users') 46 | .find(1) 47 | .del(); 48 | 49 | // Selector language 50 | select('users[name="Alex", email="alex@example.com"]'). 51 | values(function(values) {}); 52 | 53 | ## Philosophy 54 | 55 | `select` is: 56 | 57 | * A quick way of using common database features 58 | * Easy to learn for JavaScript developers 59 | * Easy to extend 60 | 61 | `select` is not: 62 | 63 | * For building schemas 64 | * An ORM 65 | * A validation library 66 | 67 | ## License 68 | 69 | (The MIT License) 70 | 71 | Copyright (c) 2011 Alex R. Young 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining 74 | a copy of this software and associated documentation files (the 75 | 'Software'), to deal in the Software without restriction, including 76 | without limitation the rights to use, copy, modify, merge, publish, 77 | distribute, sublicense, and/or sell copies of the Software, and to 78 | permit persons to whom the Software is furnished to do so, subject to 79 | the following conditions: 80 | 81 | The above copyright notice and this permission notice shall be 82 | included in all copies or substantial portions of the Software. 83 | 84 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 87 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 88 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 89 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 90 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 91 | 92 | -------------------------------------------------------------------------------- /docs/chain_parse.txt: -------------------------------------------------------------------------------- 1 | select('collection'). 2 | find(params). 3 | count(100). 4 | offset(10). 5 | (attr|del|each) => 6 | 7 | 8 | attr/del/each = execute (initiates chain parsing, then evaluates) 9 | 10 | stack => 11 | find params | 12 | count | 13 | offset | 14 | del | 15 | 16 | execution expression: del 17 | finder: params + count + offset 18 | 19 | evaluates => bulk delete for params, count offset 20 | 21 | driver: bulkDelete: 22 | SQL: DELETE FROM collection WHERE params count offset 23 | mongodb: delete based on values 24 | 25 | -------------------------------------------------------------------------------- /docs/databases.txt: -------------------------------------------------------------------------------- 1 | COUCHDB 2 | 3 | * https://github.com/creationix/couch-client 4 | * Difficulty: high 5 | * select param: design/view name 6 | * TODO: is convienient searching on a key=value possible? 7 | 8 | MONGODB 9 | 10 | * https://github.com/christkv/node-mongodb-native 11 | * Difficulty: low 12 | * select param: selector language 13 | * TODO: find all, find int/string, offset, raw 14 | 15 | MYSQL 16 | 17 | * https://github.com/felixge/node-mysql 18 | * Difficulty: low 19 | * select param: selector language 20 | * TODO: tests 21 | 22 | POSTGRES 23 | 24 | * https://github.com/brianc/node-postgres 25 | 26 | REDIS 27 | 28 | RIAK 29 | 30 | SQLITE 31 | 32 | * https://github.com/pkrumins/node-sqlite 33 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | select 4 | 5 | 99 | 109 | 110 | 111 | 164 | 167 | 175 | 176 | 177 | 184 | 189 | 190 | 191 | 199 | 202 | 203 | 204 | 208 | 211 | 212 | 213 | 223 | 287 | 288 | 289 | 299 | 308 | 309 | 310 | 321 | 343 | 344 | 345 | 368 | 406 | 407 | 408 | 424 | 437 | 438 | 439 | 447 | 453 | 454 | 455 | 463 | 469 | 470 | 471 | 484 | 490 | 491 | 492 | 505 | 511 | 512 | 513 | 526 | 535 | 536 | 537 | 541 | 548 | 549 | 550 | 557 | 576 | 577 |

select

select is a database library.

112 | 113 |

Installation

114 | 115 |
  $ npm install select
116 | 117 |

Examples

118 | 119 |
  // Find some items and iterate
120 |   select('users')
121 |     .find({ name: 'Alex' })
122 |     .limit(3)
123 |     .offset(8)
124 |     .each(function(index) {
125 |       console.log(this);
126 |     });
127 | 
128 |   // Update an attribute
129 |   select('users')
130 |     .find(17)
131 |     .attr({ name: 'Yuka' });
132 | 
133 |   // Create
134 |   select('users')
135 |     .add({
136 |       name: 'Bob',
137 |       email: 'bob@example.com'
138 |     });
139 | 
140 |   // Delete
141 |   select('users')
142 |     .find(1)
143 |     .del();
144 | 
145 |   // Selector language
146 |   select('users[name="Alex", email="alex@example.com"]').
147 |     values(function(values) {});
148 | 149 |

Philosophy

150 | 151 |

select is:

152 | 153 |
  • A quick way of using common database features
  • Easy to learn for JavaScript developers
  • Easy to extend
154 | 155 |

select is not:

156 | 157 |
  • Concerned with schema
  • An ORM
  • A validation library
158 | 159 |

License

160 | 161 |

The MIT License

162 | 163 |

Copyright (c) 2011 Alex R. Young

select

lib/select.js
165 |

166 |
168 |
var path = require('path'),
169 |     Chain = require(path.join(__dirname, 'chain')),
170 |     states = require(path.join(__dirname, 'states')),
171 |     parser = require(path.join(__dirname, 'selector_parser')),
172 |     EventEmitter = require('events').EventEmitter,
173 |     events = new EventEmitter();
174 |
178 |

The main select() function. Use select('selector') with a collection or table name to start a chain.

179 | 180 |

181 | 182 |
  • param: String selector A selector. For example, 'users', 'users[name="Alex"]'

  • returns: Object A select object that can be subsequently called with database operations

183 |
185 |
select = function(selector) {
186 |   return new select.fn.init(selector, select.db);
187 | };
188 |
192 |

Set this with a database connection URI. Examples:

193 | 194 |
 select.db = 'mysql://root@localhost/select-test';
195 |  select.db = 'mongodb://localhost/select-test';
196 | 197 |

198 |
200 |
select.db = null;
201 |
205 |

Use this to change the default primary key. 206 |

207 |
209 |
select.primaryKey = null;
210 |
214 |

Access the underlying database library. For example, with MySQL:

215 | 216 |
  select().raw('SELECT * FROM users',
217 |                function(err, results) {});
218 | 219 |

220 | 221 |
  • param: Object | String A query suitable for the underlying database API

  • returns: Object A select object

222 |
224 |
select.raw = function() {
225 |   var s = new select.fn.init('', select.db);
226 |   s.raw.apply(s, arguments);
227 |   return s;
228 | };
229 | 
230 | select.on = function(name, fn) {
231 |   events.on(name, fn);
232 | };
233 | 
234 | select.emit = function(name, param) {
235 |   events.emit(name, param);
236 | }
237 | 
238 | select.states = states;
239 | 
240 | select.fn = select.prototype = {
241 |   constructor: select,
242 | 
243 |   init: function(selector, db) {
244 |     var s = parser.parse(selector);
245 |     this.collection = s.collection;
246 |     this.length = 0;
247 |     this.chain = new Chain(this);
248 |     this.db = db;
249 |     this.applyParsedTokens(s);
250 |     return this;
251 |   },
252 | 
253 |   applyParsedTokens: function(s) {
254 |     if (s.find) {
255 |       this.find(s.find);
256 |     }
257 |   },
258 | 
259 |   emit: function(name, param) {
260 |     select.emit(name, param);
261 |   },
262 | 
263 |   primaryKey: select.primaryKey,
264 | 
265 |   defaultPrimaryKeys: {
266 |     mysql:   'id',
267 |     mongodb: '_id',
268 |     memory:  'id',
269 |     couchdb: 'id'   // TODO
270 |   },
271 | 
272 |   setValues: function(values) {
273 |     this.length = values.length;
274 |     for (var i = 0; i < values.length; i++) {
275 |       this[i] = values[i];
276 |     }
277 |   },
278 | 
279 |   toArray: function() {
280 |     var values = [];
281 |     for (var i = 0; i < this.length; i++) {
282 |       values.push(this[i]);
283 |     }
284 |     return values;
285 |   },
286 |
290 |

Add a new record to the database.

291 | 292 |
 select('users').
293 |    add({ name: 'Bill Adama' });
294 | 295 |

296 | 297 |
  • param: Object record An object containing values for a new record in the collection.

  • param: Function fn An optional callback

  • returns: Object The current select object

298 |
300 |
add: function(record, fn) {
301 |     var self = this;
302 |     this.chain.push(states.write, function(client, next) {
303 |       client.add(self.collection, record, next(fn));
304 |     });
305 |     return this;
306 |   },
307 |
311 |

Modify attributes.

312 | 313 |
 select('users').
314 |    find(1).
315 |    attr({ name: 'William Adama' });
316 | 317 |

318 | 319 |
  • param: Object record An object containing values for a new record in the collection.

  • param: Function fn An optional callback

  • returns: Object The current select object

320 |
322 |
attr: function(record, fn) {
323 |     var self = this;
324 |     this.chain.push(states.write | states.update, function(client, next) {
325 |       // TODO: what about updating sets of records based on queries?
326 |       var finishedCallbacks = self.length;
327 |       function done() {
328 |         finishedCallbacks--;
329 |         if (finishedCallbacks == 0) {
330 |           if (fn) fn();
331 |         }
332 |       }
333 | 
334 |       for (var i = 0; i < self.length; i++) {
335 |         client.update(self.collection, self[i], record, done);
336 |       }
337 | 
338 |       if (self.length === 0 && fn) fn();
339 |     });
340 |     return this;
341 |   },
342 |
346 |

Delete the current set of values, or pass a delete query to bulk delete.

347 | 348 |

Delete all:

349 | 350 |
 select('users').del();
351 | 352 |

Delete by ID:

353 | 354 |
 select('users').del(1);
355 | 356 |

Delete with a query:

357 | 358 |
 select('users').del({ name: 'William Adama' });
359 | 360 |

Delete current set of values:

361 | 362 |
 select('users').find({ type: 'trial' }).del();
363 | 364 |

365 | 366 |
  • returns: Object The current select object

367 |
369 |
del: function() {
370 |     // FIXME: Value delete is causing problems, potentially just in mongodb
371 |     var self = this,
372 |         fn,
373 |         query = null;
374 | 
375 |     if (arguments.length) {
376 |       for (var i = 0; i < arguments.length; i++) {
377 |         if (typeof arguments[i] === 'function') {
378 |           fn = arguments[i];
379 |         } else if (typeof arguments[i] === 'object') {
380 |           query = arguments[i];
381 |         } else if (typeof arguments[i] === 'string' || typeof arguments[i] === 'number') {
382 |           query = arguments[i];
383 |         }
384 |       }
385 |     }
386 | 
387 |     if (this.chain.stack.length === 0 && !query) {
388 |       this.chain.push(states.write | states.update, function(client, next) {
389 |         client.remove(self.collection, fn);
390 |       });
391 |     } else if (query) {
392 |       this.find(query);
393 |       this.del(fn);
394 |     } else {
395 |       this.chain.push(states.write | states.update, function(client, next) {
396 |         for (var i = 0; i < self.length; i++) {
397 |           client.remove(self.collection, self[i], fn);
398 |         }
399 |         if (self.length === 0) client.close();
400 |         next(fn);
401 |       });
402 |     }
403 |     return this;
404 |   },
405 |
409 |

Find records.

410 | 411 |

Find based on ID: 412 | select('users'). 413 | find(1, function(err, values) {});

414 | 415 |

Find based on attributes: 416 | select('users'). 417 | find({ type: 'admin' }). 418 | each(function() { console.log(this); });

419 | 420 |

421 | 422 |
  • returns: Object The current select object

423 |
425 |
find: function(options, fn) {
426 |     var self = this;
427 |     this.chain.push(fn ? (states.read | states.exec | states.once) : states.read, function(client, next) {
428 |       self.setValues([]);
429 |       client.find(self.collection, options, self.chain.readOptions, next(function(err, values) {
430 |         self.setValues(values);
431 |         if (fn) fn(err, values);
432 |       }));
433 |     });
434 |     return this;
435 |   },
436 |
440 |

Limit the next database find query. 441 | ## TODO del/attr

442 | 443 |

444 | 445 |
  • param: Number limit Limits the next find() by this amount

  • returns: Object The current select object

446 |
448 |
limit: function(limit) {
449 |     this.chain.push(states.read | states.modify, { limit: limit });
450 |     return this;
451 |   },
452 |
456 |

Offset the next database find query. 457 | ## TODO del/attr

458 | 459 |

460 | 461 |
  • param: Number offset Offset the next find() by this amount

  • returns: Object The current select object

462 |
464 |
offset: function(offset) {
465 |     this.chain.push(states.read | states.modify, { offset: offset });
466 |     return this;
467 |   },
468 |
472 |

Sorts the results of the next find. 473 | ## TODO del/attr

474 | 475 |
 select('users').
476 |    find().
477 |    sort('name').
478 |    each(function() { console.log(this); });
479 | 480 |

481 | 482 |
  • returns: Object The current select object

483 |
485 |
sort: function() {
486 |     this.chain.push(states.read | states.modify, { sort: arguments });
487 |     return this;
488 |   },
489 |
493 |

Causes the previous find() to run, and iterates over the results. 494 | In the passed in callback, this will refer to each value.

495 | 496 |
 select('users').
497 |    find().
498 |    sort('name').
499 |    each(function() { console.log(this); });
500 | 501 |

502 | 503 |
  • param: Function fn A callback that will be run for each value

  • returns: Object The current select object

504 |
506 |
each: function(fn) {
507 |     this.chain.push(states.read | states.exec, fn);
508 |     return this;
509 |   },
510 |
514 |

Receives all of the values for the previous operations. 515 | When a query produces an empty set, each won't run, but values would.

516 | 517 |
 select('users').
518 |    find().
519 |    sort('name').
520 |    values(function(err, values) { console.log(values); });
521 | 522 |

523 | 524 |
  • param: Function fn A callback that gets err and values

  • returns: Object The current select object

525 |
527 |
values: function(fn) {
528 |     var self = this;
529 |     this.chain.push(states.exec, function() {
530 |       if (fn) fn(self.toArray());
531 |     }); 
532 |     return this;
533 |   },
534 |
538 |

Causes the current chain to execute. 539 |

540 |
542 |
end: function(fn) {
543 |     this.chain.push(states.read | states.exec, fn);
544 |     // TODO: Is this right?
545 |     return select(this.collection);
546 |   },
547 |
551 |

This adds a callback to run once other database operations have finished.

552 | 553 |

554 | 555 |
  • param: Function fn The callback

  • returns: Object The current select object

556 |
558 |
after: function(fn) {
559 |     this.chain.push(states.after, fn);
560 |     return this;
561 |   },
562 | 
563 |   raw: function(query, fn) {
564 |     var self = this;
565 |     this.chain.push(states.write | states.update, function(client, next) {
566 |       client.raw(query, fn, self.collection, next);
567 |     });
568 |     return this;
569 |   }
570 | };
571 | 
572 | select.fn.init.prototype = select.fn;
573 | module.exports = select;
574 | 
575 |
-------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | `select` is a database library. 2 | 3 | ## Installation 4 | 5 | $ npm install select 6 | 7 | ## Examples 8 | 9 | // Find some items and iterate 10 | select('users') 11 | .find({ name: 'Alex' }) 12 | .limit(3) 13 | .offset(8) 14 | .each(function(index) { 15 | console.log(this); 16 | }); 17 | 18 | // Update an attribute 19 | select('users') 20 | .find(17) 21 | .attr({ name: 'Yuka' }); 22 | 23 | // Create 24 | select('users') 25 | .add({ 26 | name: 'Bob', 27 | email: 'bob@example.com' 28 | }); 29 | 30 | // Delete 31 | select('users') 32 | .find(1) 33 | .del(); 34 | 35 | // Selector language 36 | select('users[name="Alex", email="alex@example.com"]'). 37 | values(function(values) {}); 38 | 39 | ## Philosophy 40 | 41 | `select` is: 42 | 43 | * A quick way of using common database features 44 | * Easy to learn for JavaScript developers 45 | * Easy to extend 46 | 47 | `select` is not: 48 | 49 | * Concerned with schema 50 | * An ORM 51 | * A validation library 52 | 53 | ## License 54 | 55 | The MIT License 56 | 57 | Copyright (c) 2011 Alex R. Young 58 | 59 | -------------------------------------------------------------------------------- /docs/notes.txt: -------------------------------------------------------------------------------- 1 | TODO: 2 | 3 | - plugin/middleware brainstorm 4 | - 'hooks' brainstorm 5 | - redis 6 | - count/sum operations (count should be value count, run async after finders) 7 | select(). 8 | find(). 9 | count(fn) = value count 10 | 11 | select(). 12 | count('expression', fn) = db count expression if available 13 | 14 | - mysql/memory/couch/etc. sort 15 | - can the scheduler accidentally re-run operations? 16 | - make add work with arrays 17 | - deleting with offset/limit 18 | - doing select('users').del(params) should do DELETE ... style deletes on all systems 19 | - doing select('users').find(...).del() should delete each record manually 20 | - update (attr) should work like this too 21 | - escape collection names? 22 | - bulk update 23 | 24 | * [done] Design the language for the API 25 | * [partly done] Model the 'stack' chain on jQuery (.end) 26 | * [done basics] couch 27 | * [done] Work out a stack-based state machine for holding state 28 | * [partly done] doing select('users').del() should do bulk delete for all database systems (except for couchdb) 29 | should also accept parameters like SQL DELETE 30 | 31 | PHILOSOPHY 32 | 33 | clean, cross-database API 34 | familiar to JavaScript developers, influenced by popular libraries like jQuery 35 | not ORM 36 | doesn't provide validations, schema management, or migrations -- choice from other libraries 37 | 38 | WRITE OPERATIONS 39 | 40 | create/add 41 | update/attr 42 | delete/del 43 | 44 | READ OPERATIONS 45 | 46 | find/where 47 | 48 | MODIFIERS 49 | 50 | limit 51 | sort 52 | offset 53 | 54 | COMBINED - READ TO WRITE 55 | 56 | select('users') 57 | .where({ age: { gt: 21 } }) 58 | .limit(20) 59 | .attr({ access: true }); 60 | 61 | // Update all returned values to true 62 | // Subsequent attr/del should be possible 63 | // 'add' would just create a new value on the collection and ignore the current set of matches 64 | 65 | ACCESSING VALUES 66 | 67 | each(fn) 68 | first(fn) 69 | last(fn) 70 | 71 | // promises? http://api.jquery.com/promise/ 72 | 73 | -- state transitions -- 74 | 75 | read (read/modifiers) to write 76 | - run read, get results, manipulate results 77 | 78 | read to modifier to read 79 | - compile read with modifiers 80 | - run 81 | - apply read to results *** (may come later due to duplication of database finder logic in javascript) 82 | 83 | read to write to read 84 | - write is not relevant 85 | 86 | read, end, read 87 | - end jumps up to previous state 88 | 89 | -- stack state -- 90 | 91 | 'this' holds current state 92 | end can pop this to previous states 93 | 94 | ... 95 | 96 | 97 | QUERY LANGUAGE MINI SPEC 98 | 99 | [spreadsheet] 100 | 101 | RECORDS 102 | 103 | * Should records be special objects? This would be useful for mapping from things in setValues to performing operations like delete 104 | 105 | 106 | DEPENDENCIES 107 | 108 | (make these conditional based on settings) 109 | 110 | * TODO: Report error when library is not found 111 | 112 | https://github.com/creationix/couch-client 113 | http://wiki.apache.org/couchdb/CouchIn15Minutes 114 | http://sitr.us/2009/06/30/database-queries-the-couchdb-way.html 115 | http://wiki.apache.org/couchdb/HTTP_view_API 116 | http://blog.couchbase.com/moving-from-mysql-to-couchdb-part-1 117 | http://guide.couchdb.org/draft/cookbook.html 118 | http://guide.couchdb.org/editions/1/en/index.html 119 | http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API 120 | 121 | TESTS 122 | 123 | https://github.com/cloudkick/whiskey 124 | -------------------------------------------------------------------------------- /docs/scraps.txt: -------------------------------------------------------------------------------- 1 | var select = require('select'), 2 | assert = require('assert'); 3 | 4 | module.exports = { 5 | 'test init': function() { 6 | assert.ok(select()); 7 | }, 8 | 9 | 'test configuration': function() { 10 | select.db = 'mysql://root@localhost/select-test'; 11 | 12 | select('users') 13 | .find(17) 14 | .attr({ name: 'Yuka' }); 15 | 16 | /* 17 | select('users') 18 | .add({ 19 | name: 'Bob', 20 | email: 'bob@example.com' 21 | }); 22 | */ 23 | 24 | /* 25 | select('users') 26 | .find(1) 27 | .del(); 28 | */ 29 | 30 | /* 31 | select('users') 32 | .find({ name: 'Alex' }) 33 | .limit(3) 34 | .offset(8) 35 | .each(function(index) { 36 | console.log(this); 37 | }); 38 | */ 39 | 40 | //select.db = 'http://127.0.0.1:5984/select-test'; 41 | 42 | // TODO: what should the select argument be for couch? 43 | // views? 44 | /*select('users/users') 45 | .find('bd7182a007d6db0796ba1e6a5f000f1d') 46 | .each(function(index) { 47 | console.log('index:', index); 48 | console.log('value:', this); 49 | }); 50 | */ 51 | 52 | /* 53 | select() 54 | .add({ 55 | name: 'Alex', 56 | email: 'alex@example.com', 57 | created: new Date() 58 | }) 59 | .find() 60 | .each(function(index) { 61 | console.log(this); 62 | }); 63 | */ 64 | 65 | // select.db = 'mongodb://127.0.0.1/select-test'; 66 | 67 | /*select('users').add({ 68 | name: 'Alex', 69 | email: 'alex@example.com', 70 | created: new Date() 71 | }) 72 | .find({ email: 'alex@example.com' }) 73 | .each(function(index) { 74 | console.log(this); 75 | }); 76 | */ 77 | 78 | /*select('users') 79 | .find({ email: 'alex@example.com' }) 80 | .limit(7) 81 | .sort('created', 'desc') 82 | .each(function(i) { 83 | console.log('item:', i, this); 84 | }); 85 | */ 86 | 87 | /* 88 | select('users') 89 | .find({ email: 'alex@example.com' }) 90 | .limit(2) 91 | .attr({ email: 'bob@example.com' }); 92 | 93 | select('users') 94 | .find({ email: 'alex@example.com' }) 95 | .limit(1) 96 | .del(); 97 | */ 98 | } 99 | }; 100 | 101 | 102 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/select'); 2 | 3 | -------------------------------------------------------------------------------- /lib/chain.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | states = require(path.join(__dirname, 'states')); 3 | 4 | function Operation(options) { 5 | this.type = options.type; 6 | this.fn = options.callback; 7 | this.scheduler = options.scheduler; 8 | this.delegate = options.scheduler.delegate; 9 | this.db = this.delegate.db; 10 | this.finished = options.finished; 11 | this.done = false; 12 | } 13 | 14 | Operation.prototype = { 15 | run: function() { 16 | if (this.done) return; 17 | 18 | // These are getting too unwieldy 19 | if ((this.type == states.read) 20 | || (this.type == states.write) 21 | || (this.type == (states.read | states.exec | states.once)) 22 | || (this.type == (states.write | states.update))) { 23 | this.connect(this.fn); 24 | } else if (this.type == (states.read | states.exec)) { 25 | for (var i = 0; i < this.delegate.length; i++) { 26 | if (this.fn) 27 | this.fn.apply(this.delegate[i], [i]); 28 | } 29 | } else if (this.type & states.after) { 30 | this.fn(); 31 | } else if (this.type == states.exec) { 32 | this.fn(); 33 | } 34 | this.done = true; 35 | }, 36 | 37 | connect: function(fn) { 38 | var client = this.loadDatabaseDriver(), 39 | self = this; 40 | client.connect(this.db, function(err) { 41 | if (err) { 42 | throw(err); 43 | } else { 44 | fn(client, function(callback) { 45 | return function() { 46 | if (callback) callback.apply(self.select, arguments); 47 | self.finished(); 48 | self.scheduler.readOptions = {}; 49 | } 50 | }); 51 | } 52 | }); 53 | }, 54 | 55 | databaseLibraryName: function() { 56 | switch (this.db.split(':')[0]) { 57 | case 'mongodb': return 'mongodb'; 58 | case 'http': return 'couchdb'; 59 | case 'mysql': return 'mysql'; 60 | case 'memory': return 'memory'; 61 | } 62 | }, 63 | 64 | databaseLibrary: function() { 65 | var library = this.databaseLibraryName(); 66 | if (!this.delegate.primaryKey) { 67 | this.delegate.primaryKey = this.delegate.defaultPrimaryKeys[library]; 68 | } 69 | return path.join(__dirname, 'drivers', library); 70 | }, 71 | 72 | loadDatabaseDriver: function() { 73 | try { 74 | var Lib = require(this.databaseLibrary()); 75 | return new Lib(this.delegate); 76 | } catch (e) { 77 | console.log('Missing library. Please run npm install -g ' + this.databaseLibrary()); 78 | throw(e); 79 | } 80 | } 81 | }; 82 | 83 | function Chain(delegate) { 84 | this.delegate = delegate; 85 | this.operations = []; 86 | this.stack = []; 87 | this.readOptions = {}; 88 | } 89 | 90 | Chain.prototype = { 91 | run: function() { 92 | var operation = this.operations.shift(); 93 | if (operation) operation.run(); 94 | }, 95 | 96 | push: function(type, fn) { 97 | var self = this; 98 | this.stack.push(type); 99 | 100 | if (!((type & states.read) && (type & states.modify))) { 101 | this.operations.push(new Operation({ 102 | scheduler: this, 103 | type: type, 104 | callback: fn, 105 | finished: function() { 106 | self.run(); 107 | } 108 | })); 109 | 110 | if (type !== states.read) this.run(); 111 | } 112 | 113 | if ((type & states.read) && (type & states.modify)) { 114 | for (var i in fn) { 115 | this.readOptions[i] = fn[i]; 116 | } 117 | } 118 | } 119 | }; 120 | 121 | module.exports = Chain; 122 | -------------------------------------------------------------------------------- /lib/drivers/couchdb.js: -------------------------------------------------------------------------------- 1 | var CouchClient = require('couch-client'); 2 | 3 | function CouchDB() { 4 | } 5 | 6 | CouchDB.prototype = { 7 | connect: function(connectionString, fn) { 8 | this.connectionString = connectionString; 9 | this.dbName = connectionString.split('/').slice(-1)[0]; 10 | fn(); 11 | }, 12 | 13 | createConnection: function(fn) { 14 | fn(); 15 | }, 16 | 17 | find: function(collection, query, options, fn) { 18 | var self = this; 19 | 20 | function sendResults(err, docs) { 21 | if (!err && typeof fn !== 'undefined') { 22 | if (typeof docs !== 'array') docs = [docs]; 23 | if (fn) fn(err, docs); 24 | } else { 25 | console.log(err); 26 | } 27 | } 28 | 29 | if (!query) { 30 | // curl "http://127.0.0.1:5984/select-test/_all_docs?include_docs=true" 31 | } else if (typeof query === 'object') { 32 | // TODO: create a view? 33 | } else if (typeof query === 'string' || typeof query === 'integer') { 34 | var client = CouchClient(this.connectionString); 35 | if (collection) { 36 | var design = collection.split('/')[0], 37 | view = collection.split('/')[1]; 38 | // this.dbName + '/_design/' + design + '/_view/' + view 39 | client.view(collection, { key: query }, function(err, docs) { 40 | if (docs.rows) { 41 | docs = docs.rows.map(function(d) { return d.value; }); 42 | sendResults(err, docs); 43 | } 44 | }); 45 | } else { 46 | client.get(query, function(err, docs) { 47 | sendResults(err, docs); 48 | }); 49 | } 50 | } 51 | }, 52 | 53 | add: function(collection, options, fn) { 54 | var client = CouchClient(this.connectionString + '/' + collection); 55 | client.save(options, function(err, doc) { 56 | if (err) console.log(err); 57 | if (!err && typeof fn !== 'undefined') { 58 | fn(err, doc); 59 | } 60 | }); 61 | }, 62 | 63 | update: function(collection, original, record, fn) { 64 | }, 65 | 66 | remove: function(collection, original, fn) { 67 | }, 68 | 69 | close: function() { 70 | } 71 | }; 72 | 73 | module.exports = CouchDB; 74 | 75 | -------------------------------------------------------------------------------- /lib/drivers/memory.js: -------------------------------------------------------------------------------- 1 | var url = require('url'), 2 | data = {}; 3 | 4 | function Memory(delegate) { 5 | this.delegate = delegate; 6 | } 7 | 8 | Memory.prototype = { 9 | connect: function(connectionString, fn) { 10 | this.connectionString = connectionString; 11 | fn(); 12 | }, 13 | 14 | initCollection: function(collection) { 15 | if (!data[collection]) 16 | data[collection] = []; 17 | }, 18 | 19 | matchAll: function(collection, query, i) { 20 | return Object.keys(query).every(function(key) { 21 | return data[collection][i] && data[collection][i][key] === query[key]; 22 | }); 23 | }, 24 | 25 | find: function(collection, query, options, fn) { 26 | this.initCollection(collection); 27 | 28 | var results = [], 29 | sortedData = [], 30 | start = 0, 31 | count = data[collection].length, 32 | order; 33 | 34 | if (options.offset) start = options.offset; 35 | if (options.limit) count = start + options.limit; 36 | if (options.sort) order = options.sort; 37 | if (count > data[collection].length) count = data[collection].length; 38 | 39 | if (order && typeof order === 'string') { 40 | order = [order]; 41 | order.push('asc'); 42 | } 43 | 44 | sortedData = data[collection]; 45 | 46 | if (order) { 47 | var sort = function(a, b) { 48 | if (a[order[0]] < b[order[0]]) { 49 | return -1; 50 | } else if (a[order[0]] > b[order[0]]) { 51 | return 1; 52 | } 53 | return 0; 54 | }; 55 | sortedData = sortedData.sort(order[1] == 'desc' ? function(a, b) { return sort(b, a); } : sort); 56 | } 57 | 58 | if (typeof query === 'undefined') { 59 | results = sortedData.slice(start, count); 60 | } else if (typeof query === 'number' || typeof query === 'string') { 61 | for (var i = start; i < count; i++) { 62 | if (sortedData[i] && sortedData[i][this.delegate.primaryKey] === query) { 63 | results.push(sortedData[i]); 64 | } 65 | } 66 | } else if (typeof query === 'object') { 67 | for (var i = start; i < count; i++) { 68 | if (this.matchAll(collection, query, i)) { 69 | results.push(sortedData[i]); 70 | } 71 | } 72 | } 73 | 74 | if (fn) fn(null, results); 75 | }, 76 | 77 | add: function(collection, options, fn) { 78 | this.initCollection(collection); 79 | data[collection].push(options); 80 | if (fn) fn(null, options); 81 | }, 82 | 83 | update: function(collection, original, record, fn) { 84 | if (!data[collection]) data[collection] = []; 85 | for (var i = 0; i < data[collection].length; i++) { 86 | if (this.matchAll(collection, original, i)) { 87 | for (var key in record) { 88 | data[collection][i][key] = record[key]; 89 | } 90 | } 91 | } 92 | 93 | if (fn) fn(); 94 | }, 95 | 96 | remove: function() { 97 | var collection, original, fn; 98 | 99 | if (arguments.length == 2) { 100 | collection = arguments[0]; 101 | fn = arguments[1]; 102 | } else if (arguments.length === 3) { 103 | collection = arguments[0]; 104 | original = arguments[1]; 105 | fn = arguments[2]; 106 | } 107 | 108 | if (!data[collection]) data[collection] = []; 109 | 110 | if (original) { 111 | for (var i = 0; i < data[collection].length; i++) { 112 | if (this.matchAll(collection, original, i)) { 113 | data[collection].splice(i, 1); 114 | } 115 | } 116 | } else { 117 | data[collection] = []; 118 | } 119 | 120 | if (fn) fn(); 121 | }, 122 | 123 | raw: function(str, fn) { 124 | }, 125 | 126 | close: function() { 127 | } 128 | }; 129 | 130 | module.exports = Memory; 131 | 132 | -------------------------------------------------------------------------------- /lib/drivers/mongodb.js: -------------------------------------------------------------------------------- 1 | var mongodb = require('mongodb'), 2 | Db = mongodb.Db, 3 | Server = mongodb.Server, 4 | url = require('url'); 5 | 6 | function MongoDB(delegate) { 7 | this.connected = false; 8 | this.delegate = delegate; 9 | } 10 | 11 | MongoDB.prototype = { 12 | connect: function(connectionString, fn) { 13 | var uri = url.parse(connectionString), 14 | options = {}; 15 | 16 | this.host = uri.hostname; 17 | this.port = uri.port || 27017; 18 | this.database = uri.pathname.replace(/^\//, ''); 19 | 20 | if (uri.auth) { 21 | options.auth = uri.auth.split(':'), 22 | options.username = auth[0], 23 | options.password = auth[1]; 24 | } 25 | 26 | this.options = options; 27 | this.client = this.createConnection(fn); 28 | }, 29 | 30 | error: function(err) { 31 | this.delegate.emit('error', err); 32 | }, 33 | 34 | createConnection: function(fn) { 35 | var self = this, 36 | client = new Db(this.database, new Server(this.host, this.port, this.options)); 37 | 38 | client.open(function(err, client) { 39 | if (!err && self.options.username) { 40 | client.authenticate(self.options.username, self.options.password, function(err, success) { 41 | if (success) { 42 | fn(); 43 | } else { 44 | // Throw authentication errors 45 | throw(err); 46 | } 47 | }); 48 | } else if (!err) { 49 | fn(); 50 | } else { 51 | if (err) self.error(err); 52 | } 53 | }); 54 | 55 | return client; 56 | }, 57 | 58 | find: function(collection, query, options, fn) { 59 | var self = this, 60 | primaryKey; 61 | options = options || {}; 62 | query = query || {}; 63 | 64 | // TODO: ObjectID? 65 | if (typeof query === 'number' || typeof query === 'string') { 66 | primaryKey = query; 67 | query = {}; 68 | query[this.delegate.primaryKey] = new this.client.bson_serializer.ObjectID(primaryKey); 69 | } 70 | 71 | if (options.offset) { 72 | options.skip = options.offset; 73 | delete options.offset; 74 | } 75 | 76 | if (options.sort) { 77 | options.sort = [[options.sort[0], options.sort[1] || 'asc']]; 78 | } 79 | 80 | this.client.createCollection(collection, function(err, c) { 81 | var cursor = c.find(query, options); 82 | cursor.count(function(err, count) { 83 | cursor.toArray(function(err, docs) { 84 | if (err) self.error(err); 85 | if (fn) fn(err, docs); 86 | self.close(); 87 | }); 88 | }); 89 | }); 90 | }, 91 | 92 | add: function(collection, options, fn) { 93 | var self = this; 94 | this.client.createCollection(collection, function(err, c) { 95 | c.insert(options, function(err, docs) { 96 | if (err) self.error(err); 97 | if (!err && typeof fn !== 'undefined') { 98 | fn(err, docs); 99 | } 100 | self.close(); 101 | }); 102 | }); 103 | }, 104 | 105 | update: function(collection, original, record, fn) { 106 | var self = this; 107 | 108 | this.client.createCollection(collection, function(err, c) { 109 | c.update(original, { $set: record }, { safe: true }, function(err) { 110 | if (err) self.error(err); 111 | if (!err && typeof fn !== 'undefined') { 112 | // TODO: pass values? 113 | fn(err); 114 | } 115 | self.close(); 116 | }); 117 | }); 118 | }, 119 | 120 | remove: function() { 121 | var collection, original, fn, query; 122 | 123 | if (arguments.length == 2) { 124 | collection = arguments[0]; 125 | fn = arguments[1]; 126 | query = {}; 127 | } else if (arguments.length === 3) { 128 | collection = arguments[0]; 129 | original = arguments[1]; 130 | fn = arguments[2]; 131 | // TODO: should this be _id or what the find() returned? 132 | query = {}; 133 | query[this.delegate.primaryKey] = original[this.delegate.primaryKey]; 134 | } 135 | 136 | var self = this; 137 | 138 | this.client.createCollection(collection, function(err, c) { 139 | c.remove(query, function(err, c) { 140 | if (err) self.error(err); 141 | if (!err && typeof fn !== 'undefined') { 142 | fn(err); 143 | } 144 | self.close(); 145 | }); 146 | }); 147 | }, 148 | 149 | raw: function(fn, _, collection, next) { 150 | var self = this; 151 | this.client.createCollection(collection, function(err, c) { 152 | fn(c, function() { 153 | self.close(); 154 | next(); 155 | }); 156 | }); 157 | }, 158 | 159 | close: function() { 160 | if (this.client) { 161 | this.client.close(); 162 | delete this.client; 163 | } 164 | } 165 | }; 166 | 167 | module.exports = MongoDB; 168 | -------------------------------------------------------------------------------- /lib/drivers/mysql.js: -------------------------------------------------------------------------------- 1 | var Client = require('mysql').Client, 2 | url = require('url'); 3 | 4 | function MySQL(delegate) { 5 | this.delegate = delegate; 6 | } 7 | 8 | MySQL.prototype = { 9 | // TODO: Reuse connections? Connection pooling? 10 | // what does the mysql module do/recommend? 11 | connect: function(connectionString, fn) { 12 | this.connectionString = connectionString; 13 | var curl = url.parse(connectionString); 14 | this.options = {}; 15 | this.options.database = curl.pathname.replace(/\//, ''); 16 | this.options.user = curl.auth ? curl.auth.split(':')[0] : ''; 17 | this.options.password = curl.auth ? curl.auth.split(':')[1] : ''; 18 | this.options.host = curl.hostname; 19 | this.options.port = curl.port || 3306; 20 | this.client = this.createConnection(fn); 21 | this.client.connect(); 22 | fn(); 23 | }, 24 | 25 | error: function(err) { 26 | this.delegate.emit('error', err); 27 | }, 28 | 29 | createConnection: function(fn) { 30 | return new Client(this.options); 31 | }, 32 | 33 | // I didn't want the single quotes 34 | escape: function(val) { 35 | if (val === undefined || val === null) { 36 | return 'NULL'; 37 | } 38 | 39 | switch (typeof val) { 40 | case 'boolean': return (val) ? 'true' : 'false'; 41 | case 'number': return val+''; 42 | } 43 | 44 | if (typeof val === 'object') { 45 | val = val.toString(); 46 | } 47 | 48 | val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) { 49 | switch(s) { 50 | case "\0": return "\\0"; 51 | case "\n": return "\\n"; 52 | case "\r": return "\\r"; 53 | case "\b": return "\\b"; 54 | case "\t": return "\\t"; 55 | case "\x1a": return "\\Z"; 56 | default: return "\\"+s; 57 | } 58 | }); 59 | return val; 60 | }, 61 | 62 | find: function(collection, query, options, fn) { 63 | var self = this, 64 | parts = [], 65 | values = [], 66 | keys = [], 67 | order; 68 | 69 | options = options || {}; 70 | 71 | if (typeof query === 'number' || typeof query === 'string') { 72 | parts.push('WHERE ' + this.delegate.primaryKey + ' = ?'); 73 | values.push(query); 74 | } else if (typeof query === 'object') { 75 | keys = Object.keys(query); 76 | parts.push('WHERE ' + keys.map(function(k) { return k + ' = ?'; }).join(' AND ')); 77 | keys.forEach(function(k) { values.push(query[k]); }); 78 | } 79 | 80 | if (options.sort) { 81 | order = options.sort; 82 | if (typeof order === 'string') order = [order, 'ASC']; 83 | if (!order[1]) order[1] = 'ASC'; 84 | parts.push('ORDER BY ' + this.escape(order[0]) + ' ' + this.escape(order[1])); 85 | } 86 | 87 | if (options.limit) { 88 | parts.push('LIMIT ?'); 89 | values.push(options.limit); 90 | } 91 | 92 | if (options.offset) { 93 | parts.push('OFFSET ?'); 94 | values.push(options.offset); 95 | } 96 | 97 | this.client.query( 98 | 'SELECT * FROM ' + collection + ' ' + parts.join(' '), 99 | values, 100 | function(err, results, fields) { 101 | if (err) self.error(err); 102 | if (fn) fn(err, results || []); 103 | self.client.end(); 104 | } 105 | ); 106 | }, 107 | 108 | add: function(collection, options, fn) { 109 | var keys = Object.keys(options), 110 | self = this; 111 | 112 | this.client.query( 113 | 'INSERT INTO ' + collection + ' ' + 114 | 'SET ' + keys.map(function(k) { return k + ' = ?'; }).join(', '), 115 | keys.map(function(k) { return options[k]; }), 116 | function(err, results) { 117 | if (err) self.error(err); 118 | if (fn) fn(err, results); 119 | self.close(); 120 | } 121 | ); 122 | }, 123 | 124 | update: function(collection, original, record, fn) { 125 | var self = this, 126 | keys = Object.keys(record), 127 | originalKeys = Object.keys(original), 128 | values = keys.map(function(k) { return record[k]; }), 129 | originalValues = originalKeys.map(function(k) { return original[k]; }); 130 | 131 | this.client.query( 132 | 'UPDATE ' + collection + ' ' + 133 | 'SET ' + keys.map(function(k) { return k + ' = ?'; }).join(', ') + 134 | 'WHERE ' + originalKeys.map(function(k) { return k + ' = ?'; }).join(' AND '), 135 | values.concat(originalValues), 136 | function(err, results) { 137 | if (err) self.error(err); 138 | if (fn) fn(err, results); 139 | self.close(); 140 | } 141 | ); 142 | }, 143 | 144 | remove: function() { 145 | var collection, original, fn, keys, 146 | query, values, 147 | self = this; 148 | 149 | if (arguments.length == 2) { 150 | collection = arguments[0]; 151 | fn = arguments[1]; 152 | query = 'DELETE FROM ' + collection; 153 | this.client.query( 154 | query, 155 | function(err, results) { 156 | if (fn) fn(err, results); 157 | self.close(); 158 | if (err) self.error(err); 159 | } 160 | ).on('error', function() { 161 | self.close(); 162 | }); 163 | } else if (arguments.length === 3) { 164 | collection = arguments[0]; 165 | original = arguments[1]; 166 | keys = Object.keys(original); 167 | fn = arguments[2]; 168 | query = 'DELETE FROM ' + collection + ' ' + 169 | 'WHERE ' + keys.map(function(k) { return k + ' = ?'; }).join(' AND '); 170 | values = keys.map(function(k) { return original[k]; }); 171 | 172 | if (keys.length === 0) { 173 | this.close(); 174 | fn(); 175 | return; 176 | } 177 | 178 | this.client.query( 179 | query, 180 | values, 181 | function(err, results) { 182 | if (err) self.error(err); 183 | if (fn) fn(err, results); 184 | self.close(); 185 | } 186 | ).on('error', function() { 187 | self.close(); 188 | }); 189 | } 190 | }, 191 | 192 | raw: function(sql, fn) { 193 | var self = this; 194 | this.client.query(sql, function(err, results) { 195 | if (err) self.error(err); 196 | if (fn) fn(err, results); 197 | self.close(); 198 | }); 199 | }, 200 | 201 | close: function() { 202 | if (this.client) { 203 | this.client.end(); 204 | delete this.client; 205 | } 206 | } 207 | }; 208 | 209 | module.exports = MySQL; 210 | 211 | -------------------------------------------------------------------------------- /lib/select.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Select 3 | * Copyright (C) 2011 Alex R. Young 4 | * MIT Licensed 5 | */ 6 | 7 | /** 8 | * 9 | */ 10 | var path = require('path'), 11 | Chain = require(path.join(__dirname, 'chain')), 12 | states = require(path.join(__dirname, 'states')), 13 | parser = require(path.join(__dirname, 'selector_parser')), 14 | EventEmitter = require('events').EventEmitter, 15 | events = new EventEmitter(); 16 | 17 | /** 18 | * The main `select()` function. Use `select('selector')` with a collection or table name to start a chain. 19 | * 20 | * @param {String} selector A selector. For example, `'users'`, `'users[name="Alex"]'` 21 | * @returns {Object} A `select` object that can be subsequently called with database operations 22 | */ 23 | select = function(selector) { 24 | return new select.fn.init(selector, select.db); 25 | }; 26 | 27 | /** 28 | * Set this with a database connection URI. Examples: 29 | * 30 | * select.db = 'mysql://root@localhost/select-test'; 31 | * select.db = 'mongodb://localhost/select-test'; 32 | */ 33 | select.db = null; 34 | 35 | /** 36 | * Use this to change the default primary key. 37 | */ 38 | select.primaryKey = null; 39 | 40 | /** 41 | * Access the underlying database library. For example, with MySQL: 42 | * 43 | * select().raw('SELECT * FROM users', 44 | * function(err, results) {}); 45 | * 46 | * @param {Object|String} A query suitable for the underlying database API 47 | * @returns {Object} A `select` object 48 | */ 49 | select.raw = function() { 50 | var s = new select.fn.init('', select.db); 51 | s.raw.apply(s, arguments); 52 | return s; 53 | }; 54 | 55 | select.on = function(name, fn) { 56 | events.on(name, fn); 57 | }; 58 | 59 | select.emit = function(name, param) { 60 | events.emit(name, param); 61 | } 62 | 63 | select.states = states; 64 | 65 | select.fn = select.prototype = { 66 | constructor: select, 67 | 68 | init: function(selector, db) { 69 | var s = parser.parse(selector); 70 | this.collection = s.collection; 71 | this.length = 0; 72 | this.chain = new Chain(this); 73 | this.db = db; 74 | this.applyParsedTokens(s); 75 | return this; 76 | }, 77 | 78 | applyParsedTokens: function(s) { 79 | if (s.find) { 80 | this.find(s.find); 81 | } 82 | }, 83 | 84 | emit: function(name, param) { 85 | select.emit(name, param); 86 | }, 87 | 88 | primaryKey: select.primaryKey, 89 | 90 | defaultPrimaryKeys: { 91 | mysql: 'id', 92 | mongodb: '_id', 93 | memory: 'id', 94 | couchdb: 'id' // TODO 95 | }, 96 | 97 | setValues: function(values) { 98 | this.length = values.length; 99 | for (var i = 0; i < values.length; i++) { 100 | this[i] = values[i]; 101 | } 102 | }, 103 | 104 | toArray: function() { 105 | var values = []; 106 | for (var i = 0; i < this.length; i++) { 107 | values.push(this[i]); 108 | } 109 | return values; 110 | }, 111 | 112 | /** 113 | * Add a new record to the database. 114 | * 115 | * select('users'). 116 | * add({ name: 'Bill Adama' }); 117 | * 118 | * @param {Object} record An object containing values for a new record in the collection. 119 | * @param {Function} fn An optional callback 120 | * @returns {Object} The current `select` object 121 | */ 122 | add: function(record, fn) { 123 | var self = this; 124 | this.chain.push(states.write, function(client, next) { 125 | client.add(self.collection, record, next(fn)); 126 | }); 127 | return this; 128 | }, 129 | 130 | /** 131 | * Modify attributes. 132 | * 133 | * select('users'). 134 | * find(1). 135 | * attr({ name: 'William Adama' }); 136 | * 137 | * @param {Object} record An object containing values for a new record in the collection. 138 | * @param {Function} fn An optional callback 139 | * @returns {Object} The current `select` object 140 | */ 141 | attr: function(record, fn) { 142 | var self = this; 143 | this.chain.push(states.write | states.update, function(client, next) { 144 | // TODO: what about updating sets of records based on queries? 145 | var finishedCallbacks = self.length; 146 | function done() { 147 | finishedCallbacks--; 148 | if (finishedCallbacks == 0) { 149 | if (fn) fn(); 150 | } 151 | } 152 | 153 | for (var i = 0; i < self.length; i++) { 154 | client.update(self.collection, self[i], record, done); 155 | } 156 | 157 | if (self.length === 0 && fn) fn(); 158 | }); 159 | return this; 160 | }, 161 | 162 | /** 163 | * Delete the current set of values, or pass a delete query to bulk delete. 164 | * 165 | * Delete all: 166 | * 167 | * select('users').del(); 168 | * 169 | * Delete by ID: 170 | * 171 | * select('users').del(1); 172 | * 173 | * Delete with a query: 174 | * 175 | * select('users').del({ name: 'William Adama' }); 176 | * 177 | * Delete current set of values: 178 | * 179 | * select('users').find({ type: 'trial' }).del(); 180 | * 181 | * @returns {Object} The current `select` object 182 | */ 183 | del: function() { 184 | // FIXME: Value delete is causing problems, potentially just in mongodb 185 | var self = this, 186 | fn, 187 | query = null; 188 | 189 | if (arguments.length) { 190 | for (var i = 0; i < arguments.length; i++) { 191 | if (typeof arguments[i] === 'function') { 192 | fn = arguments[i]; 193 | } else if (typeof arguments[i] === 'object') { 194 | query = arguments[i]; 195 | } else if (typeof arguments[i] === 'string' || typeof arguments[i] === 'number') { 196 | query = arguments[i]; 197 | } 198 | } 199 | } 200 | 201 | if (this.chain.stack.length === 0 && !query) { 202 | this.chain.push(states.write | states.update, function(client, next) { 203 | client.remove(self.collection, fn); 204 | }); 205 | } else if (query) { 206 | this.find(query); 207 | this.del(fn); 208 | } else { 209 | this.chain.push(states.write | states.update, function(client, next) { 210 | for (var i = 0; i < self.length; i++) { 211 | client.remove(self.collection, self[i], fn); 212 | } 213 | if (self.length === 0) client.close(); 214 | next(fn); 215 | }); 216 | } 217 | return this; 218 | }, 219 | 220 | /** 221 | * Find records. 222 | * 223 | * Find based on ID: 224 | * select('users'). 225 | * find(1, function(err, values) {}); 226 | * 227 | * Find based on attributes: 228 | * select('users'). 229 | * find({ type: 'admin' }). 230 | * each(function() { console.log(this); }); 231 | * 232 | * @returns {Object} The current `select` object 233 | */ 234 | find: function(options, fn) { 235 | var self = this; 236 | this.chain.push(fn ? (states.read | states.exec | states.once) : states.read, function(client, next) { 237 | self.setValues([]); 238 | client.find(self.collection, options, self.chain.readOptions, next(function(err, values) { 239 | self.setValues(values); 240 | if (fn) fn(err, values); 241 | })); 242 | }); 243 | return this; 244 | }, 245 | 246 | /** 247 | * Limit the next database find query. 248 | * TODO: del/attr 249 | * 250 | * @param {Number} limit Limits the next find() by this amount 251 | * @returns {Object} The current `select` object 252 | */ 253 | limit: function(limit) { 254 | this.chain.push(states.read | states.modify, { limit: limit }); 255 | return this; 256 | }, 257 | 258 | /** 259 | * Offset the next database find query. 260 | * TODO: del/attr 261 | * 262 | * @param {Number} offset Offset the next find() by this amount 263 | * @returns {Object} The current `select` object 264 | */ 265 | offset: function(offset) { 266 | this.chain.push(states.read | states.modify, { offset: offset }); 267 | return this; 268 | }, 269 | 270 | /** 271 | * Sorts the results of the next find. 272 | * TODO: del/attr 273 | * 274 | * select('users'). 275 | * find(). 276 | * sort('name'). 277 | * each(function() { console.log(this); }); 278 | * 279 | * @returns {Object} The current `select` object 280 | */ 281 | sort: function() { 282 | this.chain.push(states.read | states.modify, { sort: arguments }); 283 | return this; 284 | }, 285 | 286 | /** 287 | * Causes the previous `find()` to run, and iterates over the results. 288 | * In the passed in callback, `this` will refer to each value. 289 | * 290 | * select('users'). 291 | * find(). 292 | * sort('name'). 293 | * each(function() { console.log(this); }); 294 | * 295 | * @param {Function} fn A callback that will be run for each value 296 | * @returns {Object} The current `select` object 297 | */ 298 | each: function(fn) { 299 | this.chain.push(states.read | states.exec, fn); 300 | return this; 301 | }, 302 | 303 | /** 304 | * Receives all of the values for the previous operations. 305 | * When a query produces an empty set, `each` won't run, but `values` would. 306 | * 307 | * select('users'). 308 | * find(). 309 | * sort('name'). 310 | * values(function(err, values) { console.log(values); }); 311 | * 312 | * @param {Function} fn A callback that gets `err` and `values` 313 | * @returns {Object} The current `select` object 314 | */ 315 | values: function(fn) { 316 | var self = this; 317 | this.chain.push(states.exec, function() { 318 | if (fn) fn(self.toArray()); 319 | }); 320 | return this; 321 | }, 322 | 323 | /** 324 | * Causes the current chain to execute. 325 | */ 326 | end: function(fn) { 327 | this.chain.push(states.read | states.exec, fn); 328 | // TODO: Is this right? 329 | return select(this.collection); 330 | }, 331 | 332 | /** 333 | * This adds a callback to run once other database operations have finished. 334 | * 335 | * @param {Function} fn The callback 336 | * @returns {Object} The current `select` object 337 | */ 338 | after: function(fn) { 339 | this.chain.push(states.after, fn); 340 | return this; 341 | }, 342 | 343 | raw: function(query, fn) { 344 | var self = this; 345 | this.chain.push(states.write | states.update, function(client, next) { 346 | client.raw(query, fn, self.collection, next); 347 | }); 348 | return this; 349 | } 350 | }; 351 | 352 | select.fn.init.prototype = select.fn; 353 | module.exports = select; 354 | -------------------------------------------------------------------------------- /lib/selector_parser.js: -------------------------------------------------------------------------------- 1 | function Parser(input) { 2 | this.input = input; 3 | } 4 | 5 | Parser.parse = function(input) { 6 | return new Parser(input).parse(); 7 | }; 8 | 9 | Parser.prototype = { 10 | // TODO: Parser 11 | buildQuery: function() { 12 | var query = {}, 13 | matches = this.input.match(/([^[]*)\[([^\]]*)/), 14 | f; 15 | query.collection = matches[1]; 16 | query.find = {}; 17 | 18 | f = matches[2].split(','); 19 | f.forEach(function(v) { 20 | var params = v.split('=').map(function(w) { 21 | return w.trim().replace(/['"]/g, ''); 22 | }); 23 | query.find[params[0]] = params[1]; 24 | }); 25 | return query; 26 | }, 27 | 28 | parse: function() { 29 | if (!this.input) return {}; 30 | 31 | if (this.input.indexOf('[') === -1) { 32 | return { 33 | collection: this.input 34 | }; 35 | } else { 36 | return this.buildQuery(); 37 | } 38 | } 39 | }; 40 | 41 | 42 | module.exports = Parser; 43 | -------------------------------------------------------------------------------- /lib/states.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | write: 1 << 0, 3 | update: 1 << 1, 4 | read: 1 << 2, 5 | exec: 1 << 3, 6 | once: 1 << 4, 7 | modify: 1 << 5, 8 | after: 1 << 6 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "select" 3 | , "description": "A new kind of database library" 4 | , "homepage": "http://selectjs.com" 5 | , "version": "0.1.1" 6 | , "author": "Alex R. Young " 7 | , "repository": "git://github.com/alexyoung/select" 8 | , "main": "./index.js" 9 | , "engines": { "node": ">= 0.4.0" } 10 | , "devDependencies": { 11 | "mongodb": "0.9.x" 12 | , "whiskey": "0.3.x" 13 | , "couch-client": ">= 0.0.4" 14 | , "mysql": "0.9.x" 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /test/core_test.js: -------------------------------------------------------------------------------- 1 | var select = require('select'); 2 | 3 | exports['test init'] = function(test, assert) { 4 | assert.ok(select()); 5 | test.finish(); 6 | }; 7 | -------------------------------------------------------------------------------- /test/memory_test.js: -------------------------------------------------------------------------------- 1 | var select = require('select'); 2 | 3 | function fixtures(collection, fn) { 4 | select(collection). 5 | add({ id: 1, name: 'Alex', email: 'alex@example.com' }). 6 | add({ id: 2, name: 'Bob', email: 'bob@example.com' }). 7 | add({ id: 3, name: 'Yuka', email: 'yuka@example.com' }). 8 | after(fn); 9 | } 10 | 11 | // Setup function 12 | exports['setUp'] = function(test, assert) { 13 | select.db = 'memory://test'; 14 | fixtures('users', test.finish); 15 | }; 16 | 17 | // Tests 18 | exports['test find by ID'] = function(test, assert) { 19 | select('users'). 20 | find(1). 21 | each(function() { 22 | assert.equal('Alex', this.name); 23 | test.finish(); 24 | }); 25 | }; 26 | 27 | exports['test find with values'] = function(test, assert) { 28 | select('users'). 29 | find(). 30 | values(function(values) { 31 | assert.ok(values.length > 0); 32 | test.finish(); 33 | }); 34 | }; 35 | 36 | exports['test find with values with 0 matches'] = function(test, assert) { 37 | select('users'). 38 | find({ name: 'Benny' }). 39 | values(function(values) { 40 | assert.equal(0, values.length); 41 | test.finish(); 42 | }); 43 | }; 44 | 45 | exports['test find with an object'] = function(test, assert) { 46 | select('users'). 47 | find({ name: 'Alex' }). 48 | each(function() { 49 | assert.equal('Alex', this.name); 50 | test.finish(); 51 | }); 52 | }; 53 | 54 | exports['test find by itself'] = function(test, assert) { 55 | select('users'). 56 | find(1, function(err, values) { 57 | assert.equal(1, values.length); 58 | test.finish(); 59 | }); 60 | }; 61 | 62 | exports['test find with limit'] = function(test, assert) { 63 | var i = 0; 64 | select('users'). 65 | find(). 66 | limit(2). 67 | each(function() { 68 | i++; 69 | assert.ok(i < 3); 70 | }). 71 | after(test.finish); 72 | }; 73 | 74 | exports['test find with sort'] = function(test, assert) { 75 | select('users'). 76 | find(). 77 | sort('name', 'desc'). 78 | values(function(values) { 79 | assert.deepEqual(['Yuka', 'Bob', 'Alex'], values.map(function(u) { return u.name; })); 80 | test.finish(); 81 | }); 82 | }; 83 | 84 | exports['test find with offset'] = function(test, assert) { 85 | select('users'). 86 | find(). 87 | sort('name'). 88 | limit(1). 89 | offset(1). 90 | each(function() { 91 | assert.equal(this.name, 'Bob'); 92 | }). 93 | after(test.finish); 94 | }; 95 | 96 | exports['test find with selector'] = function(test, assert) { 97 | select('users[name="Alex", email="alex@example.com"]'). 98 | values(function(values) { 99 | assert.ok(values.length > 0); 100 | for (var i = 0; i < values.length; i++) { 101 | assert.equal(values[i].name, 'Alex'); 102 | } 103 | test.finish(); 104 | }); 105 | }; 106 | 107 | exports['test add'] = function(test, assert) { 108 | select('users'). 109 | add({ name: 'CTB', email: 'ctb@example.com' }); 110 | 111 | select('users'). 112 | find({ name: 'CTB' }). 113 | each(function() { 114 | assert.equal(this.name, 'CTB'); 115 | }). 116 | after(test.finish); 117 | }; 118 | 119 | exports['test attr'] = function(test, assert) { 120 | select('users'). 121 | find({ name: 'Alex' }). 122 | attr({ name: 'Alex 2' }); 123 | 124 | select('users'). 125 | find({ name: 'Alex 2' }). 126 | each(function() { 127 | assert.equal(this.name, 'Alex 2'); 128 | }). 129 | after(test.finish); 130 | }; 131 | 132 | exports['test attr with offset'] = function(test, assert) { 133 | fixtures('users2', function() { 134 | select('users2'). 135 | find(). 136 | limit(2). 137 | offset(1). 138 | attr({ name: 'Alex 2' }, function() { 139 | select('users2'). 140 | find(). 141 | sort('name'). 142 | values(function(values) { 143 | assert.deepEqual(['Alex', 'Alex 2', 'Alex 2'], values.map(function(u) { return u.name; })); 144 | test.finish(); 145 | }); 146 | }); 147 | }); 148 | }; 149 | 150 | exports['test delete'] = function(test, assert) { 151 | select('users'). 152 | find(3). 153 | del(function() { 154 | select('users'). 155 | find(3, function(err, values) { 156 | assert.equal(0, values.length); 157 | test.finish(); 158 | }); 159 | }); 160 | }; 161 | 162 | exports['test delete with query'] = function(test, assert) { 163 | fixtures('dudes', function() { 164 | select('dudes').del({ name: 'Alex' }, function() { 165 | select('dudes').find({ name: 'Alex' }, function(err, values) { 166 | assert.equal(0, values.length); 167 | test.finish(); 168 | }); 169 | }); 170 | }); 171 | }; 172 | 173 | exports['test delete all'] = function(test, assert) { 174 | fixtures('people', function() { 175 | select('people').del().after(function() { 176 | select('people').find({}, function(err, values) { 177 | assert.equal(0, values.length); 178 | test.finish(); 179 | }); 180 | }); 181 | }); 182 | }; 183 | 184 | exports['test delete by ID'] = function(test, assert) { 185 | fixtures('folks', function() { 186 | select('folks'). 187 | find({ name: 'Bob' }). 188 | values(function(values) { 189 | select('folks'). 190 | del(values[0].id, function() { 191 | select('folks'). 192 | find(). 193 | values(function(values) { 194 | assert.ok(values.length > 0); 195 | assert.equal(0, values.filter(function(r) { return r.name === 'Bob'; }).length); 196 | test.finish(); 197 | }); 198 | }); 199 | }); 200 | }); 201 | }; 202 | -------------------------------------------------------------------------------- /test/mongodb_test.js: -------------------------------------------------------------------------------- 1 | var select = require('select'), 2 | exampleID; 3 | 4 | function fixtures(collection, fn) { 5 | select(collection).raw(function(c, next) { 6 | c.remove(function() {}); 7 | next(); 8 | 9 | select(collection). 10 | add({ name: 'Alex', email: 'alex@example.com' }). 11 | add({ name: 'Bob', email: 'bob@example.com' }). 12 | add({ name: 'Yuka', email: 'yuka@example.com' }). 13 | after(function() { 14 | select(collection). 15 | find(). 16 | limit(1). 17 | each(function() { 18 | exampleID = this._id; 19 | fn(); 20 | }); 21 | }); 22 | }); 23 | } 24 | 25 | // Setup function 26 | exports['setUp'] = function(test, assert) { 27 | select.db = 'mongodb://localhost/select-test'; 28 | fixtures('users', test.finish); 29 | }; 30 | 31 | // Tests 32 | exports['test find by ID'] = function(test, assert) { 33 | select('users'). 34 | find(exampleID.toString()). 35 | each(function() { 36 | assert.equal(this._id.toString(), exampleID); 37 | test.finish(); 38 | }); 39 | }; 40 | 41 | exports['test find with values'] = function(test, assert) { 42 | select('users'). 43 | find(). 44 | values(function(values) { 45 | assert.ok(values.length > 0); 46 | test.finish(); 47 | }); 48 | }; 49 | 50 | exports['test find with values with 0 matches'] = function(test, assert) { 51 | select('users'). 52 | find({ name: 'Benny' }). 53 | values(function(values) { 54 | assert.equal(0, values.length); 55 | test.finish(); 56 | }); 57 | }; 58 | 59 | exports['test find with an object'] = function(test, assert) { 60 | select('users'). 61 | find({ name: 'Alex' }). 62 | each(function() { 63 | assert.equal('Alex', this.name); 64 | test.finish(); 65 | }); 66 | }; 67 | 68 | exports['test find by itself'] = function(test, assert) { 69 | select('users'). 70 | find(exampleID.toString(), function(err, values) { 71 | assert.equal(1, values.length); 72 | test.finish(); 73 | }); 74 | }; 75 | 76 | exports['test find with limit'] = function(test, assert) { 77 | var i = 0; 78 | select('users'). 79 | find(). 80 | limit(2). 81 | each(function() { 82 | i++; 83 | assert.ok(i < 3); 84 | }). 85 | after(function() { 86 | test.finish(); 87 | }); 88 | }; 89 | 90 | exports['test find with offset'] = function(test, assert) { 91 | select('users'). 92 | find(). 93 | limit(1). 94 | offset(1). 95 | each(function() { 96 | assert.equal(this.name, 'Bob'); 97 | }). 98 | after(function() { 99 | test.finish(); 100 | }); 101 | }; 102 | 103 | exports['test attr with offset'] = function(test, assert) { 104 | fixtures('users2', function() { 105 | select('users2'). 106 | find(). 107 | limit(2). 108 | offset(1). 109 | sort('name'). 110 | attr({ name: 'Alex 2' }, function() { 111 | select('users2'). 112 | find(). 113 | sort('name'). 114 | values(function(values) { 115 | assert.deepEqual(['Alex', 'Alex 2', 'Alex 2'], values.map(function(u) { return u.name; })); 116 | test.finish(); 117 | }); 118 | }); 119 | }); 120 | }; 121 | 122 | exports['test find with selector'] = function(test, assert) { 123 | select('users[name="Alex", email="alex@example.com"]'). 124 | values(function(values) { 125 | assert.ok(values.length > 0); 126 | for (var i = 0; i < values.length; i++) { 127 | assert.equal(values[i].name, 'Alex'); 128 | } 129 | test.finish(); 130 | }); 131 | }; 132 | 133 | exports['test add'] = function(test, assert) { 134 | select('users'). 135 | add({ name: 'CTB', email: 'ctb@example.com' }). 136 | after(function() { 137 | select('users'). 138 | find({ name: 'CTB' }). 139 | each(function() { 140 | assert.equal(this.name, 'CTB'); 141 | }). 142 | after(function() { 143 | test.finish(); 144 | }); 145 | }); 146 | }; 147 | 148 | exports['test update'] = function(test, assert) { 149 | select('users'). 150 | find({ name: 'Alex' }). 151 | attr({ name: 'Alex 2' }); 152 | 153 | select('users'). 154 | find({ name: 'Alex 2' }). 155 | each(function() { 156 | assert.equal(this.name, 'Alex 2'); 157 | }). 158 | after(function() { 159 | test.finish(); 160 | }); 161 | }; 162 | 163 | exports['test find with sort'] = function(test, assert) { 164 | fixtures('admins', function() { 165 | select('admins'). 166 | find(). 167 | sort('name', 'desc'). 168 | values(function(values) { 169 | assert.deepEqual(['Yuka', 'Bob', 'Alex'], values.map(function(u) { return u.name; })); 170 | test.finish(); 171 | }); 172 | }); 173 | }; 174 | 175 | exports['test find with default sort'] = function(test, assert) { 176 | fixtures('admins2', function() { 177 | select('admins2'). 178 | find(). 179 | sort('name'). 180 | values(function(values) { 181 | assert.deepEqual(['Alex', 'Bob', 'Yuka'], values.map(function(u) { return u.name; })); 182 | test.finish(); 183 | }); 184 | }); 185 | }; 186 | 187 | exports['test delete'] = function(test, assert) { 188 | fixtures('users4', function() { 189 | select('users4'). 190 | find({ 'name': 'Alex' }). 191 | del(function() { 192 | select('users4'). 193 | find({ 'name': 'Alex' }, function(err, values) { 194 | assert.equal(0, values.length); 195 | test.finish(); 196 | }); 197 | }); 198 | }); 199 | }; 200 | 201 | exports['test delete with query'] = function(test, assert) { 202 | fixtures('dudes', function() { 203 | select('dudes').del({ name: 'Alex' }, function() { 204 | select('dudes').find({ name: 'Alex' }, function(err, values) { 205 | assert.equal(0, values.length); 206 | test.finish(); 207 | }); 208 | }); 209 | }); 210 | }; 211 | 212 | exports['test delete all'] = function(test, assert) { 213 | fixtures('people', function() { 214 | select('people').del().after(function() { 215 | select('people').find({}, function(err, values) { 216 | assert.equal(0, values.length); 217 | test.finish(); 218 | }); 219 | }); 220 | }); 221 | }; 222 | 223 | exports['test delete by ID'] = function(test, assert) { 224 | fixtures('folks', function() { 225 | select('folks'). 226 | find({ name: 'Bob' }). 227 | values(function(values) { 228 | select('folks'). 229 | del(values[0]._id.toString(), function() { 230 | select('folks'). 231 | find(). 232 | values(function(values) { 233 | assert.ok(values.length > 0); 234 | assert.equal(0, values.filter(function(r) { return r.name === 'Bob'; }).length); 235 | test.finish(); 236 | }); 237 | }); 238 | }); 239 | }); 240 | }; 241 | -------------------------------------------------------------------------------- /test/mysql_test.js: -------------------------------------------------------------------------------- 1 | var select = require('select'); 2 | 3 | // Fixtures 4 | function clearDB(collection, fn) { 5 | select.raw('DROP TABLE ' + collection, fn); 6 | } 7 | 8 | function initDB(collection, fn) { 9 | select.raw('CREATE TABLE ' + collection + ' (id INT AUTO_INCREMENT, name TEXT, email TEXT, INDEX USING BTREE (id));', fn); 10 | } 11 | 12 | function populateDB(collection, fn) { 13 | select.raw('INSERT INTO ' + collection + ' (name, email) VALUES ("Alex", "alex@example.com");' + 14 | 'INSERT INTO ' + collection + ' (name, email) VALUES ("Bob", "bob@example.com");' + 15 | 'INSERT INTO ' + collection + ' (name, email) VALUES ("Yuka", "yuka@example.com");', fn); 16 | } 17 | 18 | function fixtures(collection, fn) { 19 | clearDB(collection, function() { 20 | initDB(collection, function() { 21 | populateDB(collection, function() { 22 | fn(); 23 | }); 24 | }); 25 | }); 26 | } 27 | 28 | // Setup function 29 | exports['setUp'] = function(test, assert) { 30 | select.db = 'mysql://root@localhost/select-test'; 31 | fixtures('users', test.finish); 32 | }; 33 | 34 | // Tests 35 | exports['test find by ID'] = function(test, assert) { 36 | select('users'). 37 | find(1). 38 | each(function() { 39 | assert.equal('Alex', this.name); 40 | test.finish(); 41 | }); 42 | }; 43 | 44 | exports['test find with values'] = function(test, assert) { 45 | select('users'). 46 | find(). 47 | values(function(values) { 48 | assert.ok(values.length > 0); 49 | test.finish(); 50 | }); 51 | }; 52 | 53 | exports['test find with values with 0 matches'] = function(test, assert) { 54 | select('users'). 55 | find({ name: 'Benny' }). 56 | values(function(values) { 57 | assert.equal(0, values.length); 58 | test.finish(); 59 | }); 60 | }; 61 | 62 | exports['test find with an object'] = function(test, assert) { 63 | select('users'). 64 | find({ name: 'Alex' }). 65 | each(function() { 66 | assert.equal('Alex', this.name); 67 | test.finish(); 68 | }); 69 | }; 70 | 71 | exports['test find by itself'] = function(test, assert) { 72 | select('users'). 73 | find(1, function(err, values) { 74 | assert.equal(1, values.length); 75 | test.finish(); 76 | }); 77 | }; 78 | 79 | exports['test find with limit'] = function(test, assert) { 80 | var i = 0; 81 | select('users'). 82 | find(). 83 | limit(2). 84 | each(function() { 85 | i++; 86 | assert.ok(i < 3); 87 | }). 88 | after(function() { 89 | test.finish(); 90 | }); 91 | }; 92 | 93 | exports['test find with offset'] = function(test, assert) { 94 | select('users'). 95 | find(). 96 | limit(1). 97 | offset(1). 98 | each(function() { 99 | assert.equal(this.name, 'Bob'); 100 | }). 101 | after(function() { 102 | test.finish(); 103 | }); 104 | }; 105 | 106 | exports['test find with sort'] = function(test, assert) { 107 | fixtures('admins', function() { 108 | select('admins'). 109 | find(). 110 | sort('name', 'desc'). 111 | values(function(values) { 112 | assert.deepEqual(['Yuka', 'Bob', 'Alex'], values.map(function(u) { return u.name; })); 113 | test.finish(); 114 | }); 115 | }); 116 | }; 117 | 118 | exports['test find with selector'] = function(test, assert) { 119 | select('users[name="Alex", email="alex@example.com"]'). 120 | values(function(values) { 121 | assert.ok(values.length > 0); 122 | for (var i = 0; i < values.length; i++) { 123 | assert.equal(values[i].name, 'Alex'); 124 | } 125 | test.finish(); 126 | }); 127 | }; 128 | 129 | exports['test attr with offset'] = function(test, assert) { 130 | fixtures('users2', function() { 131 | select('users2'). 132 | find(). 133 | limit(2). 134 | offset(1). 135 | sort('name'). 136 | attr({ name: 'Alex 2' }, function() { 137 | select('users2'). 138 | find(). 139 | sort('name'). 140 | values(function(values) { 141 | assert.deepEqual(['Alex', 'Alex 2', 'Alex 2'], values.map(function(u) { return u.name; })); 142 | test.finish(); 143 | }); 144 | }); 145 | }); 146 | }; 147 | 148 | exports['test sql injection'] = function(test, assert) { 149 | fixtures('users3', function() { 150 | select('users3'). 151 | find(). 152 | limit(1). 153 | sort('name'). 154 | values(function(values) { 155 | assert.deepEqual(['Alex'], values.map(function(u) { return u.name; })); 156 | test.finish(); 157 | }); 158 | }); 159 | }; 160 | 161 | exports['test add'] = function(test, assert) { 162 | select('users'). 163 | add({ name: 'CTB', email: 'ctb@example.com' }). 164 | after(function() { 165 | select('users'). 166 | find({ name: 'CTB' }). 167 | each(function() { 168 | assert.equal(this.name, 'CTB'); 169 | }). 170 | after(function() { 171 | test.finish(); 172 | }); 173 | }); 174 | }; 175 | 176 | exports['test update'] = function(test, assert) { 177 | select('users'). 178 | find({ name: 'Alex' }). 179 | attr({ name: 'Alex 2' }); 180 | 181 | select('users'). 182 | find({ name: 'Alex 2' }). 183 | each(function() { 184 | assert.equal(this.name, 'Alex 2'); 185 | }). 186 | after(function() { 187 | test.finish(); 188 | }); 189 | }; 190 | 191 | exports['test delete'] = function(test, assert) { 192 | select('users'). 193 | find(3). 194 | del(function() { 195 | select('users'). 196 | find(3, function(err, values) { 197 | assert.equal(0, values.length); 198 | test.finish(); 199 | }); 200 | }); 201 | }; 202 | 203 | exports['test delete all'] = function(test, assert) { 204 | fixtures('people', function() { 205 | select('people').del(function() { 206 | select('people').find(undefined, function(err, values) { 207 | assert.equal(0, values.length); 208 | test.finish(); 209 | }); 210 | }); 211 | }); 212 | }; 213 | 214 | exports['test delete with query'] = function(test, assert) { 215 | fixtures('dudes', function() { 216 | select('dudes').del({ name: 'Alex' }, function() { 217 | select('dudes').find({ name: 'Alex' }, function(err, values) { 218 | assert.equal(0, values.length); 219 | test.finish(); 220 | }); 221 | }); 222 | }); 223 | }; 224 | 225 | exports['test errors'] = function(test, assert) { 226 | select.on('error', function(err) { 227 | // Column or table 228 | assert.ok(err.message.match(/Unknown /)); 229 | test.finish(); 230 | }); 231 | 232 | select('users').find({ xxx: 'yyy' }).each(function() { 233 | }); 234 | }; 235 | 236 | exports['test delete by ID'] = function(test, assert) { 237 | fixtures('folks', function() { 238 | select('folks'). 239 | find({ name: 'Bob' }). 240 | values(function(values) { 241 | select('folks'). 242 | del(values[0].id, function() { 243 | select('folks'). 244 | find(). 245 | values(function(values) { 246 | assert.ok(values.length > 0); 247 | assert.equal(0, values.filter(function(r) { return r.name === 'Bob'; }).length); 248 | test.finish(); 249 | }); 250 | }); 251 | }); 252 | }); 253 | }; 254 | --------------------------------------------------------------------------------