Skip to content

Commit

Permalink
fix: reload attribute if instance updated with jsonMerge (#426)
Browse files Browse the repository at this point in the history
  • Loading branch information
cyjake authored Sep 14, 2024
1 parent 20c0761 commit a8fe6c7
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 15 deletions.
2 changes: 1 addition & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![Build Status](https://github.com/cyjake/leoric/actions/workflows/nodejs.yml/badge.svg)](https://github.com/cyjake/leoric/actions/workflows/nodejs.yml)
[![codecov](https://codecov.io/gh/cyjake/leoric/branch/master/graph/badge.svg?token=OZZWTZTDS1)](https://codecov.io/gh/cyjake/leoric)

Leoric is an object-relational mapping for Node.js, which is heavily influenced by Active Record of Ruby on Rails. See the [documentation](https://leoric.js.org) for detail.
Leoric is an object-relational mapping library for Node.js, which is heavily influenced by Active Record of Ruby on Rails. See the [documentation](https://leoric.js.org) for detail.

## Usage

Expand Down
1 change: 1 addition & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ description: > # this means to ignore newlines until "baseurl:"
An object-relational mapping for Node.js which is heavily influenced by Active Record.
# Build settings
permalink: pretty
kramdown:
toc_levels: 2..3
theme: jekyll-theme-primer
Expand Down
45 changes: 37 additions & 8 deletions src/bone.js
Original file line number Diff line number Diff line change
Expand Up @@ -682,15 +682,28 @@ class Bone {
* @memberof Bone
*/
async jsonMerge(name, jsonValue, options = {}) {
const raw = new Raw(`JSON_MERGE_PATCH(${name}, '${JSON.stringify(jsonValue)}')`);
const affectedRows = await this.update({ [name]: raw }, options);
const Model = this.constructor;
const { primaryKey, shardingKey } = Model;
if (this[primaryKey] == null) throw new Error(`unset primary key ${primaryKey}`);

const { preserve, ...restOptions } = options;
const where = { [primaryKey]: this[primaryKey] };
if (shardingKey) where[shardingKey] = this[shardingKey];

const affectedRows = await Model.jsonMerge(where, { [name]: jsonValue }, options);
// reload only the updated attribute, incase overwriting others
if (affectedRows > 0) {
const spell = Model._find(where, restOptions).$select(name).$get(0);
spell.scopes = [];
const instance = await spell;
if (instance) this.attribute(name, instance.attribute(name));
}

return affectedRows;
}

async jsonMergePreserve(name, jsonValue, options = {}) {
const raw = new Raw(`JSON_MERGE_PRESERVE(${name}, '${JSON.stringify(jsonValue)}')`);
const affectedRows = await this.update({ [name]: raw }, options);
return affectedRows;
return await this.jsonMerge(name, jsonValue, { ...options, preserve: true });
}

/**
Expand All @@ -707,9 +720,10 @@ class Bone {
const { fields = [] } = options;
if (typeof values === 'object') {
for (const name in values) {
if (values[name] !== undefined && this.hasAttribute(name) && (!fields.length || fields.includes(name))) {
const value = values[name];
if (value !== undefined && !(value instanceof Raw) && this.hasAttribute(name) && (!fields.length || fields.includes(name))) {
// exec custom setters in case it exist
this[name] = values[name];
this[name] = value;
changes[name] = this.attribute(name);
}
}
Expand Down Expand Up @@ -767,7 +781,8 @@ class Bone {
return await spell.later(result => {
// sync changes (changes has been formatted by custom setters, use this.attribute(name, value) directly)
for (const key in changes) {
this.attribute(key, changes[key]);
const value = changes[key];
if (!(value instanceof Raw)) this.attribute(key, value);
}
this.syncRaw();
return result.affectedRows;
Expand Down Expand Up @@ -1560,6 +1575,20 @@ class Bone {
});
}

static jsonMerge(conditions, values = {}, options = {}) {
const { preserve, ...restOptions } = options;
const method = preserve ? 'JSON_MERGE_PRESERVE' : 'JSON_MERGE_PATCH';
const data = { ...values };
for (const [name, value] of Object.entries(values)) {
data[name] = new Raw(`${method}(${name}, '${JSON.stringify(value)}')`);
}
return this.update(conditions, data, restOptions);
}

static jsonMergePreserve(conditions, values = {}, options = {}) {
return this.jsonMerge(conditions, values, { ...options, preserve: true });
}

/**
* Remove any record that matches `conditions`.
* - If `forceDelete` is true, `DELETE` records from database permanently.
Expand Down
10 changes: 10 additions & 0 deletions src/types/abstract_bone.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,16 @@ export class AbstractBone {
*/
jsonMerge<Key extends keyof Extract<this, Literal>>(name: Key, jsonValue: Record<string, unknown> | Array<unknown>, opts?: QueryOptions): Promise<number>;


/**
* UPDATE JSONB column with JSON_MERGE_PRESERVE function
* @example
* /// before: bone.extra equals { name: 'zhangsan', url: 'https://alibaba.com' }
* bone.jsonMergePreserve('extra', { url: 'https://taobao.com' })
* /// after: bone.extra equals { name: 'zhangsan', url: ['https://alibaba.com', 'https://taobao.com'] }
*/
jsonMergePreserve<Key extends keyof Extract<this, Literal>>(name: Key, jsonValue: Record<string, unknown> | Array<unknown>, opts?: QueryOptions): Promise<number>;

/**
* create instance
* @param opts query options
Expand Down
25 changes: 19 additions & 6 deletions test/integration/suite/json.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('=> Basic', () => {
id: { type: Bone.DataTypes.INTEGER, primaryKey: true },
name: Bone.DataTypes.STRING,
extra: Bone.DataTypes.JSONB,
deleted_at: Bone.DataTypes.DATE,
deletedAt: Bone.DataTypes.DATE,
});

before(async () => {
Expand All @@ -35,24 +35,23 @@ describe('=> Basic', () => {
await gen.update({ extra: { a: 1 } });
assert.equal(gen.extra.a, 1);
await gen.jsonMerge('extra', { b: 2, a: 3 });
await gen.reload();
assert.equal(gen.extra.a, 3);
assert.equal(gen.extra.b, 2);

const gen2 = await Gen.create({ name: 'gen2', extra: { test: 1 }});
assert.equal(gen2.extra.test, 1);
await gen2.jsonMerge('extra', { url: 'https://www.wanxiang.art/?foo=' });
await gen2.reload();
assert.equal(gen2.extra.url, 'https://www.wanxiang.art/?foo=');
});

it('bone.update(values,options) with JSON_MERGE_PATCH func should work', async () => {
it('bone.update(values, options) with JSON_MERGE_PATCH func should work', async () => {
const gen = await Gen.create({ name: 'testUpdateGen', extra: { test: 'gen' }});
assert.equal(gen.extra.test, 'gen');
assert.equal(gen.name, 'testUpdateGen');

const sql = new Raw(`JSON_MERGE_PATCH(extra, '${JSON.stringify({ url: 'https://www.taobao.com/?id=1' })}')`);
await gen.update({extra: sql});
assert.ok(!(gen.extra instanceof Raw));
await gen.reload();
assert.equal(gen.extra.url, 'https://www.taobao.com/?id=1');
});
Expand All @@ -63,13 +62,27 @@ describe('=> Basic', () => {
await gen.update({ extra: { a: 1 } });
assert.equal(gen.extra.a, 1);
await gen.jsonMergePreserve('extra', { b: 2, a: 3 });
await gen.reload();
assert.deepEqual(gen.extra.a, [1, 3]);

await gen.jsonMerge('extra', { url: 'https://wanxiang.art/?foo=' });
await gen.jsonMergePreserve('extra', { url: 'https://www.wanxiang.art/?foo=' });
await gen.reload();
assert.deepEqual(gen.extra.url, ['https://wanxiang.art/?foo=', 'https://www.wanxiang.art/?foo=']);
});

it('Bone.jsonMerge(conditions, values, options) should work', async () => {
const gen = await Gen.create({ name: '章3️⃣疯', extra: {} });
await Gen.jsonMerge({ id: gen.id }, { extra: { a: 3 } });
await gen.reload();
assert.equal(gen.extra.a, 3);
});


it('Bone.jsonMergePreserve(conditions, values, options) should work', async () => {
const gen = await Gen.create({ name: '章3️⃣疯', extra: {} });
await Gen.jsonMerge({ id: gen.id }, { extra: { a: 3 } });
await Gen.jsonMergePreserve({ id: gen.id }, { extra: { a: 4 } });
await gen.reload();
assert.deepEqual(gen.extra.a, [3, 4]);
});
});
});

0 comments on commit a8fe6c7

Please sign in to comment.