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

fix: jsonMerge with complex json literals #431

Merged
merged 2 commits into from
Jan 10, 2025
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
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"
}
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}
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
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ubuntu 22 升级到了 openssl 3,sqlcipher 编译用的还是 1.1,降级起来有点麻烦,先从默认的 CI 任务里面摘出来,必要的时候手动跑

}

##
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
Loading