diff --git a/README.md b/README.md index a69e384b..6a87dd89 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,17 @@ $ npm install pg-promise ### 1. Load the library ```javascript -var pgpLib = require('pg-promise'); +var pgpLib = require('pg-promise'); // loading the library; ``` -### 2. Configure database connection -Use one of the two ways to specify connection: +### 2. Initialize the library +```javascript +var pgp = pgpLib(/*options*/); // initializing the library, with optional global settings; +``` +You can pass additional ```options``` parameter when initilizing the library (see chapter Advanced for details). + +NOTE: Only one instance of such ```pgp``` object should exist throughout the application. +### 3. Configure database connection +Use one of the two ways to specify connection details: * Configuration object: ```javascript var cn = { @@ -35,19 +42,23 @@ var cn = "postgres://username:password@host:port/database"; This library doesn't use any of the connection's details, it simply passes them on to [PG] when opening a new connection. For more details see [ConnectionParameters] class in [PG], such as additional connection properties supported. -### 3. Initialize the library +### 4. Instantiate your database ```javascript -var pgp = pgpLib(cn); +var db = new pgp(cn); // create a new database instance based on the connection details ``` -NOTE: Only one global instance should be used throughout the application. +There can be multiple database objects instantiated in the application from different connection details. -See also chapter Advanced for the use of parameter ```options``` during initialization. But for now you are ready to use the library. +You are now ready to make queries against the database. # Usage ### The basics In order to eliminate the chances of unexpected query results and make code more robust, each request is parametrized with the expected/supported Query Result Mask, using type ```queryResult``` as shown below: ```javascript +/////////////////////////////////////////////////////// +// Query Result Mask flags; +// +// Any combination is supported, except for one + many. queryResult = { one: 1, // single-row result is expected; many: 2, // multi-row result is expected; @@ -56,18 +67,17 @@ queryResult = { ``` In the following generic-query example we indicate that the call can return any number of rows: ```javascript -pgp.query("select * from users", queryResult.none | queryResult.many); +db.query("select * from users", queryResult.none | queryResult.many); ``` which is equivalent to calling: ```javascript -pgp.manyOrNone("select * from users"); +db.manyOrNone("select * from users"); ``` - This usage pattern is facilitated through result-specific methods that can be used instead of the generic query: ```javascript -pgp.many("select * from users"); // one or more records are expected -pgp.one("select * from users limit 1"); // one record is expected -pgp.none("update users set active=TRUE where id=1"); // no records expected +db.many("select * from users"); // one or more records are expected +db.one("select * from users limit 1"); // one record is expected +db.none("update users set active=TRUE where id=1"); // no records expected ``` The mixed-result methods are: * ```oneOrNone``` - expects 1 or 0 rows to be returned; @@ -76,7 +86,7 @@ The mixed-result methods are: Each of the query calls returns a [Promise] object, as shown below, to be used in the standard way. And when the expected and actual results do not match, the call will be rejected. ```javascript -pgp.many("select * from users") +db.many("select * from users") .then(function(data){ console.log(data); // printing the data returned }, function(reason){ @@ -89,7 +99,7 @@ In PostgreSQL stored procedures are just functions that usually do not return an Suppose we want to call function ```findAudit``` to find audit records by user id and maximum timestamp. We can make such call as shown below: ```javascript -pgp.func('findAudit', [123, new Date()]) +db.func('findAudit', [123, new Date()]) .then(function(data){ console.log(data); // printing the data returned }, function(reason){ @@ -99,7 +109,7 @@ pgp.func('findAudit', [123, new Date()]) We passed it user id = 123, plus current Date/Time as the timestamp. We assume that the function signature matches the parameters that we passed. All values passed are serialized automatically to comply with PostgreSQL type formats. -And when you are not expecting any return results, call ```pgp.proc``` instead. Both methods return a [Promise] object. +And when you are not expecting any return results, call ```db.proc``` instead. Both methods return a [Promise] object. ### Transactions Every call shown in chapters above would acquire a new connection from the pool and release it when done. In order to execute a transaction on the same @@ -109,7 +119,7 @@ Example: ```javascript var promise = require('promise'); -var tx = new pgp.tx(); // creating a new transaction object +var tx = new db.tx(); // creating a new transaction object tx.exec(function(/*client*/){ @@ -133,7 +143,7 @@ we want both queries inside the transaction to resolve before executing a COM Notes * While inside a transaction, we make calls to the same-named methods as outside of transactions, except we do it on the transaction object instance now, -as opposed to the global ```pgp``` object, which gives us access to the shared connection object. The same goes for calling functions and procedures within +as opposed to the database object ```db```, which gives us access to the shared connection object. The same goes for calling functions and procedures within transactions, using ```tx.func``` and ```tx.proc``` accordingly. * Just for flexibility, the transaction call-back function takes parameter ```client``` - the connection object. @@ -149,7 +159,7 @@ pgp.as.text(value); // returns proper PostgreSQL text presentation, pgp.as.date(value); // returns proper PostgreSQL date/time presentation, // wrapped in quotes. ``` -As these helpers are not associated with a connection, and thus can be called from anywhere. +As these helpers are not associated with a database, they can be called from anywhere. # Advanced @@ -166,7 +176,7 @@ var options = { console.log("Disconnected from database '" + cp.database + "'"); } }; -var pgp = pgpLib(cn, options); +var pgp = pgpLib(options); ``` Two events supported at the moment - ```connect``` and ```disconnect```, to notify of virtual connections being established or released accordingly. Each event takes parameter ```client```, which is the client connection object. These events are mostly for connection monitoring, while debugging your application. @@ -185,13 +195,13 @@ doing it for you automatically. Usage example: ```javascript -pgp.connect().then(function(db){ +db.connect().then(function(info){ // connection was established successfully; - // do stuff with the connection object (db.client) and/or queries; + // do stuff with the connection object (info.client) and/or queries; // when done with all the queries, call done(): - db.done(); + info.done(); }, function(reason){ // failed to connect; @@ -201,6 +211,7 @@ pgp.connect().then(function(db){ NOTE: When using the direct connection, events ```connect``` and ```disconnect``` won't be fired. # History +* Version 0.2.0 introduced on March 6th, 2015, supporting multiple databases * A refined version 0.1.4 released on March 5th, 2015. * First solid Beta, 0.1.2 on March 4th, 2015. * It reached first Beta version 0.1.0 on March 4th, 2015. diff --git a/index.js b/index.js index b41e4a95..3281f0f3 100644 --- a/index.js +++ b/index.js @@ -6,11 +6,10 @@ var npm = { pg: require('pg') }; -////////////////////////////////////// -// Query result mask flags; +/////////////////////////////////////////////////////// +// Query Result Mask flags; // -// NOTE: Cannot combine one + many, while the -// rest of combinations are all supported. +// Any combination is supported, except for one + many. queryResult = { one: 1, // single-row result is expected; many: 2, // multi-row result is expected; @@ -22,347 +21,391 @@ queryResult = { // // Parameters: // -// 1. cn (required) - either configuration object or connection string. -// It is merely passed on to PG and not used by this library. -// 2. options (optional) - -// { -// connect: function(client){ -// on-connect event; -// client - pg connection object. -// }, -// disconnect: function(client){ -// on-disconnect event; -// client - pg connection object. -// } +// options (optional) - +// { +// connect: function(client){ +// on-connect event; +// client - pg connection object. +// }, +// disconnect: function(client){ +// on-disconnect event; +// client - pg connection object. // } -module.exports = function (cn, options) { +// } +module.exports = function (options) { + + var lib = function (cn) { + if(!$isEmptyObject(this)){ + // This makes it easy to locate the most common mistake - + // skipping keyword 'new' when calling: var db = new pgp(cn); + throw new Error("Invalid database object instantiation."); + } + if (!cn) { + throw new Error("Invalid 'cn' parameter passed."); + } + return dbInit(this, cn, options); + }; - if (!cn) { - throw new Error("Invalid 'cn' parameter passed."); - } + // Exposing PG library instance, just for flexibility. + lib.pg = npm.pg; - // simpler promise instantiation; - var $p = function (func) { - return new npm.promise(func); + // Terminates pg library; call it when exiting the application. + lib.end = function () { + npm.pg.end(); }; - var $self = { + // Namespace for type conversion helpers; + lib.as = { + bool: function (val) { + if ($isNull(val)) { + return 'null'; + } + return val ? 'TRUE' : 'FALSE'; + }, + text: function (val) { + if ($isNull(val)) { + return 'null'; + } + return $wrapText($fixQuotes(val)); + }, + date: function (val) { + if ($isNull(val)) { + return 'null'; + } + if (val instanceof Date) { + return $wrapText(val.toUTCString()); + } else { + throw new Error($wrapText(val) + " doesn't represent a valid Date object or value"); + } + } + }; - ///////////////////////////////////////////////////////////// - // PG library instance; - // Exposing it just for flexibility. - pg: npm.pg, + return lib; +}; - ///////////////////////////////////////////////////////////// - // Connects to the database; - // The caller must invoke done() after requests are finished. - connect: function () { - return $p(function (resolve, reject) { - npm.pg.connect(cn, function (err, client, done) { - if (err) { - reject(err); - } else { - resolve({ - client: client, - done: done - }); +function dbInit(dbInst, cn, options) { + + // Handles database connection acquire/release + // events, notifying the client as needed. + function monitor(open, db) { + if (open) { + if (options) { + var func = options.connect; + if (func) { + if (typeof(func) !== 'function') { + throw new Error('Function was expected for options.connect'); } - }); - }); - }, - - /////////////////////////////////////////////////////////////// - // Terminates pg library; call it when exiting the application. - end: function () { - npm.pg.end(); - }, + func(db.client); // notify the client; + } + } + } else { + if (options) { + var func = options.disconnect; + if (func) { + if (typeof(func) !== 'function') { + throw new Error('Function was expected for options.disconnect'); + } + func(db.client); // notify the client; + } + } + db.done(); // release database connection back to the pool; + } + } - ////////////////////////////////////////////////////////////// - // Generic query request; - // qrm is Query Result Mask, combination of queryResult flags. - query: function (query, qrm) { - return $p(function (resolve, reject) { - $self.connect() - .then(function (db) { - _global.monitor(true, db); - _global.query(db.client, query, qrm) - .then(function (data) { - _global.monitor(false, db); - resolve(data); - }, function (reason) { - _global.monitor(false, db); - reject(reason); - }); - }, function (reason) { - reject(reason); // connection failed; + ///////////////////////////////////////////////////////////////// + // Connects to the database; + // The caller must invoke done() after all requests are finished. + dbInst.connect = function () { + return $p(function (resolve, reject) { + npm.pg.connect(cn, function (err, client, done) { + if (err) { + reject(err); + } else { + resolve({ + client: client, + done: done }); + } }); - }, + }); + }; - none: function (query) { - return this.query(query, queryResult.none); - }, + ////////////////////////////////////////////////////////////// + // Generic query request; + // qrm is Query Result Mask, combination of queryResult flags. + dbInst.query = function (query, qrm) { + return $p(function (resolve, reject) { + dbInst.connect() + .then(function (db) { + monitor(true, db); + $query(db.client, query, qrm) + .then(function (data) { + monitor(false, db); + resolve(data); + }, function (reason) { + monitor(false, db); + reject(reason); + }); + }, function (reason) { + reject(reason); // connection failed; + }); + }); + }; - one: function (query) { - return this.query(query, queryResult.one); - }, + dbInst.none = function (query) { + return dbInst.query(query, queryResult.none); + }; - many: function (query) { - return this.query(query, queryResult.many); - }, + dbInst.one = function (query) { + return dbInst.query(query, queryResult.one); + }; - oneOrNone: function (query) { - return this.query(query, queryResult.one | queryResult.none); - }, + dbInst.many = function (query) { + return dbInst.query(query, queryResult.many); + }; - manyOrNone: function (query) { - return this.query(query, queryResult.many | queryResult.none); - }, + dbInst.oneOrNone = function (query) { + return dbInst.query(query, queryResult.one | queryResult.none); + }; - func: function (funcName, params) { - return this.one(_global.createFuncQuery(funcName, params)); - }, + dbInst.manyOrNone = function (query) { + return dbInst.query(query, queryResult.many | queryResult.none); + }; - proc: function (procName, params) { - return this.oneOrNone(_global.createFuncQuery(procName, params)); - }, + dbInst.func = function (funcName, params, qrm) { + var query = $createFuncQuery(funcName, params); + if (qrm) { + return dbInst.query(query); + } else { + return dbInst.one(query); + } + }; - // Namespace for type conversion helpers; - as: { - bool: function (val) { - if (_global.isNull(val)) { - return 'null'; - } - return val ? 'TRUE' : 'FALSE'; + dbInst.proc = function (procName, params) { + return dbInst.oneOrNone($createFuncQuery(procName, params)); + }; + + ////////////////////////// + // Transaction class; + dbInst.tx = function () { + + var tx = this; + + if(!$isEmptyObject(tx)){ + // This makes it easy to locate the most common mistake - + // skipping keyword 'new' when calling: var tx = new db.tx(cn); + throw new Error("Invalid transaction object instantiation."); + } + + var local = { + db: null, + start: function (db) { + this.db = db; + monitor(true, db); }, - text: function (val) { - if (_global.isNull(val)) { - return 'null'; - } - return _global.wrapText(_global.fixQuotes(val)); + finish: function () { + monitor(false, this.db); + this.db = null; }, - date: function (val) { - if (_global.isNull(val)) { - return 'null'; + call: function (cb) { + if (typeof(cb) !== 'function') { + return npm.promise.reject("Cannot invoke tx.exec() without a callback function."); } - if (val instanceof Date) { - return _global.wrapText(val.toUTCString()); + var result = cb(this.db.client); + if (result && typeof(result.then) === 'function') { + return result; } else { - throw new Error(_global.wrapText(val) + " doesn't represent a valid Date object or value"); + return npm.promise.reject("Callback function passed into tx.exec() didn't return a valid promise object."); } } - }, + }; - // Transaction class; - tx: function () { - - var tx = this; - - var _local = { - db: null, - start: function (db) { - this.db = db; - _global.monitor(true, db); - }, - finish: function () { - _global.monitor(false, this.db); - this.db = null; - }, - call: function (cb) { - if (typeof(cb) !== 'function') { - return npm.promise.reject("Cannot invoke tx.exec() without a callback function."); - } - var result = cb(this.db.client); - if (result && typeof(result.then) === 'function') { - return result; - } else { - return npm.promise.reject("Callback function passed into tx.exec() didn't return a valid promise object."); - } - } - }; - - tx.exec = function (cb) { - if (_local.db) { - throw new Error("Previous call to tx.exec() hasn't finished."); - } - var t_data, t_reason, success; - return $p(function (resolve, reject) { - $self.connect() - .then(function (db) { - _local.start(db); - return tx.query('begin', queryResult.none); - }, function (reason) { - reject(reason); // connection issue; - }) - .then(function () { - _local.call(cb) - .then(function (data) { - t_data = data; - success = true; - return tx.query('commit', queryResult.none); - }, function (reason) { - t_reason = reason; - success = false; - return tx.query('rollback', queryResult.none); - }) - .then(function () { - _local.finish(); - // either commit or rollback successfully executed; - if (success) { - resolve(t_data); - } else { - reject(t_reason); - } - }, function (reason) { - // either commit or rollback failed; - _local.finish(); - reject(reason); - }); - }, function (reason) { - _local.finish(); - reject(reason); // issue with 'begin' command; - }); - }); - }; + // Executes transaction; + tx.exec = function (cb) { + if (local.db) { + throw new Error("Previous call to tx.exec() hasn't finished."); + } + var t_data, t_reason, success; + return $p(function (resolve, reject) { + dbInst.connect() + .then(function (db) { + local.start(db); + return tx.query('begin', queryResult.none); + }, function (reason) { + reject(reason); // connection issue; + }) + .then(function () { + local.call(cb) + .then(function (data) { + t_data = data; + success = true; + return tx.query('commit', queryResult.none); + }, function (reason) { + t_reason = reason; + success = false; + return tx.query('rollback', queryResult.none); + }) + .then(function () { + local.finish(); + // either commit or rollback successfully executed; + if (success) { + resolve(t_data); + } else { + reject(t_reason); + } + }, function (reason) { + // either commit or rollback failed; + local.finish(); + reject(reason); + }); + }, function (reason) { + local.finish(); + reject(reason); // issue with 'begin' command; + }); + }); + }; - tx.query = function (query, qrm) { - if (!_local.db) { - throw new Error('Unexpected call outside of transaction'); - } - return _global.query(_local.db.client, query, qrm); - }; + tx.query = function (query, qrm) { + if (!local.db) { + throw new Error('Unexpected call outside of transaction'); + } + return $query(local.db.client, query, qrm); + }; - tx.none = function (query) { - return tx.query(query, queryResult.none); - }; + tx.none = function (query) { + return tx.query(query, queryResult.none); + }; - tx.one = function (query) { - return tx.query(query, queryResult.one); - }; + tx.one = function (query) { + return tx.query(query, queryResult.one); + }; - tx.many = function (query) { - return tx.query(query, queryResult.many); - }; + tx.many = function (query) { + return tx.query(query, queryResult.many); + }; - tx.oneOrNone = function (query) { - return tx.query(query, queryResult.one | queryResult.none); - }; + tx.oneOrNone = function (query) { + return tx.query(query, queryResult.one | queryResult.none); + }; - tx.manyOrNone = function (query) { - return tx.query(query, queryResult.many | queryResult.none); - }; + tx.manyOrNone = function (query) { + return tx.query(query, queryResult.many | queryResult.none); + }; - tx.func = function (funcName, params) { - return tx.one(_global.createFuncQuery(funcName, params)); - }; + tx.func = function (funcName, params, qrm) { + var query = $createFuncQuery(funcName, params); + if (qrm) { + return tx.query(query); + } else { + return tx.one(query); + } + }; - tx.proc = function (procName, params) { - return tx.oneOrNone(_global.createFuncQuery(procName, params)); - }; - } + tx.proc = function (procName, params) { + return tx.oneOrNone($createFuncQuery(procName, params)); + }; }; +} - var _global = { - options: options, - isNull: function (val) { - return typeof(val) === 'undefined' || val === null; - }, - fixQuotes: function (val) { - return val.replace("'", "''"); - }, - wrapText: function (text) { - return "'" + text + "'"; - }, - wrapValue: function (val) { - if (this.isNull(val)) { - return 'null'; - } - switch (typeof(val)) { - case 'string': - return $self.as.text(val); - case 'boolean': - return $self.as.bool(val); - default: - if (val instanceof Date) { - return $self.as.date(val); - } else { - return val; - } - } - }, - monitor: function (open, db) { - if (open) { - if (this.options) { - var func = this.options.connect; - if (func) { - if (typeof(func) !== 'function') { - throw new Error('Function was expected for options.connect'); - } - func(db.client); - } - } +//////////////////////////////////////////////// +// Global, reusable functions, all start with $ + +// Simpler promise instantiation; +function $p(func) { + return new npm.promise(func); +} + +// Null verification; +function $isNull(val) { + return typeof(val) === 'undefined' || val === null; +} + +// Checks if the object is empty (has no properties); +function $isEmptyObject(obj){ + return Object.keys(obj).length === 0; +} + +// Fixes single-quote symbols in text fields; +function $fixQuotes(val) { + return val.replace("'", "''"); +} + +// Wraps up text in single quotes; +function $wrapText(text) { + return "'" + text + "'"; +} + +// Translates a javascript value into its text presentation, +// according to the type, compatible with PostgreSQL format. +function $wrapValue(val) { + if ($isNull(val)) { + return 'null'; + } + switch (typeof(val)) { + case 'string': + return dbInst.as.text(val); + case 'boolean': + return dbInst.as.bool(val); + default: + if (val instanceof Date) { + return dbInst.as.date(val); } else { - if (this.options) { - var func = this.options.disconnect; - if (func) { - if (typeof(func) !== 'function') { - throw new Error('Function was expected for options.disconnect'); - } - func(db.client); - } - } - db.done(); + return val; } - }, - formatValues: function (values) { - var s = ''; - if (Array.isArray(values) && values.length > 0) { - for (var i = 0; i < values.length; i++) { - if (i > 0) { - s += ','; - } - s += this.wrapValue(values[i]); - } + } +} + +// Formats array of javascript-type values into a list of +// parameters for a function call, compatible with PostgreSQL. +function $formatValues(values) { + var s = ''; + if (Array.isArray(values) && values.length > 0) { + for (var i = 0; i < values.length; i++) { + if (i > 0) { + s += ','; } - return s; - }, - createFuncQuery: function (funcName, params) { - return 'select * from ' + funcName + '(' + this.formatValues(params) + ');'; - }, - query: function (client, query, qrm) { - return $p(function (resolve, reject) { - var badMask = queryResult.one | queryResult.many; - if ((qrm & badMask) === badMask) { - reject("Invalid query result mask: one + many"); + s += $wrapValue(values[i]); + } + } + return s; +} + +// Formats a proper function call from the parameters. +function $createFuncQuery(funcName, params) { + return 'select * from ' + funcName + '(' + $formatValues(params) + ');'; +} + +// Generic, static query call for the specified connection + query + result. +function $query(client, query, qrm) { + return $p(function (resolve, reject) { + var badMask = queryResult.one | queryResult.many; + if ((qrm & badMask) === badMask) { + reject("Invalid query result mask: one + many"); + } else { + client.query(query, function (err, result) { + if (err) { + reject(err.message); } else { - client.query(query, function (err, result) { - if (err) { - reject(err.message); + var data = result.rows; + var l = result.rows.length; + if (l) { + if (l > 1 && !(qrm & queryResult.many)) { + reject("Single row was expected from query: '" + query + "'"); } else { - var data = result.rows; - var l = result.rows.length; - if (l) { - if (l > 1 && !(qrm & queryResult.many)) { - reject("Single row was expected from query: '" + query + "'"); - } else { - if (!(qrm & queryResult.many)) { - data = result.rows[0]; - } - } - } else { - if (qrm & queryResult.none) { - data = null; - } else { - reject("No rows returned from query: '" + query + "'"); - } + if (!(qrm & queryResult.many)) { + data = result.rows[0]; } - resolve(data); } - }); + } else { + if (qrm & queryResult.none) { + data = null; + } else { + reject("No rows returned from query: '" + query + "'"); + } + } + resolve(data); } }); } - }; - - return $self; -}; + }); +} diff --git a/package.json b/package.json index 07beb3bf..6b7cdc98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pg-promise", - "version": "0.1.5", + "version": "0.2.0", "description": "PG + Promise made easy, with transactions support.", "main": "index.js", "scripts": { @@ -15,10 +15,10 @@ "url": "https://github.com/vitaly-t/pg-promise/issues" }, "keywords": [ - "postgresql", "pg", + "promise", "transaction", - "promise" + "postgresql" ], "author": { "name": "Vitaly Tomilov",