From b1d131870c44ee930b1ad5628b91e0c9b65613e4 Mon Sep 17 00:00:00 2001 From: Chen Yangjian <252317+cyjake@users.noreply.github.com> Date: Thu, 9 Jan 2025 23:10:53 +0800 Subject: [PATCH] feat: bone.jsonMerge({ extra, ... }) --- .github/workflows/nodejs.yml | 3 +++ .github/workflows/npmpublish.yml | 3 +++ package.json | 6 +++++ src/bone.js | 31 +++++++++++++++++++++---- src/types/abstract_bone.d.ts | 9 ++++++++ test/integration/suite/json.test.js | 36 +++++++++++++++++++++++++---- 6 files changed, 79 insertions(+), 9 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index a6f0b0b..db3d659 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -41,6 +41,9 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Install openssl + run: sudo apt-get install openssl + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: diff --git a/.github/workflows/npmpublish.yml b/.github/workflows/npmpublish.yml index 1c30fcb..51c4506 100644 --- a/.github/workflows/npmpublish.yml +++ b/.github/workflows/npmpublish.yml @@ -35,6 +35,9 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Install openssl + run: sudo apt-get install openssl + - uses: actions/setup-node@v2 with: node-version: 22 diff --git a/package.json b/package.json index 176f023..602ca21 100644 --- a/package.json +++ b/package.json @@ -132,5 +132,11 @@ "sqlite3": "^5.0.2", "ts-node": "^10.9.1", "typescript": "^4.9.5" + }, + "overrides": { + "sqlite3": { + "glob": "11.0.0", + "node-gyp": "10.2.0" + } } } diff --git a/src/bone.js b/src/bone.js index 2423088..9e55457 100644 --- a/src/bone.js +++ b/src/bone.js @@ -687,17 +687,32 @@ class Bone { const { primaryKey, shardingKey } = Model; if (this[primaryKey] == null) throw new Error(`unset primary key ${primaryKey}`); + let values; + if (typeof name === 'string') { + // bone.jsonMerge('extra', { a: 1 }) + values = { [name]: jsonValue }; + } else { + // bone.jsonMerge({ extra: { a: 1 }, ... }) + values = name; + options = jsonValue || options; + } + const { preserve, ...restOptions } = options; const where = { [primaryKey]: this[primaryKey] }; if (shardingKey) where[shardingKey] = this[shardingKey]; - const affectedRows = await Model.jsonMerge(where, { [name]: jsonValue }, options); + const affectedRows = await Model.jsonMerge(where, values, options); // reload only the updated attribute, incase overwriting others if (affectedRows > 0) { - const spell = Model._find(where, restOptions).$select(name).$get(0); + const keys = Object.keys(values); + const spell = Model._find(where, restOptions).$select(keys).$get(0); spell.scopes = []; const instance = await spell; - if (instance) this.attribute(name, instance.attribute(name)); + if (instance) { + for (const key of keys) { + this.attribute(key, instance.attribute(key)); + } + } } return affectedRows; @@ -722,7 +737,9 @@ class Bone { if (typeof values === 'object') { for (const name in values) { const value = values[name]; - if (value !== undefined && !(value instanceof Raw) && this.hasAttribute(name) && (!fields.length || fields.includes(name))) { + if (value instanceof Raw) { + changes[name] = value; + } else if (value !== undefined && this.hasAttribute(name) && (!fields.length || fields.includes(name))) { // exec custom setters in case it exist this[name] = value; changes[name] = this.attribute(name); @@ -1581,7 +1598,11 @@ class Bone { 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}, ${SqlString.escape(JSON.stringify(value))})`); + if (value != null && typeof value === 'object') { + data[name] = new Raw(`${method}(${name}, ${SqlString.escape(JSON.stringify(value))})`); + } else { + data[name] = value; + } } return this.update(conditions, data, restOptions); } diff --git a/src/types/abstract_bone.d.ts b/src/types/abstract_bone.d.ts index 0bb6315..fefe9a6 100644 --- a/src/types/abstract_bone.d.ts +++ b/src/types/abstract_bone.d.ts @@ -388,6 +388,15 @@ export class AbstractBone { */ jsonMerge>(name: Key, jsonValue: Record | Array, opts?: QueryOptions): Promise; + /** + * 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>(values: Record | Array | Literal>, opts?: QueryOptions): Promise; + /** * UPDATE JSONB column with JSON_MERGE_PRESERVE function diff --git a/test/integration/suite/json.test.js b/test/integration/suite/json.test.js index b66e966..c04a504 100644 --- a/test/integration/suite/json.test.js +++ b/test/integration/suite/json.test.js @@ -1,5 +1,6 @@ 'use strict'; +const SqlString = require('sqlstring'); const assert = require('assert').strict; const { Bone, Raw } = require('../../../src'); @@ -29,7 +30,7 @@ describe('=> Basic', () => { await Gen.remove({}, true); }); - it('bone.jsonMerge(name, values, options) should work', async () => { + it('bone.jsonMerge(name, value, options) should work', async () => { const gen = await Gen.create({ name: '章3️⃣疯' }); assert.equal(gen.name, '章3️⃣疯'); await gen.update({ extra: { a: 1 } }); @@ -44,7 +45,7 @@ describe('=> Basic', () => { assert.equal(gen2.extra.url, 'https://www.wanxiang.art/?foo='); }); - it('bone.jsonMerge(name, values, options) should escape double quotations', async () => { + it('bone.jsonMerge(name, value, options) should escape double quotations', async () => { const gen = await Gen.create({ name: '章3️⃣疯', extra: {} }); await gen.jsonMerge('extra', { a: `fo'o"quote"bar` }); assert.equal(gen.extra.a, `fo'o"quote"bar`); @@ -55,13 +56,40 @@ describe('=> Basic', () => { 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.update({ + extra: new Raw(`JSON_MERGE_PATCH(extra, ${SqlString.escape(JSON.stringify({ url: 'https://www.taobao.com/?id=1' }))})`), + }); assert.ok(!(gen.extra instanceof Raw)); await gen.reload(); assert.equal(gen.extra.url, 'https://www.taobao.com/?id=1'); }); + it('bone.update(values, options) with Raw and literal values mixed should work', async () => { + const gen = await Gen.create({ name: 'testUpdateGen', extra: { test: 'gen' }}); + await gen.update({ + extra: new Raw(`JSON_MERGE_PATCH(extra, ${SqlString.escape(JSON.stringify({ url: 'https://www.taobao.com/?id=2' }))})`), + name: 'gen2', + }); + assert.ok(!(gen.extra instanceof Raw)); + await gen.reload(); + assert.equal(gen.extra.test, 'gen'); + assert.equal(gen.extra.url, 'https://www.taobao.com/?id=2'); + assert.equal(gen.name, 'gen2'); + }); + + it('bone.jsonMerge(values, options) with object and primitive values mixed should work', async () => { + const gen = await Gen.create({ name: 'testUpdateGen', extra: { test: 'gen' }}); + await gen.jsonMerge({ + extra: { url: 'https://www.taobao.com/?id=2' }, + name: 'gen2', + }); + assert.ok(!(gen.extra instanceof Raw)); + await gen.reload(); + assert.equal(gen.extra.test, 'gen'); + assert.equal(gen.extra.url, 'https://www.taobao.com/?id=2'); + assert.equal(gen.name, 'gen2'); + }); + it('bone.jsonMergePreserve(name, values, options) should work', async () => { const gen = await Gen.create({ name: '章3️⃣疯' }); assert.equal(gen.name, '章3️⃣疯');