mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
fix(database): column name in array field (#4110)
* fix: column name in array field * chore: test * fix: test * fix: test * fix: test
This commit is contained in:
parent
e96e9aea6e
commit
f8067c6550
@ -170,6 +170,23 @@ describe('find with associations', () => {
|
||||
});
|
||||
|
||||
it('should filter by association array field', async () => {
|
||||
const Group = db.collection({
|
||||
name: 'groups',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'users',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
name: 'tagFields',
|
||||
},
|
||||
],
|
||||
});
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
@ -181,6 +198,120 @@ describe('find with associations', () => {
|
||||
type: 'hasMany',
|
||||
name: 'posts',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'group',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
name: 'tagFields',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'array',
|
||||
name: 'tagFields',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await Group.repository.create({
|
||||
values: [
|
||||
{
|
||||
name: 'g1',
|
||||
users: [
|
||||
{
|
||||
name: 'u1',
|
||||
tagFields: ['u1'],
|
||||
posts: [
|
||||
{
|
||||
tagFields: ['p1'],
|
||||
title: 'u1p1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tagFields: ['g1'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// zero nested
|
||||
const posts = await Post.repository.find({
|
||||
filter: {
|
||||
tagFields: {
|
||||
$match: ['p1'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(posts.length).toEqual(1);
|
||||
|
||||
const filter0 = {
|
||||
$and: [
|
||||
{
|
||||
posts: {
|
||||
tagFields: {
|
||||
$match: ['p1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const userFindResult = await User.repository.find({
|
||||
filter: filter0,
|
||||
});
|
||||
|
||||
expect(userFindResult.length).toEqual(1);
|
||||
|
||||
const filter = {
|
||||
$and: [
|
||||
{
|
||||
users: {
|
||||
posts: {
|
||||
tagFields: {
|
||||
$match: ['p1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const results = await Group.repository.find({
|
||||
filter,
|
||||
});
|
||||
|
||||
expect(results[0].get('name')).toEqual('g1');
|
||||
});
|
||||
|
||||
it('should filter by array not empty', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'posts',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
name: 'tags',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@ -204,6 +335,7 @@ describe('find with associations', () => {
|
||||
values: [
|
||||
{
|
||||
name: 'u1',
|
||||
tags: ['u1-tag', 'u2-tag'],
|
||||
posts: [
|
||||
{
|
||||
tags: ['t1'],
|
||||
@ -214,33 +346,17 @@ describe('find with associations', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const posts = await Post.repository.find({
|
||||
const posts = await User.repository.find({
|
||||
filter: {
|
||||
tags: {
|
||||
$match: ['t1'],
|
||||
'posts.tags': {
|
||||
$noneOf: ['t2'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(posts.length).toEqual(1);
|
||||
|
||||
const filter = {
|
||||
$and: [
|
||||
{
|
||||
posts: {
|
||||
tags: {
|
||||
$match: ['t1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const results = await User.repository.find({
|
||||
filter,
|
||||
});
|
||||
|
||||
expect(results[0].get('name')).toEqual('u1');
|
||||
expect(posts[0].get('name')).toEqual('u1');
|
||||
});
|
||||
|
||||
it('should filter with append', async () => {
|
||||
|
@ -3,7 +3,37 @@ import { Op, Sequelize } from 'sequelize';
|
||||
import { isMySQL, isPg } from './utils';
|
||||
|
||||
const getFieldName = (ctx) => {
|
||||
return ctx.model.rawAttributes[ctx.fieldName]?.field || ctx.fieldName;
|
||||
const fullNameSplit = ctx.fullName.split('.');
|
||||
const fieldName = ctx.fieldName;
|
||||
let columnName = fieldName;
|
||||
const associationPath = [];
|
||||
if (fullNameSplit.length > 1) {
|
||||
for (let i = 0; i < fullNameSplit.length - 1; i++) {
|
||||
associationPath.push(fullNameSplit[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const getModelFromAssociationPath = () => {
|
||||
let model = ctx.model;
|
||||
for (const association of associationPath) {
|
||||
model = model.associations[association].target;
|
||||
}
|
||||
|
||||
return model;
|
||||
};
|
||||
|
||||
const model = getModelFromAssociationPath();
|
||||
|
||||
if (model.rawAttributes[fieldName]) {
|
||||
columnName = model.rawAttributes[fieldName].field || fieldName;
|
||||
}
|
||||
|
||||
if (associationPath.length > 0) {
|
||||
const association = associationPath.join('->');
|
||||
columnName = `${association}.${columnName}`;
|
||||
}
|
||||
|
||||
return columnName;
|
||||
};
|
||||
|
||||
const escape = (value, ctx) => {
|
||||
@ -18,7 +48,9 @@ const getQueryInterface = (ctx) => {
|
||||
|
||||
const sqliteExistQuery = (value, ctx) => {
|
||||
const fieldName = getFieldName(ctx);
|
||||
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
|
||||
const queryInterface = getQueryInterface(ctx);
|
||||
|
||||
const name = queryInterface.quoteIdentifiers(fieldName);
|
||||
|
||||
const sqlArray = `(${value.map((v) => `'${v}'`).join(', ')})`;
|
||||
|
||||
@ -44,7 +76,7 @@ const emptyQuery = (ctx, operator: '=' | '>') => {
|
||||
|
||||
const queryInterface = getQueryInterface(ctx);
|
||||
|
||||
return `(select ${ifNull}(${funcName}(${queryInterface.quoteIdentifier(fieldName)}), 0) ${operator} 0)`;
|
||||
return `(select ${ifNull}(${funcName}(${queryInterface.quoteIdentifiers(fieldName)}), 0) ${operator} 0)`;
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -53,7 +85,7 @@ export default {
|
||||
const fieldName = getFieldName(ctx);
|
||||
|
||||
if (isPg(ctx)) {
|
||||
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
|
||||
const name = queryInterface.quoteIdentifiers(fieldName);
|
||||
const queryValue = escape(JSON.stringify(value), ctx);
|
||||
return Sequelize.literal(`${name} @> ${queryValue}::JSONB AND ${name} <@ ${queryValue}::JSONB`);
|
||||
}
|
||||
@ -61,7 +93,7 @@ export default {
|
||||
value = escape(JSON.stringify(value.sort()), ctx);
|
||||
|
||||
if (isMySQL(ctx)) {
|
||||
const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
|
||||
const name = queryInterface.quoteIdentifiers(fieldName);
|
||||
return Sequelize.literal(`JSON_CONTAINS(${name}, ${value}) AND JSON_CONTAINS(${value}, ${name})`);
|
||||
}
|
||||
|
||||
@ -71,16 +103,16 @@ export default {
|
||||
},
|
||||
|
||||
$notMatch(value, ctx) {
|
||||
const fieldName = getFieldName(ctx);
|
||||
const queryInterface = getQueryInterface(ctx);
|
||||
value = escape(JSON.stringify(value), ctx);
|
||||
|
||||
if (isPg(ctx)) {
|
||||
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
|
||||
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
|
||||
return Sequelize.literal(`not (${name} <@ ${value}::JSONB and ${name} @> ${value}::JSONB)`);
|
||||
}
|
||||
|
||||
if (isMySQL(ctx)) {
|
||||
const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
|
||||
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
|
||||
return Sequelize.literal(`not (JSON_CONTAINS(${name}, ${value}) AND JSON_CONTAINS(${value}, ${name}))`);
|
||||
}
|
||||
return {
|
||||
@ -91,9 +123,10 @@ export default {
|
||||
$anyOf(value, ctx) {
|
||||
const fieldName = getFieldName(ctx);
|
||||
value = _.castArray(value);
|
||||
const queryInterface = getQueryInterface(ctx);
|
||||
|
||||
if (isPg(ctx)) {
|
||||
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
|
||||
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
|
||||
return Sequelize.literal(
|
||||
`${name} ?| ${escape(
|
||||
value.map((i) => `${i}`),
|
||||
@ -104,7 +137,7 @@ export default {
|
||||
|
||||
if (isMySQL(ctx)) {
|
||||
value = escape(JSON.stringify(value), ctx);
|
||||
const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
|
||||
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
|
||||
return Sequelize.literal(`JSON_OVERLAPS(${name}, ${value})`);
|
||||
}
|
||||
|
||||
@ -117,9 +150,10 @@ export default {
|
||||
let where;
|
||||
value = _.castArray(value);
|
||||
|
||||
const queryInterface = getQueryInterface(ctx);
|
||||
|
||||
if (isPg(ctx)) {
|
||||
const fieldName = getFieldName(ctx);
|
||||
const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`;
|
||||
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
|
||||
// pg single quote
|
||||
where = Sequelize.literal(
|
||||
`not (${name} ?| ${escape(
|
||||
@ -130,7 +164,7 @@ export default {
|
||||
} else if (isMySQL(ctx)) {
|
||||
const fieldName = getFieldName(ctx);
|
||||
value = escape(JSON.stringify(value), ctx);
|
||||
const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``;
|
||||
const name = queryInterface.quoteIdentifiers(getFieldName(ctx));
|
||||
where = Sequelize.literal(`NOT JSON_OVERLAPS(${name}, ${value})`);
|
||||
} else {
|
||||
const subQuery = sqliteExistQuery(value, ctx);
|
||||
|
@ -10,4 +10,9 @@ const isMySQL = (ctx) => {
|
||||
return getDialect(ctx) === 'mysql' || getDialect(ctx) === 'mariadb';
|
||||
};
|
||||
|
||||
const getQueryInterface = (ctx) => {
|
||||
const sequelize = ctx.db.sequelize;
|
||||
return sequelize.getQueryInterface();
|
||||
};
|
||||
|
||||
export { getDialect, isPg, isMySQL };
|
||||
|
Loading…
x
Reference in New Issue
Block a user