From ecf1f0c96484178a4d7fc8228c2a41198d20ec30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Fri, 17 Nov 2023 13:13:00 +0100 Subject: [PATCH] fix(postgres): allow postgres array operators on embedded array properties Closes #4930 --- packages/core/src/utils/Utils.ts | 2 +- .../knex/src/query/CriteriaNodeFactory.ts | 15 ++++++-- packages/knex/src/query/ObjectCriteriaNode.ts | 1 - .../embedded-entities.postgres.test.ts | 35 +++++++++++++++++-- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index 043e5274830b..db726d0cb7a1 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -961,7 +961,7 @@ export class Utils { return ([] as T[]).concat.apply([], arrays); } - static isOperator(key: PropertyKey, includeGroupOperators = true): boolean { + static isOperator(key: PropertyKey, includeGroupOperators = true): key is QueryOperator { if (!includeGroupOperators) { return key in QueryOperator; } diff --git a/packages/knex/src/query/CriteriaNodeFactory.ts b/packages/knex/src/query/CriteriaNodeFactory.ts index 4b71e3d479f0..b7bfa60dc715 100644 --- a/packages/knex/src/query/CriteriaNodeFactory.ts +++ b/packages/knex/src/query/CriteriaNodeFactory.ts @@ -73,18 +73,27 @@ export class CriteriaNodeFactory { return this.createNode(metadata, entityName, map, node, key); } - const operator = Object.keys(payload[key]).some(f => Utils.isOperator(f)); + // array operators can be used on embedded properties + const allowedOperators = ['$contains', '$contained', '$overlap']; + const operator = Object.keys(payload[key]).some(f => Utils.isOperator(f) && !allowedOperators.includes(f)); if (operator) { throw ValidationError.cannotUseOperatorsInsideEmbeddables(entityName, prop.name, payload); } const map = Object.keys(payload[key]).reduce((oo, k) => { - if (!prop.embeddedProps[k]) { + if (!prop.embeddedProps[k] && !allowedOperators.includes(k)) { throw ValidationError.invalidEmbeddableQuery(entityName, k, prop.type); } - oo[prop.embeddedProps[k].name] = payload[key][k]; + if (prop.embeddedProps[k]) { + oo[prop.embeddedProps[k].name] = payload[key][k]; + } else if (typeof payload[key][k] === 'object') { + oo[k] = JSON.stringify(payload[key][k]); + } else { + oo[k] = payload[key][k]; + } + return oo; }, {} as Dictionary); diff --git a/packages/knex/src/query/ObjectCriteriaNode.ts b/packages/knex/src/query/ObjectCriteriaNode.ts index 465667ed1a1c..b6364af368b0 100644 --- a/packages/knex/src/query/ObjectCriteriaNode.ts +++ b/packages/knex/src/query/ObjectCriteriaNode.ts @@ -127,7 +127,6 @@ export class ObjectCriteriaNode extends CriteriaNode { o[`${alias}.${field}`] = { [k]: tmp, ...(o[`${alias}.${field}`] || {}) }; } else if (this.isPrefixed(k) || Utils.isOperator(k) || !childAlias) { const idx = prop.referencedPKs.indexOf(k as EntityKey); - // FIXME maybe other kinds should be supported too? const key = idx !== -1 && !childAlias && ![ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) ? prop.joinColumns[idx] : k; if (key in o) { diff --git a/tests/features/embeddables/embedded-entities.postgres.test.ts b/tests/features/embeddables/embedded-entities.postgres.test.ts index aa3ad92ad968..0f43258a8096 100644 --- a/tests/features/embeddables/embedded-entities.postgres.test.ts +++ b/tests/features/embeddables/embedded-entities.postgres.test.ts @@ -178,7 +178,13 @@ describe('embedded entities in postgresql', () => { user.email = 'test'; expect(user.addresses).toEqual([]); const address1 = new Address1('Downing street 13A', 10, '10A', 'London 4A', 'UK 4A'); - const address2 = { street: 'Downing street 23A', number: 20, postalCode: '20A', city: 'London 24A', country: 'UK 24A' }; + const address2 = { + street: 'Downing street 23A', + number: 20, + postalCode: '20A', + city: 'London 24A', + country: 'UK 24A', + }; orm.em.assign(user, { addresses: [address1] }); expect(user.addresses).toEqual([address1]); @@ -280,7 +286,7 @@ describe('embedded entities in postgresql', () => { expect(u3.address1.city).toBe('London 1'); expect(u3.address1.postalCode).toBe('123'); expect(u3).toBe(u1); - const err = "Using operators inside embeddables is not allowed, move the operator above. (property: User.address1, payload: { address1: { '$or': [ [Object], [Object] ] } })"; + const err = 'Using operators inside embeddables is not allowed, move the operator above. (property: User.address1, payload: { address1: { \'$or\': [ [Object], [Object] ] } })'; await expect(orm.em.findOneOrFail(User, { address1: { $or: [{ city: 'London 1' }, { city: 'Berlin' }] } })).rejects.toThrowError(err); const u4 = await orm.em.findOneOrFail(User, { address4: { postalCode: '999' } }); expect(u4).toBe(u1); @@ -484,4 +490,29 @@ describe('embedded entities in postgresql', () => { '(address4->>\'postal_code\')::text = \'12000\''); }); + test('array operators', async () => { + await createUser(); + const qb = orm.em.createQueryBuilder(User).select('*').where({ + addresses: { $contains: [{ street: 'Downing street 13A' }] }, + }); + expect(qb.getFormattedQuery()).toBe(`select "u0".* from "user" as "u0" where "u0"."addresses" @> '[{"street":"Downing street 13A"}]'`); + const res = await qb; + expect(res[0].addresses).toEqual([ + { + street: 'Downing street 13A', + number: 10, + postalCode: '10A', + city: 'London 4A', + country: 'UK 4A', + }, + { + street: 'Downing street 13B', + number: 10, + postalCode: '10B', + city: 'London 4B', + country: 'UK 4B', + }, + ]); + }); + });