Skip to content

Commit

Permalink
fix(postgres): allow postgres array operators on embedded array prope…
Browse files Browse the repository at this point in the history
…rties

Closes #4930
  • Loading branch information
B4nan committed Nov 17, 2023
1 parent abad6ca commit ecf1f0c
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 7 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/utils/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
15 changes: 12 additions & 3 deletions packages/knex/src/query/CriteriaNodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
1 change: 0 additions & 1 deletion packages/knex/src/query/ObjectCriteriaNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ export class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {
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) {
Expand Down
35 changes: 33 additions & 2 deletions tests/features/embeddables/embedded-entities.postgres.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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',
},
]);
});

});

0 comments on commit ecf1f0c

Please sign in to comment.