diff --git a/src/bone.d.ts b/src/bone.d.ts index b6dd1e57..a48f7813 100644 --- a/src/bone.d.ts +++ b/src/bone.d.ts @@ -44,6 +44,15 @@ export default class Bone extends AbstractBone { */ static update(this: T, whereConditions: WhereConditions, values?: Values> & Partial, Literal>>, opts?: QueryOptions): Spell; + /** + * 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>(name: Key, jsonValue: Record | Array, opts?: QueryOptions): Promise; + /** * Discard all the applied scopes. * @example diff --git a/src/bone.js b/src/bone.js index 6577c9e2..36be809f 100644 --- a/src/bone.js +++ b/src/bone.js @@ -673,6 +673,28 @@ class Bone { }); } + /** + * @public + * @param {String} name + * @param {Object} jsonValue + * @param {Object?} options + * @returns {Promise} + * @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 @@ -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 @@ -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 diff --git a/src/drivers/abstract/spellbook.js b/src/drivers/abstract/spellbook.js index 5b2bc0c0..32acc308 100644 --- a/src/drivers/abstract/spellbook.js +++ b/src/drivers/abstract/spellbook.js @@ -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]); } } diff --git a/src/spell.js b/src/spell.js index 5731ad21..a75a0f4f 100644 --- a/src/spell.js +++ b/src/spell.js @@ -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); diff --git a/test/integration/mysql.test.js b/test/integration/mysql.test.js index fd696922..c7187f9a 100644 --- a/test/integration/mysql.test.js +++ b/test/integration/mysql.test.js @@ -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'); diff --git a/test/integration/suite/json.test.js b/test/integration/suite/json.test.js new file mode 100644 index 00000000..3b51f939 --- /dev/null +++ b/test/integration/suite/json.test.js @@ -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'); + }); + }); +});