Skip to content

Commit 3273d42

Browse files
feat: partial json update
Signed-off-by: Vaibhav Bhalla <vaibhav.bhalla@sourcefuse.com>
1 parent a91abba commit 3273d42

File tree

3 files changed

+135
-1
lines changed

3 files changed

+135
-1
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,43 @@ CustomerRepository.find({
545545
});
546546
```
547547
548+
## Partial updates of JSON fields (CONCAT)
549+
550+
By default, updating a property mapped to the PostgreSQL 'jsonb' data type will replace the entire JSON value.
551+
With this enhancement, you can now perform partial updates (merges) of jsonb columns using the PostgreSQL JSONB concatenation operator (||).
552+
553+
### How it works
554+
555+
If you set the value of a property to an object containing a special CONCAT key, the connector will:
556+
557+
- Generate an UPDATE statement using || ?::jsonb
558+
559+
- Merge the object specified in CONCAT into the existing JSONB column value, overriding fields with the same key but leaving others unchanged.
560+
561+
Assuming a model property such as this:
562+
563+
```ts
564+
@property({
565+
type: 'object',
566+
postgresql: {
567+
dataType: 'jsonb'
568+
},
569+
})
570+
address?: object;
571+
```
572+
573+
Now perform a partial update to change only the city leaving the street intact:
574+
575+
```ts
576+
await customerRepository.updateById(customerId, {
577+
address: {
578+
CONCAT: {
579+
city: 'New City'
580+
}
581+
}
582+
});
583+
```
584+
548585
## Extended operators
549586
550587
PostgreSQL supports the following PostgreSQL-specific operators:

lib/postgresql.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,7 @@ PostgreSQL.prototype._buildWhere = function(model, where) {
788788
* @param {boolean} isWhereClause
789789
* @returns {*} The escaped value of DB column
790790
*/
791-
PostgreSQL.prototype.toColumnValue = function(prop, val, isWhereClause) {
791+
PostgreSQL.prototype.toColumnValue = function(prop, val, isWhereClause, fieldName) {
792792
if (val == null) {
793793
// PostgreSQL complains with NULLs in not null columns
794794
// If we have an autoincrement value, return DEFAULT instead
@@ -857,6 +857,22 @@ PostgreSQL.prototype.toColumnValue = function(prop, val, isWhereClause) {
857857
}
858858
}
859859
}
860+
if (prop.postgresql && prop.postgresql.dataType === 'jsonb') {
861+
// check for any json operator for updates
862+
const jsonType = prop.postgresql.dataType;
863+
try {
864+
const rawData = JSON.parse(val);
865+
if (rawData['CONCAT']) {
866+
// If the property is a json type and partial update is enabled,
867+
return new ParameterizedSQL({
868+
sql: `${fieldName} || ?::${jsonType}`,
869+
params: [JSON.stringify(rawData['CONCAT'])],
870+
});
871+
}
872+
} catch (e) {
873+
// do nothing
874+
}
875+
}
860876

861877
return val;
862878
};

test/postgresql.test.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,12 @@ describe('postgresql connector', function() {
848848
dataType: 'json',
849849
},
850850
},
851+
metadata: {
852+
type: 'object',
853+
postgresql: {
854+
dataType: 'jsonb',
855+
},
856+
},
851857
});
852858

853859
db.automigrate(function(err) {
@@ -963,6 +969,81 @@ describe('postgresql connector', function() {
963969
done();
964970
});
965971
});
972+
it('should support partial update of json data type using CONCAT', function(done) {
973+
Customer.create({
974+
address: {
975+
city: 'Old City',
976+
street: {
977+
number: 100,
978+
name: 'Old Street',
979+
},
980+
},
981+
metadata: {
982+
externalid: '123',
983+
isactive: true,
984+
},
985+
}, function(err, customer) {
986+
if (err) return done(err);
987+
const partialUpdate = {
988+
metadata: {
989+
CONCAT: {
990+
isactive: false,
991+
},
992+
},
993+
};
994+
995+
Customer.updateAll({id: customer.id}, partialUpdate, function(err) {
996+
if (err) return done(err);
997+
Customer.findById(customer.id, function(err, updatedCustomer) {
998+
if (err) return done(err);
999+
updatedCustomer.metadata.isactive.should.equal(false);
1000+
updatedCustomer.metadata.externalid.should.equal('123');
1001+
done();
1002+
});
1003+
});
1004+
});
1005+
});
1006+
1007+
it('should support update of multiple fields in jsonb data type using CONCAT', function(done) {
1008+
Customer.create({
1009+
address: {
1010+
city: 'Test City',
1011+
street: {
1012+
number: 200,
1013+
name: 'Test Street',
1014+
},
1015+
},
1016+
metadata: {
1017+
externalid: '456',
1018+
isactive: true,
1019+
status: 'pending',
1020+
priority: 'low',
1021+
},
1022+
}, function(err, customer) {
1023+
if (err) return done(err);
1024+
const partialUpdate = {
1025+
metadata: {
1026+
CONCAT: {
1027+
isactive: false,
1028+
status: 'completed',
1029+
priority: 'high',
1030+
},
1031+
},
1032+
};
1033+
1034+
Customer.updateAll({id: customer.id}, partialUpdate, function(err) {
1035+
if (err) return done(err);
1036+
Customer.findById(customer.id, function(err, updatedCustomer) {
1037+
if (err) return done(err);
1038+
updatedCustomer.metadata.isactive.should.equal(false);
1039+
updatedCustomer.metadata.status.should.equal('completed');
1040+
updatedCustomer.metadata.priority.should.equal('high');
1041+
updatedCustomer.metadata.externalid.should.equal('456');
1042+
done();
1043+
});
1044+
});
1045+
});
1046+
});
9661047
});
9671048

9681049
it('should return array of models with id column value for createAll()', function(done) {

0 commit comments

Comments
 (0)