diff --git a/packages/core/database/src/__tests__/fields/array.test.ts b/packages/core/database/src/__tests__/fields/array.test.ts new file mode 100644 index 000000000..c8d06ad8c --- /dev/null +++ b/packages/core/database/src/__tests__/fields/array.test.ts @@ -0,0 +1,66 @@ +import { mockDatabase } from '../'; +import { Database } from '../../database'; + +describe('array field', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should not be ambiguous', async () => { + db.collection({ + name: 'a', + fields: [ + { + type: 'array', + name: 'arr', + }, + { + type: 'belongsToMany', + name: 'b', + target: 'b', + }, + ], + }); + db.collection({ + name: 'b', + fields: [ + { + type: 'array', + name: 'arr', + }, + ], + }); + await db.sync(); + const repository = db.getRepository('a'); + await repository.find({ + filter: { + 'arr.$match': ['aa'], + }, + appends: ['b'], + }); + await repository.find({ + filter: { + 'arr.$notMatch': ['aa'], + }, + appends: ['b'], + }); + await repository.find({ + filter: { + 'arr.$anyOf': ['aa'], + }, + appends: ['b'], + }); + await repository.find({ + filter: { + 'arr.$noneOf': ['aa'], + }, + appends: ['b'], + }); + }); +}); diff --git a/packages/core/database/src/filter-parser.ts b/packages/core/database/src/filter-parser.ts index 7ae323306..6162a2602 100644 --- a/packages/core/database/src/filter-parser.ts +++ b/packages/core/database/src/filter-parser.ts @@ -124,12 +124,13 @@ export default class FilterParser { skipPrefix = origins.join('.'); const queryValue = lodash.get(unflatten(originalFiler), skipPrefix); - + const [fieldName, fullName] = this.getFieldNameFromQueryPath(skipPrefix); value = opKey(queryValue, { app: this.context.app, db: this.database, path: skipPrefix, - fieldName: this.getFieldNameFromQueryPath(skipPrefix), + fullName, + fieldName, model: this.model, }); break; @@ -230,14 +231,14 @@ export default class FilterParser { private getFieldNameFromQueryPath(queryPath: string) { const paths = queryPath.split('.'); let fieldName; + let fullPaths = []; for (const path of paths) { if (path.startsWith('$') || !lodash.isNaN(parseInt(path))) { continue; } - + fullPaths.push(path); fieldName = path; } - - return fieldName; + return [fieldName, fullPaths.join('.')]; } } diff --git a/packages/core/database/src/operators/array.ts b/packages/core/database/src/operators/array.ts index 33e7ec94a..d2322b3bd 100644 --- a/packages/core/database/src/operators/array.ts +++ b/packages/core/database/src/operators/array.ts @@ -1,5 +1,5 @@ import { Op, Sequelize } from 'sequelize'; -import { isPg, isMySQL } from './utils'; +import { isMySQL, isPg } from './utils'; const getFieldName = (ctx) => { const fieldName = ctx.fieldName; @@ -13,10 +13,11 @@ const escape = (value, ctx) => { const sqliteExistQuery = (value, ctx) => { const fieldName = getFieldName(ctx); + const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`; const sqlArray = `(${value.map((v) => `'${v}'`).join(', ')})`; - const subQuery = `exists (select * from json_each(${fieldName}) where json_each.value in ${sqlArray})`; + const subQuery = `exists (select * from json_each(${name}) where json_each.value in ${sqlArray})`; return subQuery; }; @@ -53,7 +54,8 @@ export default { value = escape(JSON.stringify(value.sort()), ctx); if (isMySQL(ctx)) { - return Sequelize.literal(`JSON_CONTAINS(${fieldName}, ${value}) AND JSON_CONTAINS(${value}, ${fieldName})`); + const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``; + return Sequelize.literal(`JSON_CONTAINS(${name}, ${value}) AND JSON_CONTAINS(${value}, ${name})`); } return { @@ -66,11 +68,13 @@ export default { value = escape(JSON.stringify(value), ctx); if (isPg(ctx)) { - return Sequelize.literal(`not (${fieldName} <@ ${value}::JSONB and ${fieldName} @> ${value}::JSONB)`); + const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`; + return Sequelize.literal(`not (${name} <@ ${value}::JSONB and ${name} @> ${value}::JSONB)`); } if (isMySQL(ctx)) { - return Sequelize.literal(`not (JSON_CONTAINS(${fieldName}, ${value}) AND JSON_CONTAINS(${value}, ${fieldName}))`); + const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``; + return Sequelize.literal(`not (JSON_CONTAINS(${name}, ${value}) AND JSON_CONTAINS(${value}, ${name}))`); } return { [Op.ne]: Sequelize.literal(`json(${value})`), @@ -81,8 +85,9 @@ export default { const fieldName = getFieldName(ctx); if (isPg(ctx)) { + const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`; return Sequelize.literal( - `${fieldName} ?| ${escape( + `${name} ?| ${escape( value.map((i) => `${i}`), ctx, )}`, @@ -91,8 +96,8 @@ export default { if (isMySQL(ctx)) { value = escape(JSON.stringify(value), ctx); - - return Sequelize.literal(`JSON_OVERLAPS(${fieldName}, ${value})`); + const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``; + return Sequelize.literal(`JSON_OVERLAPS(${name}, ${value})`); } const subQuery = sqliteExistQuery(value, ctx); @@ -105,9 +110,10 @@ export default { if (isPg(ctx)) { const fieldName = getFieldName(ctx); + const name = ctx.fullName === fieldName ? `"${ctx.model.name}"."${fieldName}"` : `"${fieldName}"`; // pg single quote where = Sequelize.literal( - `not (${fieldName} ?| ${escape( + `not (${name} ?| ${escape( value.map((i) => `${i}`), ctx, )})`, @@ -115,7 +121,8 @@ export default { } else if (isMySQL(ctx)) { const fieldName = getFieldName(ctx); value = escape(JSON.stringify(value), ctx); - where = Sequelize.literal(`NOT JSON_OVERLAPS(${fieldName}, ${value})`); + const name = ctx.fullName === fieldName ? `\`${ctx.model.name}\`.\`${fieldName}\`` : `\`${fieldName}\``; + where = Sequelize.literal(`NOT JSON_OVERLAPS(${name}, ${value})`); } else { const subQuery = sqliteExistQuery(value, ctx);