Skip to content

Commit

Permalink
feat(repository): transparently handle foreign keys in inclusion reso…
Browse files Browse the repository at this point in the history
…lvers
  • Loading branch information
InvictusMB committed May 28, 2020
1 parent bc99c60 commit 8060eb5
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,88 @@ describe('HasMany relation', () => {
expect(orders).to.deepEqual(persistedOrders);
});

it('can find an instance of the related model when foreign key is omitted in filter', async () => {
const order = await customerOrderRepo.create({
description: 'an order desc',
});
const notMyOrder = await orderRepo.create({
description: "someone else's order desc",
customerId: existingCustomerId + 1, // a different customerId,
});

const orders = await customerOrderRepo.find({
fields: {
description: true,
},
});

withProtoCheck(false, () => {
expect(orders).to.matchEach((v: Partial<Order>) => {
expect(v).to.deepEqual({
id: undefined,
description: order.description,
customerId: undefined,
});
});
expect(orders).to.not.matchEach({
description: notMyOrder.description,
});
});
});

it('can find an instance of the related model when foreign key is disabled in filter', async () => {
const order = await customerOrderRepo.create({
description: 'an order desc',
});
const notMyOrder = await orderRepo.create({
description: "someone else's order desc",
customerId: existingCustomerId + 1, // a different customerId,
});

const orders = await customerOrderRepo.find({
fields: {
description: true,
customerId: false,
},
});

withProtoCheck(false, () => {
expect(orders).to.matchEach((v: Partial<Order>) => {
expect(v).to.deepEqual({
id: undefined,
description: order.description,
customerId: undefined,
});
});
expect(orders).to.not.matchEach({
description: notMyOrder.description,
});
});
});

it('preserves foreign key value when set in filter', async () => {
const order = await customerOrderRepo.create({
description: 'an order desc',
});

const orders = await customerOrderRepo.find({
fields: {
description: true,
customerId: true,
},
});

withProtoCheck(false, () => {
expect(orders).to.matchEach((v: Partial<Order>) => {
expect(v).to.deepEqual({
id: undefined,
description: order.description,
customerId: order.customerId,
});
});
});
});

it('finds appropriate related model instances for multiple relations', async () => {
// note(shimks): roundabout way of creating reviews with 'approves'
// ideally, the review repository should have a approve function
Expand Down Expand Up @@ -284,3 +366,14 @@ function givenCrudRepositories() {
orderRepo = new DefaultCrudRepository(Order, db);
reviewRepo = new DefaultCrudRepository(Review, db);
}

function withProtoCheck(value: boolean, fn: Function) {
const shouldJs = (expect as unknown) as {config: {checkProtoEql: boolean}};
const oldValue = shouldJs.config.checkProtoEql;
shouldJs.config.checkProtoEql = value;
try {
fn();
} finally {
shouldJs.config.checkProtoEql = oldValue;
}
}
24 changes: 24 additions & 0 deletions packages/repository/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,30 @@ export class FilterBuilder<MT extends object = AnyObject> {
}
}

/**
* Checks if all fields are present in a target filter
*
* @param fields - array of fields to search in a filter
* @param filter - target filter
*
*/
export function hasFields<T extends object = AnyObject>(
fields: (keyof T)[],
filter?: Filter<T>,
) {
const normalized = new FilterBuilder(filter).build();
const targetFields = normalized.fields;
if (!targetFields) {
return true;
}
for (const f of fields) {
if (!targetFields[f]) {
return false;
}
}
return true;
}

/**
* Get nested properties by path
* @param value - Value of an object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import {AnyObject, Options} from '../../common-types';
import {Entity} from '../../model';
import {Filter, Inclusion} from '../../query';
import {Filter, Inclusion, hasFields} from '../../query';
import {EntityCrudRepository} from '../../repositories/repository';
import {
deduplicate,
Expand Down Expand Up @@ -54,6 +54,7 @@ export function createBelongsToInclusionResolver<
const targetKey = relationMeta.keyTo as StringKeyOf<Target>;
const dedupedSourceIds = deduplicate(sourceIds);

const pruneTargetKey = !hasFields([targetKey], inclusion.scope);
const targetRepo = await getTargetRepo();
const targetsFound = await findByForeignKeys(
targetRepo,
Expand All @@ -63,6 +64,11 @@ export function createBelongsToInclusionResolver<
options,
);

return flattenTargetsOfOneToOneRelation(sourceIds, targetsFound, targetKey);
return flattenTargetsOfOneToOneRelation(
sourceIds,
targetsFound,
targetKey,
pruneTargetKey,
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import debugFactory from 'debug';
import {AnyObject, Options} from '../../common-types';
import {Entity} from '../../model';
import {Filter, Inclusion} from '../../query';
import {Filter, Inclusion, hasFields} from '../../query';
import {EntityCrudRepository} from '../../repositories/repository';
import {
findByForeignKeys,
Expand Down Expand Up @@ -60,6 +60,7 @@ export function createHasManyInclusionResolver<
sourceIds.map(i => typeof i),
);

const pruneTargetKey = !hasFields([targetKey], inclusion.scope);
const targetRepo = await getTargetRepo();
const targetsFound = await findByForeignKeys(
targetRepo,
Expand All @@ -75,6 +76,7 @@ export function createHasManyInclusionResolver<
sourceIds,
targetsFound,
targetKey,
pruneTargetKey,
);

debug('fetchHasManyModels result', result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import {AnyObject, Options} from '../../common-types';
import {Entity} from '../../model';
import {Filter, Inclusion} from '../../query';
import {Filter, Inclusion, hasFields} from '../../query';
import {EntityCrudRepository} from '../../repositories/repository';
import {
findByForeignKeys,
Expand Down Expand Up @@ -48,6 +48,7 @@ export function createHasOneInclusionResolver<
const sourceIds = entities.map(e => (e as AnyObject)[sourceKey]);
const targetKey = relationMeta.keyTo as StringKeyOf<Target>;

const pruneTargetKey = !hasFields([targetKey], inclusion.scope);
const targetRepo = await getTargetRepo();
const targetsFound = await findByForeignKeys(
targetRepo,
Expand All @@ -57,6 +58,11 @@ export function createHasOneInclusionResolver<
options,
);

return flattenTargetsOfOneToOneRelation(sourceIds, targetsFound, targetKey);
return flattenTargetsOfOneToOneRelation(
sourceIds,
targetsFound,
targetKey,
pruneTargetKey,
);
};
}
18 changes: 17 additions & 1 deletion packages/repository/src/relations/relation.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export async function findByForeignKeys<

if (scope && !_.isEmpty(scope)) {
// combine where clause to scope filter
scope = new FilterBuilder(scope).impose({where}).filter;
scope = new FilterBuilder(scope).fields([fkName]).impose({where}).filter;
} else {
scope = {where} as Filter<Target>;
}
Expand Down Expand Up @@ -149,6 +149,7 @@ function isInclusionAllowed<T extends Entity, Relations extends object = {}>(
* @param sourceIds - One value or array of values of the target key
* @param targetEntities - target entities that satisfy targetKey's value (ids).
* @param targetKey - name of the target key
* @param pruneTargetKey - remove target key value from targetEntities after flattening
*
*/
export function flattenTargetsOfOneToOneRelation<
Expand All @@ -158,13 +159,20 @@ export function flattenTargetsOfOneToOneRelation<
sourceIds: unknown[],
targetEntities: Target[],
targetKey: StringKeyOf<Target>,
pruneTargetKey = false,
): (Target | undefined)[] {
const lookup = buildLookupMap<unknown, Target, Target>(
targetEntities,
targetKey,
reduceAsSingleItem,
);

if (pruneTargetKey) {
targetEntities.forEach((e: Partial<Target>) => {
e[targetKey] = undefined;
});
}

return flattenMapByKeys(sourceIds, lookup);
}

Expand All @@ -176,12 +184,14 @@ export function flattenTargetsOfOneToOneRelation<
* @param sourceIds - One value or array of values of the target key
* @param targetEntities - target entities that satisfy targetKey's value (ids).
* @param targetKey - name of the target key
* @param pruneTargetKey - remove target key value from targetEntities after flattening
*
*/
export function flattenTargetsOfOneToManyRelation<Target extends Entity>(
sourceIds: unknown[],
targetEntities: Target[],
targetKey: StringKeyOf<Target>,
pruneTargetKey = false,
): (Target[] | undefined)[] {
debug('flattenTargetsOfOneToManyRelation');
debug('sourceIds', sourceIds);
Expand All @@ -198,6 +208,12 @@ export function flattenTargetsOfOneToManyRelation<Target extends Entity>(
reduceAsArray,
);

if (pruneTargetKey) {
targetEntities.forEach((e: Partial<Target>) => {
e[targetKey] = undefined;
});
}

debug('lookup map', lookup);

return flattenMapByKeys(sourceIds, lookup);
Expand Down

0 comments on commit 8060eb5

Please sign in to comment.