Skip to content

Commit

Permalink
refactor: move json_merge_patch() handling to the spell prepartion phase
Browse files Browse the repository at this point in the history
  • Loading branch information
coinkits authored and cyang committed Aug 21, 2024
1 parent 5009b4a commit d47f7ac
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 4 deletions.
9 changes: 9 additions & 0 deletions src/bone.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ export default class Bone extends AbstractBone {
*/
static update<T extends typeof Bone>(this: T, whereConditions: WhereConditions<T>, values?: Values<InstanceType<T>> & Partial<Record<BoneColumns<T>, Literal>>, opts?: QueryOptions): Spell<T, number>;

/**
* UPDATE JSONB row width JSON_MERGE_PARCH Function
* @example
* /// before: bone.extra equals { name: 'zhangsan', url: 'https://alibaba.com' }
* bone.jsonMerge('extra',{ url: 'https://taobao.com' })
* /// after: bone.extra equals { name: 'zhangsan', url: 'https://taobao.com' }
*/
jsonMerge<Key extends keyof Partial<typeof this>>(name: Key, jsonValue: Record<string, unknown> | Array<unknown>, opts?: QueryOptions): Promise<number>;

/**
* Discard all the applied scopes.
* @example
Expand Down
24 changes: 23 additions & 1 deletion src/bone.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,28 @@ class Bone {
});
}

/**
* @public
* @param {String} name
* @param {Object} jsonValue
* @param {Object?} options
* @returns {Promise<number>}
* @memberof Bone
*/
async jsonMerge(name, jsonValue, options = {}) {
const attribute = this.attribute(name);
try {
const raw = new Raw(`JSON_MERGE_PATCH(${name}, '${JSON.stringify(jsonValue)}')`);
const rows = await this.update({ [name]: raw }, options);
await this.reload();
return rows;
} catch (error) {
this[name] = attribute;
throw error;
}

}

/**
* Persist changes on current instance back to database with `UPDATE`.
* @public
Expand All @@ -696,6 +718,7 @@ class Bone {
}
try {
const res = await this._update(Object.keys(changes).length? changes : values, options);
require('fs').appendFileSync('attribute.txt',JSON.stringify(this.#raw)+'\n');
return res;
} catch (error) {
// revert value in case update failed
Expand Down Expand Up @@ -1158,7 +1181,6 @@ class Bone {
}
return name;
}

/**
* Load attribute definition to merge default getter/setter and custom descriptor on prototype
* @param {string} name attribute name
Expand Down
7 changes: 4 additions & 3 deletions src/drivers/abstract/spellbook.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,13 +363,14 @@ class SpellBook {
const { escapeId } = Model.driver;
for (const name in sets) {
const value = sets[name];
const columnName = escapeId(Model.unalias(name));
if (value && value.__expr) {
assigns.push(`${escapeId(Model.unalias(name))} = ${formatExpr(spell, value)}`);
assigns.push(`${columnName} = ${formatExpr(spell, value)}`);
collectLiteral(spell, value, values);
} else if (value instanceof Raw) {
assigns.push(`${escapeId(Model.unalias(name))} = ${value.value}`);
assigns.push(`${columnName} = ${value.value}`);
} else {
assigns.push(`${escapeId(Model.unalias(name))} = ?`);
assigns.push(`${columnName} = ?`);
values.push(sets[name]);
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/spell.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ function formatValueSet(spell, obj, strict = true) {

// raw sql don't need to uncast
if (value instanceof Raw) {
try {
const expr = parseExpr(value.value);
if (expr.type === 'func' && ['json_merge_patch', 'json_merge_preserve'].includes(expr.name)) {
sets[name] = { ...expr, __expr: true };
continue;
}
} catch {
// ignored
}
sets[name] = value;
} else {
sets[name] = attribute.uncast(value);
Expand Down
1 change: 1 addition & 0 deletions test/integration/mysql.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ before(async function() {

require('./suite/index.test');
require('./suite/dates.test');
require('./suite/json.test');

describe('=> Date functions (mysql)', function() {
const Post = require('../models/post');
Expand Down
59 changes: 59 additions & 0 deletions test/integration/suite/json.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
'use strict';

const assert = require('assert').strict;

const { Bone, Raw } = require('../../../src');

describe('=> Basic', () => {

describe('=> JSON Functions', ()=>{

class Gen extends Bone { }
Gen.init({
id: { type: Bone.DataTypes.INTEGER, primaryKey: true },
name: Bone.DataTypes.STRING,
extra: Bone.DataTypes.JSONB,
deleted_at: Bone.DataTypes.DATE,
});

before(async () => {
await Bone.driver.dropTable('gens');
await Gen.sync();
});

after(async () => {
await Bone.driver.dropTable('gens');
});

beforeEach(async () => {
await Gen.remove({}, true);
});

it('bone.jsonMerge(name, values, options) should work', async () => {
const gen = await Gen.create({ name: '章3️⃣疯' });
assert.equal(gen.name, '章3️⃣疯');
await gen.update({ extra: { a: 1 } });
assert.equal(gen.extra.a, 1);
await gen.jsonMerge('extra', { b: 2, a: 3 });
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=' });
assert.equal(gen2.extra.url, 'https://www.wanxiang.art/?foo=');
});

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});
await gen.reload();

assert.equal(gen.extra.url, 'https://www.taobao.com/?id=1');
});
});
});

0 comments on commit d47f7ac

Please sign in to comment.