Skip to content

Commit

Permalink
refactor: move json_merge_patch() handling to the spell prepartion ph…
Browse files Browse the repository at this point in the history
…ase (#422)
  • Loading branch information
coinkits authored Aug 23, 2024
1 parent 5009b4a commit 002eca9
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 5 deletions.
19 changes: 18 additions & 1 deletion src/bone.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,21 @@ class Bone {
});
}

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

}

/**
* Persist changes on current instance back to database with `UPDATE`.
* @public
Expand All @@ -696,6 +711,9 @@ class Bone {
}
try {
const res = await this._update(Object.keys(changes).length? changes : values, options);
if (typeof values === 'object' && Object.values(values).some(v => v instanceof Raw)) {
await this.reload();
}
return res;
} catch (error) {
// revert value in case update failed
Expand Down Expand Up @@ -1158,7 +1176,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
9 changes: 9 additions & 0 deletions src/types/abstract_bone.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,15 @@ export class AbstractBone {
*/
update(changes?: { [key: string]: Literal } | { [Property in keyof Extract<this, Literal>]?: Literal }, opts?: QueryOptions): Promise<number>;

/**
* UPDATE JSONB column with JSON_MERGE_PATCH 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 Extract<this, Literal>>(name: Key, jsonValue: Record<string, unknown> | Array<unknown>, opts?: QueryOptions): Promise<number>;

/**
* create instance
* @param opts query options
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
1 change: 1 addition & 0 deletions test/integration/mysql2.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ before(async function() {

require('./suite/index.test');
require('./suite/dates.test');
require('./suite/json.test');
14 changes: 13 additions & 1 deletion test/integration/sqlite.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const assert = require('assert').strict;
const path = require('path');
const sinon = require('sinon');

const { connect, raw, Bone } = require('../../src');
const { connect, raw, Bone, Raw } = require('../../src');
const { checkDefinitions } = require('./helpers');

before(async function() {
Expand Down Expand Up @@ -104,3 +104,15 @@ describe('=> upsert (sqlite)', function () {
);
});
});

describe('=> update(sqlite) & jsonMerge(sqlite)', () => {
const Post = require('../models/post');
it('JSON_MERGE_PATCH can not work in sqlite', async () => {
const post = await Post.create({ title: 'new post', extra: { uid: 2200 }});
assert.equal(post.extra.uid, 2200);
await assert.rejects(async () => await post.jsonMerge('extra', { uid: 9527 }));
assert.equal(post.extra.uid, 2200);
await assert.rejects(async () => await post.update({ extra: new Raw(`JSON_MERGE_PATCH(extra, '${JSON.stringify({ uid: 4396 })}')`)}));
assert.equal(post.extra.uid, 2200);
});
});
57 changes: 57 additions & 0 deletions test/integration/suite/json.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'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});
assert.equal(gen.extra.url, 'https://www.taobao.com/?id=1');
});
});
});

0 comments on commit 002eca9

Please sign in to comment.