Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 17 additions & 2 deletions lib/postgresql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
};

Expand Down
42 changes: 40 additions & 2 deletions test/postgresql.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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([{
Expand All @@ -871,7 +876,6 @@ describe('postgresql connector', function() {
});
});
});

it('allows querying for nested json properties', function(done) {
Customer.find({
where: {
Expand Down Expand Up @@ -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) {
Expand Down