diff --git a/README.md b/README.md index beaa8e70..b3f9ab73 100644 --- a/README.md +++ b/README.md @@ -545,6 +545,43 @@ CustomerRepository.find({ }); ``` +## Partial updates of JSON fields (CONCAT) + +By default, updating a property mapped to the PostgreSQL 'jsonb' data type will replace the entire JSON value. +With this enhancement, you can now perform partial updates (merges) of jsonb columns using the PostgreSQL JSONB concatenation operator (||). + + ### How it works + + If you set the value of a property to an object containing a special CONCAT key, the connector will: + +- Generate an UPDATE statement using || ?::jsonb + +- Merge the object specified in CONCAT into the existing JSONB column value, overriding fields with the same key but leaving others unchanged. + +Assuming a model property such as this: + +```ts +@property({ + type: 'object', + postgresql: { + dataType: 'jsonb' + }, +}) +address?: object; +``` + +Now perform a partial update to change only the city leaving the street intact: + +```ts +await customerRepository.updateById(customerId, { + address: { + CONCAT: { + city: 'New City' + } + } +}); +``` + ## Extended operators PostgreSQL supports the following PostgreSQL-specific operators: diff --git a/lib/postgresql.js b/lib/postgresql.js index 1b55e1d1..d7477478 100644 --- a/lib/postgresql.js +++ b/lib/postgresql.js @@ -788,7 +788,7 @@ PostgreSQL.prototype._buildWhere = function(model, where) { * @param {boolean} isWhereClause * @returns {*} The escaped value of DB column */ -PostgreSQL.prototype.toColumnValue = function(prop, val, isWhereClause) { +PostgreSQL.prototype.toColumnValue = function(prop, val, isWhereClause, fieldName) { if (val == null) { // PostgreSQL complains with NULLs in not null columns // If we have an autoincrement value, return DEFAULT instead @@ -857,7 +857,22 @@ PostgreSQL.prototype.toColumnValue = function(prop, val, isWhereClause) { } } } - + if (prop.postgresql && prop.postgresql.dataType === 'jsonb') { + // check for any json operator for updates + const jsonType = prop.postgresql.dataType; + try { + const rawData = JSON.parse(val); + if (rawData['CONCAT']) { + // If the property is a json type and partial update is enabled, + return new ParameterizedSQL({ + sql: `${fieldName} || ?::${jsonType}`, + params: [JSON.stringify(rawData['CONCAT'])], + }); + } + } catch (e) { + // do nothing + } + } return val; }; diff --git a/test/postgresql.test.js b/test/postgresql.test.js index 01a637cb..efa9bfc1 100644 --- a/test/postgresql.test.js +++ b/test/postgresql.test.js @@ -848,8 +848,13 @@ describe('postgresql connector', function() { dataType: 'json', }, }, + metadata: { + type: 'object', + postgresql: { + dataType: 'jsonb', + }, + }, }); - db.automigrate(function(err) { if (err) return done(err); Customer.createAll([{ @@ -871,7 +876,6 @@ describe('postgresql connector', function() { }); }); }); - it('allows querying for nested json properties', function(done) { Customer.find({ where: { @@ -963,6 +967,40 @@ describe('postgresql connector', function() { done(); }); }); + it('should support partial update of json data type using CONCAT', function(done) { + Customer.create({ + address: { + city: 'Old City', + street: { + number: 100, + name: 'Old Street', + }, + }, + metadata: { + extenalid: '123', + isactive: true, + }, + }, function(err, customer) { + if (err) return done(err); + const partialUpdate = { + metadata: { + CONCAT: { + isactive: false, + }, + }, + }; + + Customer.updateAll({id: customer.id}, partialUpdate, function(err) { + if (err) return done(err); + Customer.findById(customer.id, function(err, updatedCustomer) { + if (err) return done(err); + updatedCustomer.metadata.isactive.should.equal(false); + updatedCustomer.metadata.extenalid.should.equal('123'); + done(); + }); + }); + }); + }); }); it('should return array of models with id column value for createAll()', function(done) {