Skip to content
Closed

uuid #405

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 101 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,57 @@ If there is a reference to an object being deleted then the `DELETE` will fail.

**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 `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
{
"name": "Customer",
Expand All @@ -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
},
Expand All @@ -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": {
Expand All @@ -490,6 +541,53 @@ 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 have enable it with setting:

```ts
{
id: true,
type: 'String',
required: false,
// settings below are needed
generated: true,
useDefaultIdType: false,
postgresql: {
dataType: 'uuid',
},
}
```
The settings 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'
},
}
```
{% include important.html content="It is users' responsibility to make sure the provided extension and function are valid." %}

## Running tests

### Own instance
Expand Down
88 changes: 66 additions & 22 deletions lib/migration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};
Expand All @@ -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) {
Expand Down Expand Up @@ -481,7 +524,7 @@ function mixinMigration(PostgreSQL) {
default:
case 'String':
case 'JSON':
return 'TEXT';
case 'Uuid':
case 'Text':
return 'TEXT';
case 'Number':
Expand Down Expand Up @@ -645,6 +688,7 @@ function mixinMigration(PostgreSQL) {
case 'CHARACTER':
case 'CHAR':
case 'TEXT':
case 'UUID':
return 'String';

case 'BYTEA':
Expand Down
71 changes: 70 additions & 1 deletion test/postgresql.migration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
});
}