Skip to content

Commit 656ce28

Browse files
author
Agnes Lin
committed
fix: allow string type id to be auto-generated
1 parent e3c03e6 commit 656ce28

File tree

3 files changed

+239
-27
lines changed

3 files changed

+239
-27
lines changed

README.md

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,58 @@ Foreign key constraints can be defined in the model `options`. Removing or updat
436436

437437
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.
438438

439-
**Note**: The order of table creation is important. A referenced table must exist before creating a foreign key constraint.
439+
**Note**: The order of table creation is important. A referenced table must exist before creating a foreign key constraint.
440+
441+
For **LoopBack 4** users, define your models under the `models/` folder as follows:
442+
443+
`customer.model.ts`:
444+
445+
```ts
446+
@model()
447+
export class Customer extends Entity {
448+
@property({
449+
id: true,
450+
type: 'Number',
451+
required: false,
452+
length: 20
453+
})
454+
id: number;
455+
456+
@property({
457+
type: 'string',
458+
length: 20
459+
})
460+
name: string;
461+
}
462+
```
463+
`order.model.ts`:
464+
465+
```ts
466+
@model()
467+
export class Order extends Entity {
468+
@property({
469+
id: true,
470+
type: 'Number',
471+
required: false,
472+
length: 20
473+
})
474+
id: number;
475+
476+
@property({
477+
type: 'string',
478+
length: 20
479+
})
480+
name: string;
481+
482+
@property({
483+
type: 'Number',
484+
length: 20
485+
})
486+
customerId: number;
487+
}
488+
```
489+
490+
For **LoopBack 3** users, you can define your model JSON schema as follows:
440491

441492
```json
442493
{
@@ -446,7 +497,7 @@ If there is a reference to an object being deleted then the `DELETE` will fail.
446497
},
447498
"properties": {
448499
"id": {
449-
"type": "String",
500+
"type": "Number",
450501
"length": 20,
451502
"id": 1
452503
},
@@ -473,12 +524,12 @@ If there is a reference to an object being deleted then the `DELETE` will fail.
473524
},
474525
"properties": {
475526
"id": {
476-
"type": "String",
527+
"type": "Number",
477528
"length": 20,
478529
"id": 1
479530
},
480531
"customerId": {
481-
"type": "String",
532+
"type": "Number",
482533
"length": 20
483534
},
484535
"description": {
@@ -490,6 +541,54 @@ If there is a reference to an object being deleted then the `DELETE` will fail.
490541
}
491542
```
492543

544+
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:
545+
546+
```ts
547+
{
548+
id: true,
549+
type: 'Number',
550+
required: false,
551+
generated: true // enables auto-generation
552+
}
553+
```
554+
555+
It is common to use UUIDs as the primary key in PostgreSQL instead of integers. You can enable it with the following settings:
556+
557+
```ts
558+
{
559+
id: true,
560+
type: 'String',
561+
required: false,
562+
// settings below are needed
563+
generated: true,
564+
useDefaultIdType: false,
565+
postgresql: {
566+
dataType: 'uuid',
567+
},
568+
}
569+
```
570+
The setting uses `uuid-ossp` extension and `uuid_generate_v4()` function as default.
571+
572+
If you'd like to use other extensions and functions, you can do:
573+
574+
```ts
575+
{
576+
id: true,
577+
type: 'String',
578+
required: false,
579+
// settings below are needed
580+
generated: true,
581+
useDefaultIdType: false,
582+
postgresql: {
583+
dataType: 'uuid',
584+
extension: 'myExtension',
585+
defaultFn: 'myuuid'
586+
},
587+
}
588+
```
589+
590+
WARNING: It is the users' responsibility to make sure the provided extension and function are valid.
591+
493592
## Running tests
494593

495594
### Own instance

lib/migration.js

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const SG = require('strong-globalize');
88
const g = SG();
99
const async = require('async');
10+
const chalk = require('chalk');
1011
const debug = require('debug')('loopback:connector:postgresql:migration');
1112

1213
module.exports = mixinMigration;
@@ -295,12 +296,33 @@ function mixinMigration(PostgreSQL) {
295296
const self = this;
296297
const modelDef = this.getModelDefinition(model);
297298
const prop = modelDef.properties[propName];
299+
let result = self.columnDataType(model, propName);
300+
301+
// checks if dataType is set to uuid
302+
let postgDefaultFn;
303+
let postgType;
304+
const postgSettings = prop.postgresql;
305+
if (postgSettings && postgSettings.dataType) {
306+
postgType = postgSettings.dataType.toUpperCase();
307+
}
308+
298309
if (prop.generated) {
299-
return 'SERIAL';
310+
if (result === 'INTEGER') {
311+
return 'SERIAL';
312+
} else if (postgType === 'UUID') {
313+
if (postgSettings && postgSettings.defaultFn && postgSettings.extension) {
314+
// if user provides their own extension and function
315+
postgDefaultFn = postgSettings.defaultFn;
316+
return result + ' NOT NULL' + ' DEFAULT ' + postgDefaultFn;
317+
}
318+
return result + ' NOT NULL' + ' DEFAULT uuid_generate_v4()';
319+
} else {
320+
console.log(chalk.red('>>> WARNING: ') +
321+
`auto-generation is not supported for type "${chalk.yellow(prop.type)}". \
322+
Please add your own function to the table "${chalk.yellow(model)}".`);
323+
}
300324
}
301-
let result = self.columnDataType(model, propName);
302325
if (!self.isNullable(prop)) result = result + ' NOT NULL';
303-
304326
result += self.columnDbDefault(model, propName);
305327
return result;
306328
};
@@ -313,32 +335,53 @@ function mixinMigration(PostgreSQL) {
313335
PostgreSQL.prototype.createTable = function(model, cb) {
314336
const self = this;
315337
const name = self.tableEscaped(model);
338+
const modelDef = this.getModelDefinition(model);
339+
340+
// collects all extensions needed to be created
341+
let createExtensions;
342+
Object.keys(this.getModelDefinition(model).properties).forEach(function(propName) {
343+
const prop = modelDef.properties[propName];
344+
345+
// checks if dataType is set to uuid
346+
const postgSettings = prop.postgresql;
347+
if (postgSettings && postgSettings.dataType && postgSettings.dataType === 'UUID'
348+
&& postgSettings.defaultFn && postgSettings.extension) {
349+
createExtensions += 'CREATE EXTENSION IF NOT EXISTS "' + postgSettings.extension + '";';
350+
}
351+
});
352+
// default extension
353+
if (!createExtensions) {
354+
createExtensions = 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";';
355+
}
316356

317357
// Please note IF NOT EXISTS is introduced in postgresql v9.3
318-
self.execute('CREATE SCHEMA ' +
358+
self.execute(
359+
createExtensions +
360+
'CREATE SCHEMA ' +
319361
self.escapeName(self.schema(model)),
320-
function(err) {
321-
if (err && err.code !== '42P06') {
322-
return cb && cb(err);
323-
}
324-
self.execute('CREATE TABLE ' + name + ' (\n ' +
325-
self.propertiesSQL(model) + '\n)',
326-
function(err, info) {
327-
if (err) {
328-
return cb(err, info);
362+
function(err) {
363+
if (err && err.code !== '42P06') {
364+
return cb && cb(err);
329365
}
330-
self.addIndexes(model, undefined, function(err) {
366+
self.execute('CREATE TABLE ' + name + ' (\n ' +
367+
self.propertiesSQL(model) + '\n)',
368+
function(err, info) {
331369
if (err) {
332-
return cb(err);
370+
return cb(err, info);
333371
}
334-
const fkSQL = self.getForeignKeySQL(model,
335-
self.getModelDefinition(model).settings.foreignKeys);
336-
self.addForeignKeys(model, fkSQL, function(err, result) {
337-
cb(err);
372+
self.addIndexes(model, undefined, function(err) {
373+
if (err) {
374+
return cb(err);
375+
}
376+
const fkSQL = self.getForeignKeySQL(model,
377+
self.getModelDefinition(model).settings.foreignKeys);
378+
self.addForeignKeys(model, fkSQL, function(err, result) {
379+
cb(err);
380+
});
338381
});
339382
});
340-
});
341-
});
383+
},
384+
);
342385
};
343386

344387
PostgreSQL.prototype.buildIndex = function(model, property) {
@@ -481,7 +524,7 @@ function mixinMigration(PostgreSQL) {
481524
default:
482525
case 'String':
483526
case 'JSON':
484-
return 'TEXT';
527+
case 'Uuid':
485528
case 'Text':
486529
return 'TEXT';
487530
case 'Number':
@@ -645,6 +688,7 @@ function mixinMigration(PostgreSQL) {
645688
case 'CHARACTER':
646689
case 'CHAR':
647690
case 'TEXT':
691+
case 'UUID':
648692
return 'String';
649693

650694
case 'BYTEA':

test/postgresql.migration.test.js

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('migrations', function() {
1414
before(setup);
1515

1616
it('should run migration', function(done) {
17-
db.automigrate('UserDataWithIndexes', done);
17+
db.automigrate(['UserDataWithIndexes', 'OrderData', 'DefaultUuid'], done);
1818
});
1919

2020
it('UserDataWithIndexes should have correct indexes', function(done) {
@@ -73,6 +73,42 @@ describe('migrations', function() {
7373
done();
7474
});
7575
});
76+
77+
it('OrderData should have correct prop type uuid with custom generation function', function(done) {
78+
checkColumns('OrderData', function(err, cols) {
79+
assert.deepEqual(cols, {
80+
ordercode:
81+
{column_name: 'ordercode',
82+
column_default: 'uuid_generate_v1()',
83+
data_type: 'uuid'},
84+
ordername:
85+
{column_name: 'ordername',
86+
column_default: null,
87+
data_type: 'text'},
88+
id:
89+
{column_name: 'id',
90+
column_default: 'nextval(\'orderdata_id_seq\'::regclass)',
91+
data_type: 'integer'},
92+
});
93+
done();
94+
});
95+
});
96+
97+
it('DefaultUuid should have correct id type uuid and default function v4', function(done) {
98+
checkColumns('DefaultUuid', function(err, cols) {
99+
assert.deepEqual(cols, {
100+
defaultcode:
101+
{column_name: 'defaultcode',
102+
column_default: 'uuid_generate_v4()',
103+
data_type: 'uuid'},
104+
id:
105+
{column_name: 'id',
106+
column_default: 'nextval(\'defaultuuid_id_seq\'::regclass)',
107+
data_type: 'integer'},
108+
});
109+
done();
110+
});
111+
});
76112
});
77113

78114
function setup(done) {
@@ -118,6 +154,23 @@ function setup(done) {
118154
},
119155
},
120156
});
157+
const OrderData = db.define('OrderData', {
158+
ordercode: {type: 'String', required: true, generated: true, useDefaultIdType: false,
159+
postgresql: {
160+
dataType: 'uuid',
161+
defaultFn: 'uuid_generate_v1()',
162+
extension: 'uuid-ossp',
163+
}},
164+
ordername: {type: 'String'},
165+
});
166+
167+
const DefaultUuid = db.define('DefaultUuid', {
168+
defaultCode: {type: 'String', required: true, generated: true, useDefaultIdType: false,
169+
postgresql: {
170+
dataType: 'uuid',
171+
defaultFn: 'uuid_generate_v1()', // lack extension
172+
}},
173+
});
121174

122175
done();
123176
}
@@ -161,3 +214,19 @@ function table(model) {
161214
function query(sql, cb) {
162215
db.adapter.query(sql, cb);
163216
}
217+
218+
function checkColumns(table, cb) {
219+
const tableName = table.toLowerCase();
220+
query('SELECT column_name, column_default, data_type FROM information_schema.columns \
221+
WHERE(table_schema, table_name) = (\'public\', \'' + tableName + '\');',
222+
function(err, data) {
223+
const cols = {};
224+
if (!err) {
225+
data.forEach(function(index) {
226+
cols[index.column_name] = index;
227+
delete index.name;
228+
});
229+
}
230+
cb(err, cols);
231+
});
232+
}

0 commit comments

Comments
 (0)