Skip to content

Commit

Permalink
feat: add two fan out methods to association loader (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
wschurman authored Jun 30, 2020
1 parent 35db7e9 commit 89cfb3d
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 2 deletions.
92 changes: 92 additions & 0 deletions packages/entity/src/EntityAssociationLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,52 @@ export default class EntityAssociationLoader<
return await loader.loadByIDAsync((associatedEntityID as unknown) as TAssociatedID);
}

/**
* Load many entities associated with this entity, often referred to as entites belonging
* to this entity. In a relational database, the field in the foreign entity is a
* foreign key to the ID of this entity. Also commonly referred to as a has many relationship,
* where this entity has many associated entities.
* @param associatedEntityClass - class of the associated entities
* @param associatedEntityFieldContainingThisID - field of associated entity which contains the ID of this entity
* @param queryContext - query context in which to perform the load
*/
async loadManyAssociatedEntitiesAsync<
TAssociatedFields,
TAssociatedID,
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity
>
>(
associatedEntityClass: IEntityClass<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedPrivacyPolicy
>,
associatedEntityFieldContainingThisID: keyof TAssociatedFields,
queryContext: EntityQueryContext = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getQueryContextProvider()
.getRegularEntityQueryContext()
): Promise<readonly Result<TAssociatedEntity>[]> {
const thisID = this.entity.getID();
const loader = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext);
return await loader.loadManyByFieldEqualingAsync(
associatedEntityFieldContainingThisID,
thisID as any
);
}

/**
* Load an associated entity identified by a field value of this entity. In a relational database,
* the field in this entity is a foreign key to a unique field of the associated entity.
Expand Down Expand Up @@ -101,6 +147,52 @@ export default class EntityAssociationLoader<
);
}

/**
* Load many associated entities identified by a field value of this entity. In a relational database,
* the field in this entity refers to a field of the associated entity.
* @param fieldIdentifyingAssociatedEntity - field of this entity containing the value with which to look up associated entities
* @param associatedEntityClass - class of the associated entities
* @param associatedEntityLookupByField - field of associated entities with which to look up the associated entities
* @param queryContext - query context in which to perform the load
*/
async loadManyAssociatedEntitiesByFieldEqualingAsync<
TAssociatedFields,
TAssociatedID,
TAssociatedEntity extends ReadonlyEntity<TAssociatedFields, TAssociatedID, TViewerContext>,
TAssociatedPrivacyPolicy extends EntityPrivacyPolicy<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity
>
>(
fieldIdentifyingAssociatedEntity: keyof TFields,
associatedEntityClass: IEntityClass<
TAssociatedFields,
TAssociatedID,
TViewerContext,
TAssociatedEntity,
TAssociatedPrivacyPolicy
>,
associatedEntityLookupByField: keyof TAssociatedFields,
queryContext: EntityQueryContext = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getQueryContextProvider()
.getRegularEntityQueryContext()
): Promise<readonly Result<TAssociatedEntity>[]> {
const associatedFieldValue = this.entity.getField(fieldIdentifyingAssociatedEntity);
const loader = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext);
return await loader.loadManyByFieldEqualingAsync(
associatedEntityLookupByField,
associatedFieldValue as any
);
}

/**
* Load an associated entity by folding a sequence of {@link EntityLoadThroughDirective}. At each
* fold step, load an associated entity identified by a field value of the current fold value.
Expand Down
51 changes: 49 additions & 2 deletions packages/entity/src/__tests__/EntityAssociationLoader-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { enforceAsyncResult } from '@expo/results';

import EntityAssociationLoader from '../EntityAssociationLoader';
import { enforceResultsAsync } from '../entityUtils';
import TestEntity from '../testfixtures/TestEntity';
import TestEntity2 from '../testfixtures/TestEntity2';
import TestViewerContext from '../testfixtures/TestViewerContext';
Expand All @@ -26,8 +27,28 @@ describe(EntityAssociationLoader, () => {
});
});

describe('loadRelatedEntityByFieldEqualingAsync', () => {
it('loads associated entities by field equaling', async () => {
describe('loadManyAssociatedEntitiesAsync', () => {
it('loads many associated entities referencing this entity', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(TestEntity.creator(viewerContext).createAsync());
const testOtherEntity1 = await enforceAsyncResult(
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const testOtherEntity2 = await enforceAsyncResult(
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const loaded = await enforceResultsAsync(
testEntity.associationLoader().loadManyAssociatedEntitiesAsync(TestEntity, 'stringField')
);
expect(loaded).toHaveLength(2);
expect(loaded.find((e) => e.getID() === testOtherEntity1.getID())).not.toBeUndefined();
expect(loaded.find((e) => e.getID() === testOtherEntity2.getID())).not.toBeUndefined();
});
});

describe('loadAssociatedEntityByFieldEqualingAsync', () => {
it('loads associated entity by field equaling', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testOtherEntity = await enforceAsyncResult(
Expand Down Expand Up @@ -57,6 +78,32 @@ describe(EntityAssociationLoader, () => {
});
});

describe('loadManyAssociatedEntitiesByFieldEqualingAsync', () => {
it('loads many associated entities by field equaling', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(TestEntity.creator(viewerContext).createAsync());
const testOtherEntity1 = await enforceAsyncResult(
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const testOtherEntity2 = await enforceAsyncResult(
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const loaded = await enforceResultsAsync(
testEntity
.associationLoader()
.loadManyAssociatedEntitiesByFieldEqualingAsync(
'customIdField',
TestEntity,
'stringField'
)
);
expect(loaded).toHaveLength(2);
expect(loaded.find((e) => e.getID() === testOtherEntity1.getID())).not.toBeUndefined();
expect(loaded.find((e) => e.getID() === testOtherEntity2.getID())).not.toBeUndefined();
});
});

describe('loadAssociatedEntityThroughAsync', () => {
it('chain loads associated entities', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
Expand Down

0 comments on commit 89cfb3d

Please sign in to comment.