diff --git a/README.md b/README.md index fa7765cb..9d0aa524 100644 --- a/README.md +++ b/README.md @@ -436,7 +436,58 @@ Foreign key constraints can be defined in the model `options`. Removing or updat If there is a reference to an object being deleted then the `DELETE` will fail. Likewise if there is a create with an invalid FK id then the `POST` will fail. -**Note**: The order of table creation is important. A referenced table must exist before creating a foreign key constraint. +**Note**: The order of table creation is important. A referenced table must exist before creating a foreign key constraint. + +For **LoopBack 4** users, define your models under the `models/` folder as follows: + +`customer.model.ts`: + +```ts +@model() +export class Customer extends Entity { + @property({ + id: true, + type: 'Number', + required: false, + length: 20 + }) + id: number; + + @property({ + type: 'string', + length: 20 + }) + name: string; +} +``` +`order.model.ts`: + +```ts +@model() +export class Order extends Entity { + @property({ + id: true, + type: 'Number', + required: false, + length: 20 + }) + id: number; + + @property({ + type: 'string', + length: 20 + }) + name: string; + + @property({ + type: 'Number', + length: 20 + }) + customerId: number; +} +``` + +For **LoopBack 3** users, you can define your model JSON schema as follows: ```json { @@ -446,7 +497,7 @@ If there is a reference to an object being deleted then the `DELETE` will fail. }, "properties": { "id": { - "type": "String", + "type": "Number", "length": 20, "id": 1 }, @@ -473,12 +524,12 @@ If there is a reference to an object being deleted then the `DELETE` will fail. }, "properties": { "id": { - "type": "String", + "type": "Number", "length": 20, "id": 1 }, "customerId": { - "type": "String", + "type": "Number", "length": 20 }, "description": { @@ -490,6 +541,54 @@ If there is a reference to an object being deleted then the `DELETE` will fail. } ``` +Auto-migrate supports the automatic generation of property values. For PostgreSQL, the default id type is _integer_. If you have `generated: true` in the id property, it generates integers by default: + +```ts +{ + id: true, + type: 'Number', + required: false, + generated: true // enables auto-generation +} +``` + +It is common to use UUIDs as the primary key in PostgreSQL instead of integers. You can enable it with the following settings: + +```ts +{ + id: true, + type: 'String', + required: false, + // settings below are needed + generated: true, + useDefaultIdType: false, + postgresql: { + dataType: 'uuid', + }, +} +``` +The setting uses `uuid-ossp` extension and `uuid_generate_v4()` function as default. + +If you'd like to use other extensions and functions, you can do: + +```ts +{ + id: true, + type: 'String', + required: false, + // settings below are needed + generated: true, + useDefaultIdType: false, + postgresql: { + dataType: 'uuid', + extension: 'myExtension', + defaultFn: 'myuuid' + }, +} +``` + +WARNING: It is the users' responsibility to make sure the provided extension and function are valid. + ## Running tests ### Own instance diff --git a/lib/migration.js b/lib/migration.js index d682a8af..2e16d0cf 100644 --- a/lib/migration.js +++ b/lib/migration.js @@ -7,6 +7,7 @@ const SG = require('strong-globalize'); const g = SG(); const async = require('async'); +const chalk = require('chalk'); const debug = require('debug')('loopback:connector:postgresql:migration'); module.exports = mixinMigration; @@ -295,12 +296,33 @@ function mixinMigration(PostgreSQL) { const self = this; const modelDef = this.getModelDefinition(model); const prop = modelDef.properties[propName]; + let result = self.columnDataType(model, propName); + + // checks if dataType is set to uuid + let postgDefaultFn; + let postgType; + const postgSettings = prop.postgresql; + if (postgSettings && postgSettings.dataType) { + postgType = postgSettings.dataType.toUpperCase(); + } + if (prop.generated) { - return 'SERIAL'; + if (result === 'INTEGER') { + return 'SERIAL'; + } else if (postgType === 'UUID') { + if (postgSettings && postgSettings.defaultFn && postgSettings.extension) { + // if user provides their own extension and function + postgDefaultFn = postgSettings.defaultFn; + return result + ' NOT NULL' + ' DEFAULT ' + postgDefaultFn; + } + return result + ' NOT NULL' + ' DEFAULT uuid_generate_v4()'; + } else { + console.log(chalk.red('>>> WARNING: ') + + `auto-generation is not supported for type "${chalk.yellow(prop.type)}". \ + Please add your own function to the table "${chalk.yellow(model)}".`); + } } - let result = self.columnDataType(model, propName); if (!self.isNullable(prop)) result = result + ' NOT NULL'; - result += self.columnDbDefault(model, propName); return result; }; @@ -313,32 +335,53 @@ function mixinMigration(PostgreSQL) { PostgreSQL.prototype.createTable = function(model, cb) { const self = this; const name = self.tableEscaped(model); + const modelDef = this.getModelDefinition(model); + + // collects all extensions needed to be created + let createExtensions; + Object.keys(this.getModelDefinition(model).properties).forEach(function(propName) { + const prop = modelDef.properties[propName]; + + // checks if dataType is set to uuid + const postgSettings = prop.postgresql; + if (postgSettings && postgSettings.dataType && postgSettings.dataType === 'UUID' + && postgSettings.defaultFn && postgSettings.extension) { + createExtensions += 'CREATE EXTENSION IF NOT EXISTS "' + postgSettings.extension + '";'; + } + }); + // default extension + if (!createExtensions) { + createExtensions = 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'; + } // Please note IF NOT EXISTS is introduced in postgresql v9.3 - self.execute('CREATE SCHEMA ' + + self.execute( + createExtensions + + 'CREATE SCHEMA ' + self.escapeName(self.schema(model)), - function(err) { - if (err && err.code !== '42P06') { - return cb && cb(err); - } - self.execute('CREATE TABLE ' + name + ' (\n ' + - self.propertiesSQL(model) + '\n)', - function(err, info) { - if (err) { - return cb(err, info); + function(err) { + if (err && err.code !== '42P06') { + return cb && cb(err); } - self.addIndexes(model, undefined, function(err) { + self.execute('CREATE TABLE ' + name + ' (\n ' + + self.propertiesSQL(model) + '\n)', + function(err, info) { if (err) { - return cb(err); + return cb(err, info); } - const fkSQL = self.getForeignKeySQL(model, - self.getModelDefinition(model).settings.foreignKeys); - self.addForeignKeys(model, fkSQL, function(err, result) { - cb(err); + self.addIndexes(model, undefined, function(err) { + if (err) { + return cb(err); + } + const fkSQL = self.getForeignKeySQL(model, + self.getModelDefinition(model).settings.foreignKeys); + self.addForeignKeys(model, fkSQL, function(err, result) { + cb(err); + }); }); }); - }); - }); + }, + ); }; PostgreSQL.prototype.buildIndex = function(model, property) { @@ -481,7 +524,7 @@ function mixinMigration(PostgreSQL) { default: case 'String': case 'JSON': - return 'TEXT'; + case 'Uuid': case 'Text': return 'TEXT'; case 'Number': @@ -645,6 +688,7 @@ function mixinMigration(PostgreSQL) { case 'CHARACTER': case 'CHAR': case 'TEXT': + case 'UUID': return 'String'; case 'BYTEA': diff --git a/test/postgresql.migration.test.js b/test/postgresql.migration.test.js index 4c432cd1..fa9ff0da 100644 --- a/test/postgresql.migration.test.js +++ b/test/postgresql.migration.test.js @@ -14,7 +14,7 @@ describe('migrations', function() { before(setup); it('should run migration', function(done) { - db.automigrate('UserDataWithIndexes', done); + db.automigrate(['UserDataWithIndexes', 'OrderData', 'DefaultUuid'], done); }); it('UserDataWithIndexes should have correct indexes', function(done) { @@ -73,6 +73,42 @@ describe('migrations', function() { done(); }); }); + + it('OrderData should have correct prop type uuid with custom generation function', function(done) { + checkColumns('OrderData', function(err, cols) { + assert.deepEqual(cols, { + ordercode: + {column_name: 'ordercode', + column_default: 'uuid_generate_v1()', + data_type: 'uuid'}, + ordername: + {column_name: 'ordername', + column_default: null, + data_type: 'text'}, + id: + {column_name: 'id', + column_default: 'nextval(\'orderdata_id_seq\'::regclass)', + data_type: 'integer'}, + }); + done(); + }); + }); + + it('DefaultUuid should have correct id type uuid and default function v4', function(done) { + checkColumns('DefaultUuid', function(err, cols) { + assert.deepEqual(cols, { + defaultcode: + {column_name: 'defaultcode', + column_default: 'uuid_generate_v4()', + data_type: 'uuid'}, + id: + {column_name: 'id', + column_default: 'nextval(\'defaultuuid_id_seq\'::regclass)', + data_type: 'integer'}, + }); + done(); + }); + }); }); function setup(done) { @@ -118,6 +154,23 @@ function setup(done) { }, }, }); + const OrderData = db.define('OrderData', { + ordercode: {type: 'String', required: true, generated: true, useDefaultIdType: false, + postgresql: { + dataType: 'uuid', + defaultFn: 'uuid_generate_v1()', + extension: 'uuid-ossp', + }}, + ordername: {type: 'String'}, + }); + + const DefaultUuid = db.define('DefaultUuid', { + defaultCode: {type: 'String', required: true, generated: true, useDefaultIdType: false, + postgresql: { + dataType: 'uuid', + defaultFn: 'uuid_generate_v1()', // lack extension + }}, + }); done(); } @@ -161,3 +214,19 @@ function table(model) { function query(sql, cb) { db.adapter.query(sql, cb); } + +function checkColumns(table, cb) { + const tableName = table.toLowerCase(); + query('SELECT column_name, column_default, data_type FROM information_schema.columns \ + WHERE(table_schema, table_name) = (\'public\', \'' + tableName + '\');', + function(err, data) { + const cols = {}; + if (!err) { + data.forEach(function(index) { + cols[index.column_name] = index; + delete index.name; + }); + } + cb(err, cols); + }); +}