diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..ccc915a7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,36 @@ + + +### Bug or feature request + + + +- [ ] Bug +- [ ] Feature request + +### Description of feature (or steps to reproduce if bug) + + + +### Link to sample repo to reproduce issue (if bug) + + + +### Expected result + + + +### Actual result (if bug) + + + +### Additional information (Node.js version, LoopBack version, etc) + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..d2b240f5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ +### Description + + +#### Related issues + + + +- None + +### Checklist + + + +- [ ] New tests added or existing tests modified to cover all changes +- [ ] Code conforms with the [style + guide](http://loopback.io/doc/en/contrib/style-guide.html) diff --git a/CHANGES.md b/CHANGES.md index 0000c628..ba918808 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,45 @@ +2018-01-12, Version 2.9.0 +========================= + + * Use pg@7 (Robin Biondi) + + * drop node 0.10 and 0.12 support (Diana Lau) + + * fix 2.x CI (Diana Lau) + + +2017-02-24, Version 2.8.0 +========================= + + * Add test for bulk transactions (Zak Barbuto) + + * Use pg callback over connection.release (#109) (Zak Barbuto) + + * Use pool.pool.release over pool.release (#109) (Zak Barbuto) + + * Add test env information to README (Zak Barbuto) + + * update README for local postgres setup (Diana Lau) + + * Update postgresql.js (tmclouisluk) + + * Fix bug when using postgresql 8.x (tmclouisluk) + + * Use unique param for affectedRows (Loay) + + * Move info from docs into README (#199) (Rand McKinney) + + * Update paid support URL (Siddhi Pai) + + * Revert loopback 2.x (siddhipai) + + * Revert dev-dependency on loopback to 2.x (Siddhi Pai) + + * Set publish tag to "lts" (Siddhi Pai) + + * Update README with correct doc links, etc (Amir Jafarian) + + 2016-10-14, Version 2.7.0 ========================= diff --git a/README.md b/README.md index ee2a927e..33815beb 100644 --- a/README.md +++ b/README.md @@ -1,182 +1,394 @@ # loopback-connector-postgresql -The official PostgreSQL connector for the LoopBack framework. +[PostgreSQL](https://www.postgresql.org/), is a popular open-source object-relational database. +The `loopback-connector-postgresql` module is the PostgreSQL connector for the LoopBack framework. -Please see the [official documentation](http://loopback.io/doc/en/lb2/PostgreSQL-connector.html). +
For more information, see the documentation. +

+NOTE: The PostgreSQL connector requires PostgreSQL 8.x or 9.x. +
+## Installation -## Connector settings +In your application root directory, enter this command to install the connector: -The connector can be configured using the following settings from the data source. -* url: The URL to the database, such as 'postgres://test:mypassword@localhost:5432/dev' -* host or hostname (default to 'localhost'): The host name or ip address of the PostgreSQL DB server -* port (default to 5432): The port number of the PostgreSQL DB server -* username or user: The user name to connect to the PostgreSQL DB -* password: The password -* database: The PostgreSQL database name -* debug (default to false) +```shell +$ npm install loopback-connector-postgresql --save +``` -**NOTE**: By default, the 'public' schema is used for all tables. +This installs the module from npm and adds it as a dependency to the application's `package.json` file. -The PostgreSQL connector uses [node-postgres](https://github.com/brianc/node-postgres) as the driver. See more -information about configuration parameters, check [https://github.com/brianc/node-postgres/wiki/Client#constructors](https://github.com/brianc/node-postgres/wiki/Client#constructors). +If you create a PostgreSQL data source using the data source generator as described below, you don't have to do this, since the generator will run `npm install` for you. -## Discovering Models +## Creating a data source -PostgreSQL data sources allow you to discover model definition information from existing postgresql databases. See the following APIs: +Use the [Data source generator](http://loopback.io/doc/en/lb3/Data-source-generator.html) to add a PostgreSQL data source to your application. +The generator will prompt for the database server hostname, port, and other settings +required to connect to a PostgreSQL database. It will also run the `npm install` command above for you. - - [dataSource.discoverModelDefinitions([username], fn)](https://github.com/strongloop/loopback#datasourcediscovermodeldefinitionsusername-fn) - - [dataSource.discoverSchema([owner], name, fn)](https://github.com/strongloop/loopback#datasourcediscoverschemaowner-name-fn) +The entry in the application's `/server/datasources.json` will look like this: +{% include code-caption.html content="/server/datasources.json" %} +```javascript +"mydb": { + "name": "mydb", + "connector": "postgresql" -## Model definition for PostgreSQL + "host": "mydbhost", + "port": 5432, + "url": "postgres://admin:admin@myhost/db", + "database": "db1", + "password": "admin", + "user": "admin" +} +``` -The model definition consists of the following properties: +Edit `datasources.json` to add other properties that enable you to connect the data source to a PostgreSQL database. + +### Properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeDescription
connectorString + Connector name, either "loopback-connector-postgresql" or "postgresql" +
databaseStringDatabase name
debugBooleanIf true, turn on verbose mode to debug database queries and lifecycle.
hostStringDatabase host name
passwordStringPassword to connect to database
portNumberDatabase TCP port
urlStringUse instead of thehost,port,user,password, + anddatabaseproperties. For example:'postgres://test:mypassword@localhost:5432/dev'. +
usernameStringUsername to connect to database
-* name: Name of the model, by default, it's the camel case of the table -* options: Model level operations and mapping to PostgreSQL schema/table -* properties: Property definitions, including mapping to PostgreSQL column +**NOTE**: By default, the 'public' schema is used for all tables. -```json +The PostgreSQL connector uses [node-postgres](https://github.com/brianc/node-postgres) as the driver. For more +information about configuration parameters, see [node-postgres documentation](https://github.com/brianc/node-postgres/wiki/Client#constructors). + +### Connecting to UNIX domain socket + +A common PostgreSQL configuration is to connect to the UNIX domain socket `/var/run/postgresql/.s.PGSQL.5432` instead of using the TCP/IP port. For example: + +```javascript +{ + "postgres": { + "host": "/var/run/postgresql/", + "port": "5432", + "database": "dbname", + "username": "dbuser", + "password": "dbpassword", + "name": "postgres", + "debug": true, + "connector": "postgresql" + } +} +``` - {"name": "Inventory", "options": { - "idInjection": false, +## Defining models + +The model definition consists of the following properties. + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDefaultDescription
nameCamel-case of the database table nameName of the model.
optionsN/AModel level operations and mapping to PostgreSQL schema/table
propertiesN/AProperty definitions, including mapping to PostgreSQL column
+ +For example: + +{% include code-caption.html content="/common/models/model.json" %} +```javascript +{ + "name": "Inventory", + "options": { + "idInjection": false, + "postgresql": { + "schema": "strongloop", + "table": "inventory" + } + }, + "properties": { + "id": { + "type": "String", + "required": false, + "length": 64, + "precision": null, + "scale": null, "postgresql": { - "schema": "strongloop", - "table": "inventory" + "columnName": "id", + "dataType": "character varying", + "dataLength": 64, + "dataPrecision": null, + "dataScale": null, + "nullable": "NO" } - }, "properties": { - "id": { - "type": "String", - "required": false, - "length": 64, - "precision": null, - "scale": null, - "postgresql": { - "columnName": "id", - "dataType": "character varying", - "dataLength": 64, - "dataPrecision": null, - "dataScale": null, - "nullable": "NO" - } - }, - "productId": { - "type": "String", - "required": false, - "length": 20, - "precision": null, - "scale": null, - "id": 1, - "postgresql": { - "columnName": "product_id", - "dataType": "character varying", - "dataLength": 20, - "dataPrecision": null, - "dataScale": null, - "nullable": "YES" - } - }, - "locationId": { - "type": "String", - "required": false, - "length": 20, - "precision": null, - "scale": null, - "id": 1, - "postgresql": { - "columnName": "location_id", - "dataType": "character varying", - "dataLength": 20, - "dataPrecision": null, - "dataScale": null, - "nullable": "YES" - } - }, - "available": { - "type": "Number", - "required": false, - "length": null, - "precision": 32, - "scale": 0, - "postgresql": { - "columnName": "available", - "dataType": "integer", - "dataLength": null, - "dataPrecision": 32, - "dataScale": 0, - "nullable": "YES" - } - }, - "total": { - "type": "Number", - "required": false, - "length": null, - "precision": 32, - "scale": 0, - "postgresql": { - "columnName": "total", - "dataType": "integer", - "dataLength": null, - "dataPrecision": 32, - "dataScale": 0, - "nullable": "YES" - } + }, + "productId": { + "type": "String", + "required": false, + "length": 20, + "precision": null, + "scale": null, + "id": 1, + "postgresql": { + "columnName": "product_id", + "dataType": "character varying", + "dataLength": 20, + "dataPrecision": null, + "dataScale": null, + "nullable": "YES" } - }} - + }, + "locationId": { + "type": "String", + "required": false, + "length": 20, + "precision": null, + "scale": null, + "id": 1, + "postgresql": { + "columnName": "location_id", + "dataType": "character varying", + "dataLength": 20, + "dataPrecision": null, + "dataScale": null, + "nullable": "YES" + } + }, + "available": { + "type": "Number", + "required": false, + "length": null, + "precision": 32, + "scale": 0, + "postgresql": { + "columnName": "available", + "dataType": "integer", + "dataLength": null, + "dataPrecision": 32, + "dataScale": 0, + "nullable": "YES" + } + }, + "total": { + "type": "Number", + "required": false, + "length": null, + "precision": 32, + "scale": 0, + "postgresql": { + "columnName": "total", + "dataType": "integer", + "dataLength": null, + "dataPrecision": 32, + "dataScale": 0, + "nullable": "YES" + } + } + } +} ``` -## Type Mapping +## Type mapping + +See [LoopBack types](http://loopback.io/doc/en/lb3/LoopBack-types.html) for details on LoopBack's data types. + +### LoopBack to PostgreSQL types + + + + + + + + + + + + + + + + + + + + + + + + +
LoopBack TypePostgreSQL Type
String
JSON
Text
Default
+ VARCHAR2
+ Default length is 1024 +
NumberINTEGER
DateTIMESTAMP WITH TIME ZONE
BooleanBOOLEAN
+ +### PostgreSQL types to LoopBack + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PostgreSQL TypeLoopBack Type
BOOLEANBoolean
+ VARCHAR
CHARACTER VARYING
CHARACTER
CHAR
TEXT +
String
BYTEANode.js Buffer object
SMALLINT
INTEGER
BIGINT
DECIMAL
NUMERIC
REAL
DOUBLE
SERIAL
BIGSERIAL
Number
DATE
TIMESTAMP
TIME
Date
POINTGeoPoint
+ +## Discovery and auto-migration + +### Model discovery + +The PostgreSQL connector supports _model discovery_ that enables you to create LoopBack models +based on an existing database schema using the unified [database discovery API](http://apidocs.strongloop.com/loopback-datasource-juggler/#datasource-prototype-discoverandbuildmodels). For more information on discovery, see [Discovering models from relational databases](https://loopback.io/doc/en/lb3/Discovering-models-from-relational-databases.html). + +### Auto-migratiion + +The PostgreSQL connector also supports _auto-migration_ that enables you to create a database schema +from LoopBack models using the [LoopBack automigrate method](http://apidocs.strongloop.com/loopback-datasource-juggler/#datasource-prototype-automigrate). + +For more information on auto-migration, see [Creating a database schema from models](https://loopback.io/doc/en/lb3/Creating-a-database-schema-from-models.html) for more information. + +LoopBack PostgreSQL connector creates the following schema objects for a given +model: a table, for example, PRODUCT under the 'public' schema within the database. - - Number - - Boolean - - String - - Object - - Date - - Array - - Buffer +The auto-migrate method: -### JSON to PostgreSQL Types +* Defines a primary key for the properties whose `id` property is true (or a positive number). +* Creates a column with 'SERIAL' type if the `generated` property of the `id` property is true. -* String|JSON|Text|default: VARCHAR, default length is 1024 -* Number: INTEGER -* Date: TIMESTAMP WITH TIME ZONE -* Timestamp: TIMESTAMP WITH TIME ZONE -* Boolean: BOOLEAN +Destroying models may result in errors due to foreign key integrity. First delete any related models by calling delete on models with relationships. -### PostgreSQL Types to JSON +## Running tests -* BOOLEAN: Boolean -* VARCHAR|CHARACTER VARYING|CHARACTER|CHAR|TEXT: String -* BYTEA: Binary; -* SMALLINT|INTEGER|BIGINT|DECIMAL|NUMERIC|REAL|DOUBLE|SERIAL|BIGSERIAL: Number -* DATE|TIMESTAMP|TIME: Date -* POINT: GeoPoint +The tests in this repository are mainly integration tests, meaning you will need to run them using our preconfigured test server. -## Destroying Models +1. Ask a core developer for instructions on how to set up test server + credentials on your machine +2. `npm test` -Destroying models may result in errors due to foreign key integrity. Make sure -to delete any related models first before calling delete on model's with -relationships. +If you wish to run the tests using your own test database instance, -## Auto Migrate / Auto Update +__Set up the database__ -After making changes to your model properties you must call `Model.automigrate()` -or `Model.autoupdate()`. Only call `Model.automigrate()` on new models -as it will drop existing tables. +1. Go to pgAdmin. +By default, the local database is one of the servers under Server Groups > Servers. +2. Under Login Roles, add a user called ```strongloop```. -LoopBack PostgreSQL connector creates the following schema objects for a given -model: +__Change configuration for database connection__ + +In ```test\init.js```, change the value of ```config``` to be pointing to the local database. For example, +``` + var config = { + host: 'localhost', + port: '5432', + database:'strongloop', + username: 'postgres', + password: 'postgres', + }; +``` -* A table, for example, PRODUCT under the 'public' schema within the database +2. (`Linux Only`) `CI=true PGHOST=localhost PGPORT= PGDATABASE= PGUSER= PGPASSWORD= npm test` +__Troubleshooting__ -## Running tests +When running `npm test`, it runs the ```pretest.js``` which eventually runs ```schema.sql``` to set up the database and tables. +If there is problem, you can run the ```schema.sql``` manually. To do this: -The tests in this repository are mainly integration tests, meaning you will need -to run them using our preconfigured test server. +1. Go to SQL Shell (psql) +2. Run: +``` +\i <> -1. Ask a core developer for instructions on how to set up test server - credentials on your machine -2. `npm test` +For example on Windows, +\i c:\somepath\test\schema.sql +``` \ No newline at end of file diff --git a/lib/postgresql.js b/lib/postgresql.js index 329a4ef6..13d78f0a 100644 --- a/lib/postgresql.js +++ b/lib/postgresql.js @@ -153,7 +153,7 @@ PostgreSQL.prototype.executeSQL = function(sql, params, options, callback) { switch (data.command) { case 'DELETE': case 'UPDATE': - result = {count: data.rowCount}; + result = {affectedRows: data.rowCount, count: data.rowCount}; break; default: result = data.rows; @@ -387,10 +387,10 @@ PostgreSQL.prototype.buildExpression = function(columnName, operator, operatorValue, propertyDefinition) { switch (operator) { case 'like': - return new ParameterizedSQL(columnName + " LIKE ? ESCAPE '\\'", + return new ParameterizedSQL(columnName + " LIKE ? ESCAPE E'\\\\'", [operatorValue]); case 'nlike': - return new ParameterizedSQL(columnName + " NOT LIKE ? ESCAPE '\\'", + return new ParameterizedSQL(columnName + " NOT LIKE ? ESCAPE E'\\\\'", [operatorValue]); case 'regexp': if (operatorValue.global) @@ -520,7 +520,7 @@ PostgreSQL.prototype.getPlaceholderForValue = function(key) { }; PostgreSQL.prototype.getCountForAffectedRows = function(model, info) { - return info && info.count; + return info && info.affectedRows; }; require('./discovery')(PostgreSQL); diff --git a/lib/transaction.js b/lib/transaction.js index 953dee3b..58aa8f66 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -21,6 +21,7 @@ function mixinTransaction(PostgreSQL) { debug('Begin a transaction with isolation level: %s', isolationLevel); this.pg.connect(function(err, connection, done) { if (err) return cb(err); + connection.autorelease = done; connection.query('BEGIN TRANSACTION ISOLATION LEVEL ' + isolationLevel, function(err) { if (err) return cb(err); @@ -63,16 +64,11 @@ function mixinTransaction(PostgreSQL) { }; PostgreSQL.prototype.releaseConnection = function(connection, err) { - if (typeof connection.release === 'function') { - connection.release(err); - connection.release = null; + if (typeof connection.autorelease === 'function') { + connection.autorelease(err); + connection.autorelease = null; } else { - var pool = this.pg; - if (err) { - pool.destroy(connection); - } else { - pool.release(connection); - } + connection.release(); } }; } diff --git a/package.json b/package.json index 1b00ba1a..6127c56b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "loopback-connector-postgresql", - "version": "2.7.0", + "version": "2.9.0", + "publishConfig": { + "tag": "lts" + }, "description": "Loopback PostgreSQL Connector", "keywords": [ "StrongLoop", @@ -9,6 +12,9 @@ "DataSource", "Connector" ], + "engines": { + "node": ">=4" + }, "main": "index.js", "scripts": { "pretest": "node pretest.js", @@ -21,13 +27,13 @@ "bluebird": "^3.4.6", "debug": "^2.1.1", "loopback-connector": "^2.1.0", - "pg": "^6.0.0", + "pg": "^7.0.0", "strong-globalize": "^2.6.2" }, "devDependencies": { "eslint": "^2.13.1", "eslint-config-loopback": "^4.0.0", - "loopback-datasource-juggler": "^3.0.0", + "loopback-datasource-juggler": "^2.53.0", "mocha": "^2.1.0", "rc": "^1.0.0", "should": "^8.0.2", diff --git a/test/init.js b/test/init.js index e67b256b..9f4fe0ad 100644 --- a/test/init.js +++ b/test/init.js @@ -43,9 +43,14 @@ global.getDBConfig = function(useUrl) { }; return settings; }; + +var db; global.getDataSource = global.getSchema = function(useUrl) { + // Return cached data source if possible to avoid too many client error + // due to multiple instances of connection pools + if (!useUrl && db) return db; var settings = getDBConfig(useUrl); - var db = new DataSource(require('../'), settings); + db = new DataSource(require('../'), settings); db.log = function(a) { // console.log(a); }; diff --git a/test/postgresql.dbdefaults.test.js b/test/postgresql.dbdefaults.test.js index e3755563..633302cc 100644 --- a/test/postgresql.dbdefaults.test.js +++ b/test/postgresql.dbdefaults.test.js @@ -34,7 +34,7 @@ describe('database default field values', function() { created: { type: 'Date', postgresql: { - dbDefault: "'5'", + dbDefault: '\'5\'', }, }, }); @@ -49,6 +49,9 @@ describe('database default field values', function() { it('should report inconsistent default values used', function(done) { db.automigrate('PostWithInvalidDbDefaultValue', function(err) { should.exists(err); + // The InvalidDefaults test is polluting the default date + // types of the other tests! + delete db.connector._models.PostWithInvalidDbDefaultValue; done(); }); }); diff --git a/test/postgresql.initialization.test.js b/test/postgresql.initialization.test.js new file mode 100644 index 00000000..ab83eaa9 --- /dev/null +++ b/test/postgresql.initialization.test.js @@ -0,0 +1,70 @@ +'use strict'; +require('./init'); +var Promise = require('bluebird'); +var connector = require('..'); +var DataSource = require('loopback-datasource-juggler').DataSource; +var should = require('should'); + +// simple wrapper that uses JSON.parse(JSON.stringify()) as cheap clone +function newConfig(withURL) { + return JSON.parse(JSON.stringify(getDBConfig(withURL))); +} + +describe('initialization', function() { + it('honours user-defined pg-pool settings', function() { + var dataSource = new DataSource(connector, newConfig()); + var pool = dataSource.connector.pg; + pool.options.max.should.not.equal(999); + + var settings = newConfig(); + settings.max = 999; // non-default value + var dataSource = new DataSource(connector, settings); + var pool = dataSource.connector.pg; + pool.options.max.should.equal(999); + }); + + it('honours user-defined url settings', function() { + var settings = newConfig(); + + var dataSource = new DataSource(connector, settings); + var clientConfig = dataSource.connector.clientConfig; + should.not.exist(clientConfig.connectionString); + + settings = newConfig(true); + var dataSource = new DataSource(connector, settings); + var clientConfig = dataSource.connector.clientConfig; + clientConfig.connectionString.should.equal(settings.url); + }); +}); + +describe('postgresql connector errors', function() { + it('Should complete these 4 queries without dying', function(done) { + var dataSource = getDataSource(); + var db = dataSource.connector; + var pool = db.pg; + pool.options.max = 5; + var errors = 0; + var shouldGet = 0; + function runErrorQuery() { + shouldGet++; + return new Promise(function(resolve, reject) { + db.executeSQL("SELECT 'asd'+1 ", [], {}, function(err, res) { + if (err) { + errors++; + resolve(err); + } else { + reject(res); // this should always error + } + }); + }); + }; + var ps = []; + for (var i = 0; i < 12; i++) { + ps.push(runErrorQuery()); + } + Promise.all(ps).then(function() { + shouldGet.should.equal(errors); + done(); + }); + }); +}); diff --git a/test/postgresql.test.js b/test/postgresql.test.js index f8893b7b..ecf05800 100644 --- a/test/postgresql.test.js +++ b/test/postgresql.test.js @@ -90,6 +90,21 @@ describe('postgresql connector', function() { }); }); + it('should preserve property `count` after query execution', function(done) { + Post.create( + {title: 'T10', content: 'C10'}, + function(err, p) { + if (err) return done(err); + post = p; + var query = "UPDATE PostWithBoolean SET title ='T20' WHERE id=" + post.id; + db.connector.execute(query, function(err, results) { + results.should.have.property('count', 1); + results.should.have.property('affectedRows', 1); + done(err); + }); + }); + }); + it('should support updating boolean types with false value', function(done) { Post.update({id: post.id}, {approved: false}, function(err) { should.not.exists(err); diff --git a/test/postgresql.transaction.test.js b/test/postgresql.transaction.test.js index ce4783aa..a9926498 100644 --- a/test/postgresql.transaction.test.js +++ b/test/postgresql.transaction.test.js @@ -58,6 +58,41 @@ describe('transactions', function() { }; } + describe('bulk', function() { + it('should work with bulk transactions', function(done) { + var completed = 0; + var concurrent = 20; + for (var i = 0; i <= concurrent; i++) { + var post = {title: 'tb' + i, content: 'cb' + i}; + var create = createPostInTx(post); + Transaction.begin(db.connector, Transaction.SERIALIZABLE, + function(err, tx) { + if (err) return done(err); + Post.create(post, {transaction: tx}, + function(err, p) { + if (err) { + done(err); + } else { + tx.commit(function(err) { + if (err) { + done(err); + } + completed++; + checkResults(); + }); + } + }); + }); + } + + function checkResults() { + if (completed === concurrent) { + done(); + } + } + }); + }); + describe('commit', function() { var post = {title: 't1', content: 'c1'}; before(createPostInTx(post));