Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move json_merge_patch() handling to the spell prepartion phase #422

Merged
merged 1 commit into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
});
Loading