Skip to content

Commit

Permalink
fix: jsonMerge with complex json literals (#431)
Browse files Browse the repository at this point in the history
* fix: jsonMerge with complex json literals

* feat: bone.jsonMerge({ extra, ... })
  • Loading branch information
cyjake authored Jan 10, 2025
1 parent 4840937 commit 586442d
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 11 deletions.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
32 changes: 27 additions & 5 deletions src/bone.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const deepEqual = require('deep-equal');
const debug = require('debug')('leoric');
const pluralize = require('pluralize');
const { executeValidator, LeoricValidateError } = require('./validator');
const SqlString = require('sqlstring');
require('reflect-metadata');

const { default: DataTypes } = require('./data_types');
Expand Down Expand Up @@ -686,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;
Expand All @@ -721,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);
Expand Down Expand Up @@ -1580,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}, '${JSON.stringify(value).replace(/'/g, "\\'")}')`);
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);
}
Expand Down
10 changes: 8 additions & 2 deletions src/expr.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,16 @@ function parseExprList(str, ...values) {

function string() {
let value = '';
let escaped = false;
const quote = chr;
next();
while (chr && chr !== quote) {
value += chr;
while (chr && (chr !== quote || escaped)) {
if (chr === '\\' && !escaped) {
escaped = true;
} else {
value += chr;
escaped = false;
}
next();
}
if (chr !== quote) {
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 @@ -388,6 +388,15 @@ export class AbstractBone {
*/
jsonMerge<Key extends keyof Extract<this, Literal>>(name: Key, jsonValue: Record<string, unknown> | Array<unknown>, 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>>(values: Record<Key, Record<string, unknown> | Array<unknown> | Literal>, opts?: QueryOptions): Promise<number>;


/**
* UPDATE JSONB column with JSON_MERGE_PRESERVE function
Expand Down
47 changes: 44 additions & 3 deletions test/integration/suite/json.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

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

const { Bone, Raw } = require('../../../src');
Expand Down Expand Up @@ -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 } });
Expand All @@ -44,18 +45,51 @@ describe('=> Basic', () => {
assert.equal(gen2.extra.url, 'https://www.wanxiang.art/?foo=');
});

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`);
});

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.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️⃣疯');
Expand Down Expand Up @@ -83,6 +117,13 @@ describe('=> Basic', () => {
assert.equal(gen.extra.a, 'foo\'bar');
});

it('Bone.jsonMerge(conditions, values, options) should escape double quotations', async () => {
const gen = await Gen.create({ name: '章3️⃣疯', extra: {} });
await Gen.jsonMerge({ id: gen.id }, { extra: { a: 'foo"quote"bar' } });
await gen.reload();
assert.equal(gen.extra.a, 'foo"quote"bar');
});

it('Bone.jsonMergePreserve(conditions, values, options) should work', async () => {
const gen = await Gen.create({ name: '章3️⃣疯', extra: {} });
await Gen.jsonMerge({ id: gen.id }, { extra: { a: 3 } });
Expand Down
6 changes: 5 additions & 1 deletion test/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ function unit {
##
# integration tests
function integration {
for file in $(ls test/integration/*.test.js); do run ${file}; done
for file in $(ls test/integration/*.test.js); do
if [ "$file" != test/integration/sqlcipher.test.js ]; then
run ${file};
fi
done
}

##
Expand Down
1 change: 1 addition & 0 deletions test/unit/expr.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('=> parse literals', function() {
assert.deepEqual(parseExpr("'b'"), { type: 'literal', value: 'b' });
// incomplete literal
assert.throws(() => parseExpr("'a"), 'Unexpected end of string');
assert.deepEqual(parseExpr('"a\'b\\"c"'), { type: 'literal', value: `a'b"c` });
});

it('parse number[]', function() {
Expand Down

0 comments on commit 586442d

Please sign in to comment.