UNPKG

17 kBMarkdownView Raw
1[![Oreo](oreo.png)](https://github.com/will123195/oreo)
2
3
4[![Build Status](https://travis-ci.org/will123195/oreo.svg?branch=master)](https://travis-ci.org/will123195/oreo)
5
6# Features
7
8- Zero configuration -- auto-detects columns, primary keys and foreign keys
9- Saves multi-table nested objects with an atomic transaction
10- Detects primary and read-only hosts
11- Use callbacks or plug in your own Promise library
12- No dependencies
13
14# Database Support
15
16- PostgreSQL 9+
17- MySQL
18
19# Installation
20
21```bash
22npm install oreo
23npm install pg
24#npm install mysql
25```
26
27# Quick Example
28
29```js
30var oreo = require('oreo')
31
32var db = oreo({
33 driver: 'mysql',
34 hosts: ['localhost'],
35 name: 'my_db',
36 user: 'root',
37 pass: ''
38}, function (err) {
39 // Assuming you have a table "artists"
40 // Get an artist by primary key
41 db.artists.get(id, function (err, artist) {
42 console.log(artist)
43 })
44}
45```
46
47<hr />
48
49# Documentation
50
51## Usage
52
53* [`oreo`](#instantiate)
54
55## Db
56
57* [`execute`](#execute)
58* [`executeWrite`](#executeWrite)
59* [`onReady`](#onReady)
60* [`end`](#end)
61
62## Table
63
64* [`find`](#find)
65* [`findOne`](#findOne)
66* [`get`](#get)
67* [`insert`](#insert)
68* [`mget`](#mget)
69* [`save`](#table_save)
70
71## Row
72
73* [`delete`](#delete)
74* [`hydrate`](#hydrate)
75* [`save`](#save)
76* [`set`](#set)
77* [`update`](#update)
78
79<hr />
80
81# Full Example
82
83&dagger; see the example database schema below
84
85```js
86var oreo = require('oreo')
87
88// initialize oreo: auto-detects the schema and determines writable/read-only hosts
89var db = oreo({
90 driver: 'pg',
91 hosts: ['localhost:5432'],
92 name: 'my_db',
93 user: 'username',
94 pass: 'password',
95 debug: console.log,
96 memoize: 150, // optional duration in ms to memoize rows
97 cache: redisClient, // optional
98 Promise: Promise, // optional, default: global.Promise
99 models: {} // optional
100}).onReady(runExampleQueries)
101
102function runExampleQueries () {
103
104 // Insert a new book, its author and some reviews (in a single transaction)
105 db.books.insert({
106 title: 'Fear and Loathing in Las Vegas',
107 author: {
108 name: 'Hunter S.Thompson'
109 },
110 reviews: [ // shorthand for 'book:reviews'
111 { stars: 5, body: 'Psychadelic!'},
112 { stars: 4, body: 'Bizarre, unpredictable yet strangely alluring.'}
113 ]
114 }).then(function (book) {
115 console.log(book) // { id: 1, title: Fear and Loathing in Las Vegas, author_id: 1 }
116
117 // Hydrate a book's author (1-to-1 linked row)
118 book.hydrate('author').then(function () {
119 console.log(book.author) // { id: 1, name: Hunter S. Thompson }
120 })
121
122 // Hydrate a book's reviews (1-to-many linked rows)
123 book.hydrate('reviews').then(function () {
124 console.log(book.reviews) // array
125 })
126
127 // Update a book
128 book.update({
129 title: 'The Rum Diary'
130 }).then(function (book) {
131 console.log(book) // { id: 1, title: The Rum Diary, author_id: 1 }
132 })
133
134 // Delete a book
135 book.delete().then(function () {
136 console.log(book) // {}
137 })
138 })
139
140 // Get an author by primary key
141 db.authors.get(1).then(function (author) {
142 console.log(author) // { id: 1, name: Hunter S. Thompson }
143 })
144
145 // Get multiple authors by primary key
146 db.authors.mget([1]).then(function (authors) {
147 console.log(authors) // [ { id: 1, name: Hunter S. Thompson } ]
148 })
149
150 // Find authors
151 db.authors.find({
152 where: {
153 name: 'Hunter S. Thompson'
154 },
155 order: 'name asc',
156 limit: 10,
157 offset: 0
158 }
159 }).then(function (authors) {
160 console.log(authors) // [ { id: 1, name: Hunter S. Thompson } ]
161 })
162
163 // Find one author
164 db.authors.findOne({
165 where: [
166 "name like 'Hunter %'"
167 ]
168 }).then(function (author) {
169 console.log(author) // { id: 1, name: Hunter S. Thompson }
170 })
171}
172```
173
174Example database schema:
175```sql
176CREATE TABLE authors (
177 id SERIAL,
178 name VARCHAR,
179 CONSTRAINT author_pkey PRIMARY KEY(id)
180);
181
182CREATE TABLE books (
183 id SERIAL,
184 title VARCHAR,
185 author_id INTEGER,
186 CONSTRAINT book_pkey PRIMARY KEY(id),
187 CONSTRAINT author FOREIGN KEY (author_id) REFERENCES authors(id)
188);
189
190CREATE TABLE reviews (
191 id SERIAL,
192 book_id INTEGER,
193 stars INTEGER,
194 body VARCHAR,
195 CONSTRAINT review_pkey PRIMARY KEY(id),
196 CONSTRAINT book FOREIGN KEY (book_id) REFERENCES book(id)
197);
198```
199**Pro Tip:** [Create a trigger](https://github.com/will123195/oreo/wiki/Trigger-to-populate-array) to auto-populate `author.books[]`.
200
201**Hacker Tip:** [Replicate to Redis](https://github.com/will123195/oreo/wiki/Replicate-to-Redis) so your cache is never stale.
202
203<hr />
204
205# Usage
206
207<a name="instantiate" />
208## oreo( opts, [cb] )
209
210Instantiates the `db` object and configures the database connection string(s).
211
212- **opts** {Object} options
213 - **driver** {String} `pg` or `mysql`
214 - **hosts** {Array} list of possible hosts, each is checked to see if it is online and writable or read-only
215 - **name** {String} the database name
216 - **user** {String} the username
217 - **password** {String} the password
218 - **debug** {Function} *(optional, default `false`)* set to `console.log` to see info about running queries
219 - **memoize** {Integer} *(optional, default `false`)* duration in milliseconds to cache rows in process memory. Setting this to `150` is generally a no-brainer to prevent redundant queries.
220 - **cache** {Object} *(optional, default `false`)* object with `get(key)` and/or `set(key, val)` methods (i.e. redis) to cache full rows (indefinitely). Cached rows are recached after `save()`/`insert()`/`update()`/`delete()`. The [Table functions](#table) fetch rows from the cache (and only fetch from sql the rows that are not cached).
221 - **Promise** {Object} *(optional, default `global.Promise`)* You may plug in your own Promise library that is compatible with native promises, i.e. `Promise: require('bluebird')`. Then a promise will be returned if a callback is not specified.
222 - **models** {Object} *(optional)* each table may have a model "class" specified which will be used to instantiate rows from that table. For example, `models.my_table = class MyTable {}`
223- **cb** {Function} *(optional)* callback(err)
224
225```js
226var oreo = require('oreo')
227var db = oreo({
228 driver: 'pg',
229 hosts: ['localhost:5432'],
230 name: 'database',
231 user: 'username',
232 pass: 'password',
233 //debug: false, //console.log
234 //memoize: 0,
235 //cache: null,
236 //Promise: global.Promise
237 //models: {}
238}, function (err) {
239 db.execute('select now() as now', function (err, rs) {
240 console.log('now:', rs[0].now)
241 })
242})
243```
244
245# Db
246
247<a name="execute" />
248## db.execute( sql, [data], [opts], [cb] )
249
250Executes an arbitrary SQL query.
251
252- **sql** {String|Array} the SQL statement
253- **data** {Object} *(optional, unless `options` is specified)* parameterized query data
254- **opts** {Object} *(optional)* query options
255 - **write** {Boolean} if truthy, forces query to run on master db, otherwise attempts to run on a read-only host
256 - **conString** {String} the connection string of the db
257- **cb** {Function} *(optional)* callback(err, rows) If *cb* is not provided, a Promise is returned.
258
259```js
260db.execute([
261 'select now()', // arrays can be used for es5 multi-line convenience
262 'as now'
263], function (err, rs) {
264 console.log(rs[0]) // 2014-06-24 21:03:08.652861-04
265})
266```
267
268Parameterized query (SQL injection safe):
269```js
270db.execute(`
271 select id
272 from authors
273 where name = :name
274`, {
275 name: 'Jack Kerouac',
276}, function (err, rows) {
277 console.log(rows[0].id) // 1
278})
279```
280
281If no callback is provided a Promise is returned:
282```js
283db.execute('select now()')
284 .then(function (rows) {
285
286 })
287 .catch(function (err) {
288
289 })
290```
291
292<a name="executeWrite" />
293## db.executeWrite( sql, [data], [opts], [cb] )
294
295Same as [`execute`](#execute) but executes the query on a writable (primary) host.
296
297<a name="onReady" />
298## db.onReady( cb )
299
300Queues a function to be called when oreo's schema detection is complete (i.e. when oreo is initialized).
301
302- **cb** {Function} callback()
303
304```js
305var db = oreo(config, function (err) {
306 if (err) return console.log(err)
307 console.log('Ready!')
308}).onReady(function () {
309 console.log('onReady #1')
310})
311db.onReady(function () {
312 console.log('onReady #2')
313})
314
315/*
316Output:
317onReady #1
318onReady #2
319Ready!
320*/
321```
322
323<a name="end" />
324## db.end( [cb] )
325
326Closes the db connection(s).
327
328# Table
329
330<a name="find" />
331## db.***table***.find( [opts], [cb] )
332
333Finds multiple rows.
334
335- **opts** {Object} *(optional)* options
336 - **where** {String|Array|Object} the where clause criteria
337 - **order** {String} i.e. `last_name ASC, age DESC`
338 - **limit** {Number}
339 - **offset** {Number}
340 - **hydrate** {String|Array} hydrates the specified foreign keys (see [`hydrate`](#hydrate))
341 - **params** {Object} key/value pairs to be substituted for `:key` patterns in the query
342- **cb** {Function} *(optional)* callback(err, rows) If *cb* is not provided, a Promise is returned.
343
344```js
345db.authors.find({
346 where: [ "name like 'Jack%'" ],
347 order: 'name asc',
348 offset: 5,
349 limit: 5,
350 hydrate: ['books']
351}, function (err, authors) {
352 console.log(authors)
353 // [ { id: 1, name: Jack Kerouac, books: [ { id: 1, title: On the Road, author_id: 1 } ] } ]
354})
355```
356
357The `where` option has several valid formats:
358- {String}
359
360 ```js
361 where: "field = :f1 and field2 > :f2",
362 params: {
363 f1: 'abc',
364 f2: 1
365 }
366 ```
367- {Array}
368
369 ```js
370 where: [
371 "field = :f1",
372 "field2 > :f2"
373 ],
374 params: {
375 f1: 'abc',
376 f2: 1
377 }
378 ```
379- {Object}
380
381 ```js
382 where: {
383 field: 'abc',
384 field2: { $gt: 1 } // query operators are coming soon
385 }
386 ```
387
388<a name="findOne" />
389## db.***table***.findOne( opts, [cb] )
390
391Finds exactly one row.
392
393- **opts** {Object} same options as [`find`](#find)
394- **cb** {Function} *(optional)* callback(err, row) If *cb* is not provided, a Promise is returned.
395
396```js
397db.authors.findOne({
398 where: [ "name like 'Jack%'" ],
399 order: 'name asc',
400 offset: 5
401}, function (err, author) {
402 console.log(author.id) // 1
403})
404```
405
406<a name="get" />
407## db.***table***.get( primaryKey, [opts], [cb] )
408
409Gets a row by primary key.
410
411- **primaryKey** {String|Number|Object} the primary key of the row to get
412- **opts** {Object} *(optional)* options
413 - **hydrate** {String|Array} hydrates the specified foreign keys (see [`hydrate`](#hydrate))
414- **cb** {Function} *(optional)* callback(err, row) If *cb* is not provided, a Promise is returned.
415
416```js
417var primaryKey = 1 // var primaryKey = { id: 1 } // this also works
418db.authors.get(primaryKey, function (err, author) {
419 console.log(author) // { id: 1, name: Jack Kerouak }
420})
421```
422
423Multi-column (composite) primary key:
424```js
425var primaryKey = {
426 company: 'Cogswell Cogs',
427 part_no: 'A-12345'
428}
429db.parts.get(primaryKey, function (err, part) {
430 console.log(part) // { company: Cogswell Cogs, part_no: A-12345, price: 9.99, in_stock: true }
431})
432```
433
434<a name="insert" />
435## db.***table***.insert( data, [cb] )
436
437Inserts a new row.
438
439- **data** {Object} the data to insert into the db
440- **cb** {Function} *(optional)* callback(err, row) If *cb* is not provided, a Promise is returned.
441
442```js
443db.books.insert({
444 title: 'On the Road',
445 author_id: 1
446}, function (err, book) {
447 console.log(book)
448 // { id: 1, title: On the Road, author_id: 1 }
449})
450```
451
452Insert multiple rows into related tables in a single transaction:
453```js
454db.books.insert({
455 title: 'On the Road',
456 author: { // "author" is the foreign key name (1-to-1)
457 name: 'Jack Kerouac'
458 },
459 reviews: [ // shorthand for 'book:reviews' <foreignKeyName>:<tableName> (1-to-many)
460 { stars: 5, body: 'Psychadelic!'},
461 { stars: 4, body: 'Bizarre, unpredictable yet strangely alluring.'}
462 ]
463}, function (err, book) {
464 console.log(book)
465 // { id: 1, title: On the Road, author_id: 1 }
466})
467```
468
469See also: [`hydrate`](#hydrate)
470
471<a name="mget" />
472## db.***table***.mget( primaryKeys, [opts], [cb] )
473
474Gets many rows by primary key in the specified order. A `null` value will be returned for each primary key that does not exist.
475
476- **primaryKeys** {Array} the primary keys of the rows to get
477- **opts** {Object} *(optional)* options
478 - **hydrate** {String|Array} hydrates the specified foreign keys (see [`hydrate`](#hydrate))
479- **cb** {Function} *(optional)* callback(err, rows) If *cb* is not provided, a Promise is returned.
480
481```js
482var bookIds = [1]
483db.books.mget(bookIds, function (err, books) {
484 console.log(books)
485 // [ { id: 1, title: On the Road, author_id: 1 } ]
486})
487```
488
489<a name="table_save" />
490## db.***table***.save( data, [cb] )
491
492Inserts or updates depending on whether the primary key exists in the db.
493
494- **data** {Object} the data to save to the db
495- **cb** {Function} *(optional)* callback(err, row) If *cb* is not provided, a Promise is returned.
496
497```js
498var formPOST = {
499 id: 1,
500 title: 'New Title'
501}
502db.books.save(formPOST, function (err, book) {
503 console.log(book)
504 // { id: 1, title: New Title, author_id: 1 }
505})
506```
507
508# Row
509
510<a name="delete" />
511## row.delete( [cb] )
512
513Deletes an existing row from the database.
514
515- **cb** {Function} *(optional)* callback(err) If *cb* is not provided, a Promise is returned.
516
517```js
518book.delete(function (err) {
519 console.log(book)
520 // {}
521})
522```
523
524<a name="hydrate" />
525## row.hydrate( propertyName, [cb] )
526
527Hydrates the row(s) linked with the specified foreign key(s) and/or foreign table(s).
528
529- **propertyName** {String|Array} the name of the hydratable property to fetch and attach to this row. There are two types of hydratable property names:
530 - 1-to-1 foreign key constraint name
531 - 1-to-many foreign table name
532- **cb** {Function} *(optional)* callback(err) If *cb* is not provided, a Promise is returned.
533
534```js
535db.books.get(1, function (err, book) {
536 console.log(book)
537 // { id: 1, title: On the Road, author_id: 1 }
538
539 // hydrate a 1-to-1 linked row
540 book.hydrate('author', function (err) {
541 console.log(book.author)
542 // { id: 1, name: Jack Kerouac }
543 })
544
545 // hydrate 1-to-many linked rows
546 book.hydrate('reviews', function (err) {
547 console.log(book.reviews)
548 // [ { stars: 5, body: 'Psychadelic!' }, { stars: 4, body: 'Bizarre...' } ]
549 })
550})
551```
552
553When hydrating a 1-to-1 row, the **propertyName** is the name of the foreign key constraint.
554
555For example, a book has one author, so we have a table `books` with a column `author_id` which has a foreign key constraint named `author` which links to `author.id`.
556
557```js
558// 1-to-1
559book.hydrate('author', function (err) {
560 console.log(book.author)
561 // { id: 1, name: Jack Kerouac }
562})
563```
564
565When hydrating 1-to-many rows, it is recommended to specify the fully qualified hydratable **propertyName** formatted as `foreignKeyName:tableName`. However, for convenience, if the foreign table has only one foreign key that references this table, you may omit `foreignKeyName:` and simply use `tableName` shorthand notation.
566
567For example, a book has many reviews, so we have a table `reviews` with a column `book_id` which has a foreign key constraint named `book` which links to `book.id`.
568
569```js
570// 1-to-many (fully qualified notation)
571book.hydrate('book:reviews', function (err) {
572 console.log(book['book:reviews'])
573 // [ { stars: 5, body: 'Psychadelic!' }, { stars: 4, body: 'Bizarre...' } ]
574})
575
576// 1-to-many (shorthand notation)
577book.hydrate('reviews', function (err) {
578 console.log(book.reviews)
579 // [ { stars: 5, body: 'Psychadelic!' }, { stars: 4, body: 'Bizarre...' } ]
580})
581```
582
583Hydrate multiple properties in parallel:
584
585```js
586book.hydrate(['author', 'reviews'], function (err) {
587 console.log(book)
588 // {
589 // id: 1,
590 // title: On the Road,
591 // author_id: 1,
592 // author: { id: 1, name: Jack Kerouac },
593 // reviews: [ { stars: 5, body: 'Psychadelic!' }, { stars: 4, body: 'Bizarre...' } ]
594 // }
595})
596```
597
598<a name="save" />
599## row.save( [cb] )
600
601Saves the modified property values to the database (and saves linked rows recursively).
602
603- **cb** {Function} *(optional)* callback(err, row) If *cb* is not provided, a Promise is returned.
604
605```js
606db.books.get(1, function (err, book) {
607 console.log(book)
608 // { id: 1, title: On the Road, author_id: 1 }
609 book.author_id = 2
610 book.save(function (err, book) {
611 console.log(book)
612 // { id: 1, title: On the Road, author_id: 2 }
613 })
614})
615```
616
617<a name="set" />
618## row.set( data )
619
620Modifies multiple property values but does NOT save to the db.
621
622- **data** {Object} the data to modify
623
624```js
625db.books.get(1, function (err, book) {
626 console.log(book)
627 // { id: 1, title: On the Road, author_id: 1 }
628 book.set({
629 title: 'New Title',
630 author_id: 2
631 })
632 book.save(function (err, book) {
633
634 })
635})
636```
637
638<a name="update" />
639## row.update( data, [cb] )
640
641Updates an existing row. A convenience method for `set()` then `save()`.
642
643- **data** {Object} the data to save
644- **cb** {Function} *(optional)* callback(err, row) If *cb* is not provided, a Promise is returned.
645
646```js
647book.update({
648 title: 'New Title'
649}, function (err, book) {
650 console.log(book)
651 // { id: 1, title: New Title, author_id: 1 }
652})
653```
654
655## Known Issues
656
657- Postgres tables containing `JSON` data type are not supported (use `JSONB` instead!)