Compare commits

...
Author SHA1 Message Date
Will Schurman 1b749a0633 feat: invert result for loader 2022-04-06 14:50:07 -06:00
27 changed files with 721 additions and 677 deletions

No files matched your search

@@ -32,9 +32,9 @@ describe(LocalMemoryCacheAdapter, () => {
.enforceCreateAsync();
// loading an entity should put it in cache
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1Created.getID());
const entity1 = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
entity1Created.getID()
);
const entitySpecificGenericCacher =
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
@@ -97,15 +97,13 @@ describe(LocalMemoryCacheAdapter, () => {
.enforceCreateAsync();
// loading an entity should put it in cache
await LocalMemoryTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1Created.getID());
await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(entity1Created.getID());
// load entity with a different request
const viewerContext2 = new TestViewerContext(createLocalMemoryTestEntityCompanionProvider());
const entity1WithVc2 = await LocalMemoryTestEntity.loader(viewerContext2)
.enforcing()
.loadByIDAsync(entity1Created.getID());
const entity1WithVc2 = await LocalMemoryTestEntity.loader(viewerContext2).loadByIDAsync(
entity1Created.getID()
);
const cacheAdapter = viewerContext.entityCompanionProvider.getCompanionForEntity(
LocalMemoryTestEntity,
@@ -140,9 +138,9 @@ describe(LocalMemoryCacheAdapter, () => {
.enforceCreateAsync();
// loading an entity will try to put it in cache but it's a noop cache, so it should be a miss
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1Created.getID());
const entity1 = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
entity1Created.getID()
);
const entitySpecificGenericCacher =
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap.get(
@@ -52,9 +52,9 @@ describe(RedisCacheAdapter, () => {
.enforceCreateAsync();
// loading an entity should put it in cache
const entity1 = await RedisTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1Created.getID());
const entity1 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(
entity1Created.getID()
);
const cachedJSON = await redisCacheAdapterContext.redisClient.get(
cacheKeyMaker('id', entity1.getID())
@@ -102,16 +102,14 @@ describe(RedisCacheAdapter, () => {
);
expect(entity1.getField('dateField')).toEqual(date);
const entity2 = await RedisTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1.getID());
const entity2 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(entity1.getID());
expect(entity2.getField('dateField')).toEqual(date);
// simulate new request
const vc2 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
);
const entity3 = await RedisTestEntity.loader(vc2).enforcing().loadByIDAsync(entity1.getID());
const entity3 = await RedisTestEntity.loader(vc2).loadByIDAsync(entity1.getID());
expect(entity3.getField('dateField')).toEqual(date);
});
@@ -122,18 +120,17 @@ describe(RedisCacheAdapter, () => {
const entity1 = await enforceAsyncResult(
RedisTestEntity.creator(viewerContext).setField('name', '').createAsync()
);
const entity2 = await RedisTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('name', '');
const entity2 = await RedisTestEntity.loader(viewerContext).loadByFieldEqualingAsync(
'name',
''
);
expect(entity2?.getID()).toEqual(entity1.getID());
// simulate new request
const vc2 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(redisCacheAdapterContext)
);
const entity3 = await RedisTestEntity.loader(vc2)
.enforcing()
.loadByFieldEqualingAsync('name', '');
const entity3 = await RedisTestEntity.loader(vc2).loadByFieldEqualingAsync('name', '');
expect(entity3?.getID()).toEqual(entity1.getID());
});
});
@@ -50,9 +50,7 @@ describe('postgres entity integration', () => {
PostgresTestEntity.updater(entity).setField('hasADog', false).updateAsync(),
]);
const loadedEntity = await PostgresTestEntity.loader(vc)
.enforcing()
.loadByIDAsync(entity.getID());
const loadedEntity = await PostgresTestEntity.loader(vc).loadByIDAsync(entity.getID());
expect(loadedEntity.getField('hasACat')).toBe(true);
expect(loadedEntity.getField('hasADog')).toBe(false);
@@ -185,26 +183,22 @@ describe('postgres entity integration', () => {
.createAsync()
);
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([
{
fieldName: 'hasACat',
fieldValue: false,
},
{
fieldName: 'hasADog',
fieldValue: true,
},
]);
const results = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync([
{
fieldName: 'hasACat',
fieldValue: false,
},
{
fieldName: 'hasADog',
fieldValue: true,
},
]);
expect(results).toHaveLength(2);
const results2 = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'hasADog', fieldValues: [true, false] },
]);
const results2 = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync(
[{ fieldName: 'hasADog', fieldValues: [true, false] }]
);
expect(results2).toHaveLength(3);
});
@@ -217,9 +211,9 @@ describe('postgres entity integration', () => {
await enforceAsyncResult(PostgresTestEntity.creator(vc1).setField('name', 'c').createAsync());
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([], {
const results = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync(
[],
{
limit: 2,
offset: 1,
orderBy: [
@@ -228,7 +222,8 @@ describe('postgres entity integration', () => {
order: OrderByOrdering.DESCENDING,
},
],
});
}
);
expect(results).toHaveLength(2);
expect(results.map((e) => e.getField('name'))).toEqual(['b', 'a']);
});
@@ -260,28 +255,26 @@ describe('postgres entity integration', () => {
.createAsync()
);
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: null }]);
const results = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'name', fieldValue: null },
]);
expect(results).toHaveLength(2);
expect(results[0]!.getField('name')).toBeNull();
const results2 = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync(
[
{ fieldName: 'name', fieldValues: ['a', null] },
{ fieldName: 'hasADog', fieldValue: true },
const results2 = await PostgresTestEntity.loader(vc1).loadManyByFieldEqualityConjunctionAsync(
[
{ fieldName: 'name', fieldValues: ['a', null] },
{ fieldName: 'hasADog', fieldValue: true },
],
{
orderBy: [
{
fieldName: 'name',
order: OrderByOrdering.DESCENDING,
},
],
{
orderBy: [
{
fieldName: 'name',
order: OrderByOrdering.DESCENDING,
},
],
}
);
}
);
expect(results2).toHaveLength(2);
expect(results2.map((e) => e.getField('name'))).toEqual([null, 'a']);
});
@@ -298,9 +291,10 @@ describe('postgres entity integration', () => {
.createAsync()
);
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByRawWhereClauseAsync('name = ?', ['hello']);
const results = await PostgresTestEntity.loader(vc1).loadManyByRawWhereClauseAsync(
'name = ?',
['hello']
);
expect(results).toHaveLength(1);
});
@@ -316,9 +310,9 @@ describe('postgres entity integration', () => {
);
await expect(
PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByRawWhereClauseAsync('invalid_column = ?', ['hello'])
PostgresTestEntity.loader(vc1).loadManyByRawWhereClauseAsync('invalid_column = ?', [
'hello',
])
).rejects.toThrow();
});
@@ -346,9 +340,10 @@ describe('postgres entity integration', () => {
.createAsync()
);
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
const results = await PostgresTestEntity.loader(vc1).loadManyByRawWhereClauseAsync(
'has_a_dog = ?',
[true],
{
limit: 2,
offset: 1,
orderBy: [
@@ -357,25 +352,26 @@ describe('postgres entity integration', () => {
order: OrderByOrdering.ASCENDING,
},
],
});
}
);
expect(results).toHaveLength(2);
expect(results.map((e) => e.getField('name'))).toEqual(['b', 'c']);
const resultsMultipleOrderBy = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
orderBy: [
{
fieldName: 'hasADog',
order: OrderByOrdering.ASCENDING,
},
{
fieldName: 'name',
order: OrderByOrdering.DESCENDING,
},
],
});
const resultsMultipleOrderBy = await PostgresTestEntity.loader(
vc1
).loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
orderBy: [
{
fieldName: 'hasADog',
order: OrderByOrdering.ASCENDING,
},
{
fieldName: 'name',
order: OrderByOrdering.DESCENDING,
},
],
});
expect(resultsMultipleOrderBy).toHaveLength(3);
expect(resultsMultipleOrderBy.map((e) => e.getField('name'))).toEqual(['c', 'b', 'a']);
@@ -395,9 +391,7 @@ describe('postgres entity integration', () => {
.enforceCreateAsync()
).rejects.toThrowError('name cannot have value beforeCreate');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeCreate')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeCreate')
).resolves.toBeNull();
await expect(
@@ -406,27 +400,21 @@ describe('postgres entity integration', () => {
.enforceCreateAsync()
).rejects.toThrowError('name cannot have value afterCreate');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterCreate')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterCreate')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.creator(vc1).setField('name', 'beforeAll').enforceCreateAsync()
).rejects.toThrowError('name cannot have value beforeAll');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeAll')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeAll')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.creator(vc1).setField('name', 'afterAll').enforceCreateAsync()
).rejects.toThrowError('name cannot have value afterAll');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterAll')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterAll')
).resolves.toBeNull();
await expect(
@@ -435,9 +423,7 @@ describe('postgres entity integration', () => {
.enforceCreateAsync()
).rejects.toThrowError('name cannot have value afterCommit');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterCommit')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterCommit')
).resolves.not.toBeNull();
});
});
@@ -458,9 +444,7 @@ describe('postgres entity integration', () => {
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value beforeUpdate');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeUpdate')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeUpdate')
).resolves.toBeNull();
await expect(
@@ -469,9 +453,7 @@ describe('postgres entity integration', () => {
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value afterUpdate');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterUpdate')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterUpdate')
).resolves.toBeNull();
await expect(
@@ -480,9 +462,7 @@ describe('postgres entity integration', () => {
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value beforeAll');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeAll')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeAll')
).resolves.toBeNull();
await expect(
@@ -491,9 +471,7 @@ describe('postgres entity integration', () => {
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value afterAll');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterAll')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterAll')
).resolves.toBeNull();
await expect(
@@ -502,9 +480,7 @@ describe('postgres entity integration', () => {
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value afterCommit');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterCommit')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterCommit')
).resolves.not.toBeNull();
});
});
@@ -522,9 +498,7 @@ describe('postgres entity integration', () => {
PostgresTriggerTestEntity.enforceDeleteAsync(entityBeforeDelete)
).rejects.toThrowError('name cannot have value beforeDelete');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeDelete')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'beforeDelete')
).resolves.not.toBeNull();
const entityAfterDelete = await PostgresTriggerTestEntity.creator(vc1)
@@ -534,9 +508,7 @@ describe('postgres entity integration', () => {
PostgresTriggerTestEntity.enforceDeleteAsync(entityAfterDelete)
).rejects.toThrowError('name cannot have value afterDelete');
await expect(
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterDelete')
PostgresTriggerTestEntity.loader(vc1).loadByFieldEqualingAsync('name', 'afterDelete')
).resolves.not.toBeNull();
});
});
@@ -553,9 +525,10 @@ describe('postgres entity integration', () => {
.enforceCreateAsync()
).rejects.toThrowError('name cannot have value beforeCreateAndBeforeUpdate');
await expect(
PostgresValidatorTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeCreateAndBeforeUpdate')
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
'name',
'beforeCreateAndBeforeUpdate'
)
).resolves.toBeNull();
});
});
@@ -575,9 +548,10 @@ describe('postgres entity integration', () => {
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value beforeCreateAndBeforeUpdate');
await expect(
PostgresValidatorTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeCreateAndBeforeUpdate')
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
'name',
'beforeCreateAndBeforeUpdate'
)
).resolves.toBeNull();
});
});
@@ -592,9 +566,10 @@ describe('postgres entity integration', () => {
.enforceCreateAsync();
await PostgresValidatorTestEntity.enforceDeleteAsync(entityToDelete);
await expect(
PostgresValidatorTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'shouldBeDeleted')
PostgresValidatorTestEntity.loader(vc1).loadByFieldEqualingAsync(
'name',
'shouldBeDeleted'
)
).resolves.toBeNull();
});
});
@@ -60,7 +60,7 @@ describe('postgres errors', () => {
const vc2 = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(shortTimeoutKnexInstance)
);
await expect(ErrorsTestEntity.loader(vc2).enforcing().loadByIDAsync(1)).rejects.toThrow(
await expect(ErrorsTestEntity.loader(vc2).loadByIDAsync(1)).rejects.toThrow(
EntityDatabaseAdapterTransientError
);
await shortTimeoutKnexInstance.destroy();
@@ -19,9 +19,10 @@ router.get('/', async (ctx: ExampleContext) => {
const viewerContext = ctx.state.viewerContext;
let notes: readonly NoteEntity[] = [];
if (viewerContext.isUserViewerContext()) {
notes = await NoteEntity.loader(viewerContext)
.enforcing()
.loadManyByFieldEqualingAsync('userID', viewerContext.userID);
notes = await NoteEntity.loader(viewerContext).loadManyByFieldEqualingAsync(
'userID',
viewerContext.userID
);
}
ctx.body = {
notes: notes.map((note) => note.getAllFields()),
+4 -10
View File
@@ -46,16 +46,14 @@ export const resolvers: IResolvers<any, GraphqlContext> = {
return viewerContext.userID;
},
async noteByID(_root, args, { viewerContext }) {
return await NoteEntity.loader(viewerContext).enforcing().loadByIDAsync(args.id);
return await NoteEntity.loader(viewerContext).loadByIDAsync(args.id);
},
} as IResolverObject<any, GraphqlContext>,
User: {
id: (root) => root,
async notes(root, _args, { viewerContext }) {
return await NoteEntity.loader(viewerContext)
.enforcing()
.loadManyByFieldEqualingAsync('userID', root);
return await NoteEntity.loader(viewerContext).loadManyByFieldEqualingAsync('userID', root);
},
} as IResolverObject<string, GraphqlContext>,
@@ -79,18 +77,14 @@ export const resolvers: IResolvers<any, GraphqlContext> = {
.enforceCreateAsync();
},
async updateNote(_root, args, { viewerContext }) {
const existingNote = await NoteEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(args.id);
const existingNote = await NoteEntity.loader(viewerContext).loadByIDAsync(args.id);
return await NoteEntity.updater(existingNote)
.setField('title', args.note.title)
.setField('body', args.note.body)
.enforceUpdateAsync();
},
async deleteNote(_root, args, { viewerContext }) {
const existingNote = await NoteEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(args.id);
const existingNote = await NoteEntity.loader(viewerContext).loadByIDAsync(args.id);
await NoteEntity.enforceDeleteAsync(existingNote);
return existingNote;
},
@@ -153,10 +153,8 @@ describe('Entity cache inconsistency', () => {
.enforceCreateAsync();
// put entities in cache and dataloader
await TestEntity.loader(viewerContext).enforcing().loadByIDAsync(entity1.getID());
await TestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('other_string', 'hello');
await TestEntity.loader(viewerContext).loadByIDAsync(entity1.getID());
await TestEntity.loader(viewerContext).loadByFieldEqualingAsync('other_string', 'hello');
let openBarrier1: () => void;
const barrier1 = new Promise<void>((resolve) => {
@@ -179,7 +177,7 @@ describe('Entity cache inconsistency', () => {
const viewerContextInternal = new ViewerContext(
createFullIntegrationTestEntityCompanionProvider(knexInstance, redisCacheAdapterContext)
);
await TestEntity.loader(viewerContextInternal).enforcing().loadByIDAsync(entity1.getID());
await TestEntity.loader(viewerContextInternal).loadByIDAsync(entity1.getID());
openBarrier2!();
})(),
@@ -203,12 +201,11 @@ describe('Entity cache inconsistency', () => {
createFullIntegrationTestEntityCompanionProvider(knexInstance, redisCacheAdapterContext)
);
const loadedById = await TestEntity.loader(viewerContextLast)
.enforcing()
.loadByIDAsync(entity1.getID());
const loadedByField = await TestEntity.loader(viewerContextLast)
.enforcing()
.loadByFieldEqualingAsync('other_string', 'hello');
const loadedById = await TestEntity.loader(viewerContextLast).loadByIDAsync(entity1.getID());
const loadedByField = await TestEntity.loader(viewerContextLast).loadByFieldEqualingAsync(
'other_string',
'hello'
);
expect(loadedById.getField('third_string')).toEqual('updated');
expect(loadedByField!.getField('third_string')).toEqual('updated');
@@ -87,22 +87,20 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
.enforceCreateAsync();
await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_id', parent.getID())
ChildEntity.loader(viewerContext).loadByFieldEqualingAsync('parent_id', parent.getID())
).resolves.not.toBeNull();
await ParentEntity.enforceDeleteAsync(parent);
await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();
await expect(
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
).resolves.toBeNull();
});
});
@@ -224,18 +224,18 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
if (edgeDeletionBehavior === EntityEdgeDeletionBehavior.SET_NULL) {
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(category1.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(category1.getID())
).resolves.toBeNull();
const otherLoaded = await OtherEntity.loader(viewerContext)
.enforcing()
.loadByIDNullableAsync(other1.getID());
const otherLoaded = await OtherEntity.loader(viewerContext).loadByIDNullableAsync(
other1.getID()
);
expect(otherLoaded?.getField('parent_category_id')).toBeNull();
} else {
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(category1.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(category1.getID())
).resolves.toBeNull();
await expect(
OtherEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(other1.getID())
OtherEntity.loader(viewerContext).loadByIDNullableAsync(other1.getID())
).resolves.toBeNull();
}
});
@@ -159,11 +159,9 @@ class TestSecondaryLocalMemoryCacheLoader extends EntitySecondaryCacheLoader<
}
return nullthrows(
(
await this.entityLoader
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'id', fieldValue: loadParams.id },
])
await this.entityLoader.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'id', fieldValue: loadParams.id },
])
)[0]
).getAllFields();
});
@@ -40,11 +40,9 @@ class TestSecondaryRedisCacheLoader extends EntitySecondaryCacheLoader<
}
return nullthrows(
(
await this.entityLoader
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'id', fieldValue: loadParams.id },
])
await this.entityLoader.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'id', fieldValue: loadParams.id },
])
)[0]
).getAllFields();
});
@@ -1,144 +0,0 @@
import { FieldEqualityCondition, QuerySelectionModifiers } from './EntityDatabaseAdapter';
import EntityLoader from './EntityLoader';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
import { mapMap } from './utils/collections/maps';
/**
* Enforcing view on an entity loader. All loads through this loader will throw
* if the loads are not successful.
*/
export default class EnforcingEntityLoader<
TFields,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields
> {
constructor(
private readonly entityLoader: EntityLoader<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>
) {}
/**
* Enforcing version of entity loader method by the same name.
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
*/
async loadManyByFieldEqualingManyAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly TEntity[]>> {
const fieldValuesToResults = await this.entityLoader.loadManyByFieldEqualingManyAsync(
fieldName,
fieldValues
);
return mapMap(fieldValuesToResults, (results) =>
results.map((result) => result.enforceValue())
);
}
/**
* Enforcing version of entity loader method by the same name.
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
*/
async loadManyByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>
): Promise<readonly TEntity[]> {
const entityResults = await this.entityLoader.loadManyByFieldEqualingAsync(
fieldName,
fieldValue
);
return entityResults.map((result) => result.enforceValue());
}
/**
* Enforcing version of entity loader method by the same name.
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view the returned entity
* @throws when multiple entities are found matching the condition
*/
async loadByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
uniqueFieldName: N,
fieldValue: NonNullable<TFields[N]>
): Promise<TEntity | null> {
const entityResult = await this.entityLoader.loadByFieldEqualingAsync(
uniqueFieldName,
fieldValue
);
return entityResult ? entityResult.enforceValue() : null;
}
/**
* Enforcing version of entity loader method by the same name.
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view the returned entity
*/
async loadByIDAsync(id: TID): Promise<TEntity> {
const entityResult = await this.entityLoader.loadByIDAsync(id);
return entityResult.enforceValue();
}
/**
* Enforcing version of entity loader method by the same name.
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view the returned entity
* @throws when multiple entities are found matching the condition
*/
async loadByIDNullableAsync(id: TID): Promise<TEntity | null> {
const entityResult = await this.entityLoader.loadByIDNullableAsync(id);
return entityResult ? entityResult.enforceValue() : null;
}
/**
* Enforcing version of entity loader method by the same name.
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
*/
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, TEntity>> {
const entityResults = await this.entityLoader.loadManyByIDsAsync(ids);
return mapMap(entityResults, (result) => result.enforceValue());
}
/**
* Enforcing version of entity loader method by the same name.
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
*/
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
): Promise<readonly TEntity[]> {
const entityResults = await this.entityLoader.loadManyByFieldEqualityConjunctionAsync(
fieldEqualityOperands,
querySelectionModifiers
);
return entityResults.map((result) => result.enforceValue());
}
/**
* Enforcing version of entity loader method by the same name.
* @throws {@link EntityNotAuthorizedError} when viewer is not authorized to view one or more of the returned entities
*/
async loadManyByRawWhereClauseAsync(
rawWhereClause: string,
bindings: any[] | object,
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
): Promise<readonly TEntity[]> {
const entityResults = await this.entityLoader.loadManyByRawWhereClauseAsync(
rawWhereClause,
bindings,
querySelectionModifiers
);
return entityResults.map((result) => result.enforceValue());
}
}
@@ -71,7 +71,9 @@ export default class EntityAssociationLoader<
.getLoaderFactory()
.forLoad(queryContext, { cascadingDeleteCause: null });
return (await loader.loadByIDAsync(associatedEntityID as unknown as TAssociatedID)) as Result<
return (await loader.resultLoader.loadByIDAsync(
associatedEntityID as unknown as TAssociatedID
)) as Result<
null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity
>;
}
@@ -124,7 +126,7 @@ export default class EntityAssociationLoader<
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext, { cascadingDeleteCause: null });
return await loader.loadManyByFieldEqualingAsync(
return await loader.resultLoader.loadManyByFieldEqualingAsync(
associatedEntityFieldContainingThisID,
thisID as any
);
@@ -181,7 +183,7 @@ export default class EntityAssociationLoader<
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext, { cascadingDeleteCause: null });
return await loader.loadByFieldEqualingAsync(
return await loader.resultLoader.loadByFieldEqualingAsync(
associatedEntityLookupByField,
associatedFieldValue as any
);
@@ -239,7 +241,7 @@ export default class EntityAssociationLoader<
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getLoaderFactory()
.forLoad(queryContext, { cascadingDeleteCause: null });
return await loader.loadManyByFieldEqualingAsync(
return await loader.resultLoader.loadManyByFieldEqualingAsync(
associatedEntityLookupByField,
associatedFieldValue as any
);
+70 -195
View File
@@ -1,23 +1,14 @@
import { Result, asyncResult, result } from '@expo/results';
import invariant from 'invariant';
import EnforcingEntityLoader from './EnforcingEntityLoader';
import { IEntityClass } from './Entity';
import EntityConfiguration from './EntityConfiguration';
import {
FieldEqualityCondition,
QuerySelectionModifiers,
isSingleValueFieldEqualityCondition,
} from './EntityDatabaseAdapter';
import { FieldEqualityCondition, QuerySelectionModifiers } from './EntityDatabaseAdapter';
import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy';
import { EntityQueryContext } from './EntityQueryContext';
import EntityResultLoader from './EntityResultLoader';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
import EntityInvalidFieldValueError from './errors/EntityInvalidFieldValueError';
import EntityNotFoundError from './errors/EntityNotFoundError';
import EntityDataManager from './internal/EntityDataManager';
import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
import { mapMap, mapMapAsync } from './utils/collections/maps';
import { mapMap } from './utils/collections/maps';
/**
* The primary interface for loading entities. All normal loads are batched,
@@ -37,12 +28,24 @@ export default class EntityLoader<
>,
TSelectedFields extends keyof TFields
> {
/**
* Result-based view on an entity loader. All loads through this loader will wrap their results in Result.
*/
public readonly resultLoader: EntityResultLoader<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>;
constructor(
private readonly viewerContext: TViewerContext,
private readonly queryContext: EntityQueryContext,
private readonly privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext,
private readonly entityConfiguration: EntityConfiguration<TFields>,
private readonly entityClass: IEntityClass<
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext,
entityConfiguration: EntityConfiguration<TFields>,
entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
@@ -50,133 +53,105 @@ export default class EntityLoader<
TPrivacyPolicy,
TSelectedFields
>,
private readonly privacyPolicy: TPrivacyPolicy,
privacyPolicy: TPrivacyPolicy,
private readonly dataManager: EntityDataManager<TFields>,
protected readonly metricsAdapter: IEntityMetricsAdapter
) {}
/**
* Enforcing view on this entity loader. All loads through this view are
* guaranteed to be the values of successful results (or null for some loader methods),
* and will throw otherwise.
*/
enforcing(): EnforcingEntityLoader<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
> {
return new EnforcingEntityLoader(this);
) {
this.resultLoader = new EntityResultLoader(
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
entityConfiguration,
entityClass,
privacyPolicy,
dataManager,
metricsAdapter
);
}
/**
* Load many entities where fieldName is one of fieldValues.
* @param fieldName - entity field being queried
* @param fieldValues - fieldName field values being queried
* @returns map from fieldValue to entity results that match the query for that fieldValue,
* where result errors can be UnauthorizedError
* @returns map from fieldValue to entities that match the query for that fieldValue
*/
async loadManyByFieldEqualingManyAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Result<TEntity>[]>> {
this.validateFieldValues(fieldName, fieldValues);
const fieldValuesToFieldObjects = await this.dataManager.loadManyByFieldEqualingAsync(
this.queryContext,
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly TEntity[]>> {
const fieldValuesToResults = await this.resultLoader.loadManyByFieldEqualingManyAsync(
fieldName,
fieldValues
);
return await this.constructAndAuthorizeEntitiesAsync(fieldValuesToFieldObjects);
return mapMap(fieldValuesToResults, (results) =>
results.map((result) => result.enforceValue())
);
}
/**
* Load many entities where fieldName equals fieldValue.
* @param fieldName - entity field being queried
* @param fieldValue - fieldName field value being queried
* @returns array of entity results that match the query for fieldValue, where result error can be UnauthorizedError
* @returns array of entities that match the query for fieldValue
*/
async loadManyByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>
): Promise<readonly Result<TEntity>[]> {
const entityResults = await this.loadManyByFieldEqualingManyAsync(fieldName, [fieldValue]);
const entityResultsForFieldValue = entityResults.get(fieldValue);
invariant(
entityResultsForFieldValue !== undefined,
`${fieldValue} should be guaranteed to be present in returned map of entities`
): Promise<readonly TEntity[]> {
const entityResults = await this.resultLoader.loadManyByFieldEqualingAsync(
fieldName,
fieldValue
);
return entityResultsForFieldValue!;
return entityResults.map((result) => result.enforceValue());
}
/**
* Load an entity where fieldName equals fieldValue, or null if no entity exists.
* @param uniqueFieldName - entity field being queried
* @param fieldValue - uniqueFieldName field value being queried
* @returns entity result where uniqueFieldName equals fieldValue, or null if no entity matches the condition.
* @returns entity where uniqueFieldName equals fieldValue, or null if no entity matches the condition.
* @throws when multiple entities match the condition
*/
async loadByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
uniqueFieldName: N,
fieldValue: NonNullable<TFields[N]>
): Promise<Result<TEntity> | null> {
const entityResults = await this.loadManyByFieldEqualingAsync(uniqueFieldName, fieldValue);
invariant(
entityResults.length <= 1,
`loadByFieldEqualing: Multiple entities of type ${this.entityClass.name} found for ${uniqueFieldName}=${fieldValue}`
): Promise<TEntity | null> {
const entityResult = await this.resultLoader.loadByFieldEqualingAsync(
uniqueFieldName,
fieldValue
);
return entityResults[0] ?? null;
return entityResult ? entityResult.enforceValue() : null;
}
/**
* Loads an entity by a specified ID.
* @param id - ID of the entity
* @returns entity result for matching ID, where result error can be UnauthorizedError or EntityNotFoundError.
* @returns entity for matching ID
*/
async loadByIDAsync(id: TID): Promise<Result<TEntity>> {
const entityResults = await this.loadManyByIDsAsync([id]);
const entityResult = entityResults.get(id);
if (entityResult === undefined) {
return result(
new EntityNotFoundError(this.entityClass, this.entityConfiguration.idField, id)
);
}
return entityResult;
async loadByIDAsync(id: TID): Promise<TEntity> {
const entityResult = await this.resultLoader.loadByIDAsync(id);
return entityResult.enforceValue();
}
/**
* Load an entity by a specified ID, or return null if non-existent.
* @param id - ID of the entity
* @returns entity result for matching ID, or null if no entity exists for ID.
* @returns entity for matching ID, or null if no entity exists for ID.
*/
async loadByIDNullableAsync(id: TID): Promise<Result<TEntity> | null> {
return await this.loadByFieldEqualingAsync(
this.entityConfiguration.idField as TSelectedFields,
id
);
async loadByIDNullableAsync(id: TID): Promise<TEntity | null> {
const entityResult = await this.resultLoader.loadByIDNullableAsync(id);
return entityResult ? entityResult.enforceValue() : null;
}
/**
* Loads many entities for a list of IDs.
* @param viewerContext - viewer context of loading user
* @param ids - IDs of the entities to load
* @returns map from ID to corresponding entity result, where result error can be UnauthorizedError or EntityNotFoundError.
* @returns map from ID to corresponding entity
*/
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, Result<TEntity>>> {
const entityResults = (await this.loadManyByFieldEqualingManyAsync(
this.entityConfiguration.idField as TSelectedFields,
ids
)) as ReadonlyMap<TID, readonly Result<TEntity>[]>;
return mapMap(entityResults, (entityResultsForId, id) => {
const entityResult = entityResultsForId[0];
return (
entityResult ??
result(new EntityNotFoundError(this.entityClass, this.entityConfiguration.idField, id))
);
});
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, TEntity>> {
const entityResults = await this.resultLoader.loadManyByIDsAsync(ids);
return mapMap(entityResults, (result) => result.enforceValue());
}
/**
@@ -191,41 +166,17 @@ export default class EntityLoader<
*
* @param fieldEqualityOperands - list of field equality WHERE clause operand specifications
* @param querySelectionModifiers - limit, offset, and orderBy for the query
* @returns array of entity results that match the query, where result error can be UnauthorizedError
* @returns array of entities that match the query
*/
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
): Promise<readonly Result<TEntity>[]> {
for (const fieldEqualityOperand of fieldEqualityOperands) {
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
? [fieldEqualityOperand.fieldValue]
: fieldEqualityOperand.fieldValues;
this.validateFieldValues(fieldEqualityOperand.fieldName, fieldValues);
}
const fieldObjects = await this.dataManager.loadManyByFieldEqualityConjunctionAsync(
this.queryContext,
): Promise<readonly TEntity[]> {
const entityResults = await this.resultLoader.loadManyByFieldEqualityConjunctionAsync(
fieldEqualityOperands,
querySelectionModifiers
);
const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
return await Promise.all(
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
if (!uncheckedEntityResult.ok) {
return uncheckedEntityResult;
}
return await asyncResult(
this.privacyPolicy.authorizeReadAsync(
this.viewerContext,
this.queryContext,
this.privacyPolicyEvaluationContext,
uncheckedEntityResult.value,
this.metricsAdapter
)
);
})
);
return entityResults.map((result) => result.enforceValue());
}
/**
@@ -248,7 +199,7 @@ export default class EntityLoader<
* @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
* @param bindings - array of positional bindings or object of named bindings
* @param querySelectionModifiers - limit, offset, and orderBy for the query
* @returns array of entity results that match the query, where result error can be UnauthorizedError
* @returns array of entities that match the query
* @throws Error when rawWhereClause or bindings are invalid
*
* @deprecated prefer caching loaders
@@ -257,30 +208,13 @@ export default class EntityLoader<
rawWhereClause: string,
bindings: any[] | object,
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
): Promise<readonly Result<TEntity>[]> {
const fieldObjects = await this.dataManager.loadManyByRawWhereClauseAsync(
this.queryContext,
): Promise<readonly TEntity[]> {
const entityResults = await this.resultLoader.loadManyByRawWhereClauseAsync(
rawWhereClause,
bindings,
querySelectionModifiers
);
const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
return await Promise.all(
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
if (!uncheckedEntityResult.ok) {
return uncheckedEntityResult;
}
return await asyncResult(
this.privacyPolicy.authorizeReadAsync(
this.viewerContext,
this.queryContext,
this.privacyPolicyEvaluationContext,
uncheckedEntityResult.value,
this.metricsAdapter
)
);
})
);
return entityResults.map((result) => result.enforceValue());
}
/**
@@ -299,63 +233,4 @@ export default class EntityLoader<
async invalidateEntityAsync(entity: TEntity): Promise<void> {
await this.invalidateFieldsAsync(entity.getAllDatabaseFields());
}
private tryConstructEntities(fieldsObjects: readonly TFields[]): readonly Result<TEntity>[] {
return fieldsObjects.map((fieldsObject) => {
try {
return result(new this.entityClass(this.viewerContext, fieldsObject));
} catch (e) {
if (!(e instanceof Error)) {
throw e;
}
return result(e);
}
});
}
/**
* Construct and authorize entities from fields map, returning error results for entities that fail
* to construct or fail to authorize.
*
* @param map - map from an arbitrary key type to an array of entity field objects
*/
public async constructAndAuthorizeEntitiesAsync<K>(
map: ReadonlyMap<K, readonly Readonly<TFields>[]>
): Promise<ReadonlyMap<K, readonly Result<TEntity>[]>> {
const uncheckedEntityResultsMap = mapMap(map, (fieldObjects) =>
this.tryConstructEntities(fieldObjects)
);
return await mapMapAsync(uncheckedEntityResultsMap, async (uncheckedEntityResults) => {
return await Promise.all(
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
if (!uncheckedEntityResult.ok) {
return uncheckedEntityResult;
}
return await asyncResult(
this.privacyPolicy.authorizeReadAsync(
this.viewerContext,
this.queryContext,
this.privacyPolicyEvaluationContext,
uncheckedEntityResult.value,
this.metricsAdapter
)
);
})
);
});
}
private validateFieldValues<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValues: readonly TFields[N][]
): void {
const fieldDefinition = this.entityConfiguration.schema.get(fieldName);
invariant(fieldDefinition, `must have field definition for field = ${fieldName}`);
for (const fieldValue of fieldValues) {
const isInputValid = fieldDefinition.validateInputValue(fieldValue);
if (!isInputValid) {
throw new EntityInvalidFieldValueError(this.entityClass, fieldName, fieldValue);
}
}
}
}
+4 -8
View File
@@ -261,9 +261,7 @@ export class CreateMutator<
);
const unauthorizedEntityAfterInsert = new this.entityClass(this.viewerContext, insertResult);
const newEntity = await entityLoader
.enforcing()
.loadByIDAsync(unauthorizedEntityAfterInsert.getID());
const newEntity = await entityLoader.loadByIDAsync(unauthorizedEntityAfterInsert.getID());
await this.executeMutationTriggersAsync(
this.mutationTriggers.afterCreate,
@@ -469,9 +467,7 @@ export class UpdateMutator<
);
const unauthorizedEntityAfterUpdate = new this.entityClass(this.viewerContext, updateResult);
const updatedEntity = await entityLoader
.enforcing()
.loadByIDAsync(unauthorizedEntityAfterUpdate.getID());
const updatedEntity = await entityLoader.loadByIDAsync(unauthorizedEntityAfterUpdate.getID());
await this.executeMutationTriggersAsync(
this.mutationTriggers.afterUpdate,
@@ -759,7 +755,7 @@ export class DeleteMutator<
if (associatedEntityLookupByField) {
inboundReferenceEntities = await loaderFactory
.forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause })
.enforcing()
.loadManyByFieldEqualingAsync(
fieldName,
entity.getField(associatedEntityLookupByField as any)
@@ -767,7 +763,7 @@ export class DeleteMutator<
} else {
inboundReferenceEntities = await loaderFactory
.forLoad(queryContext, { cascadingDeleteCause: newCascadingDeleteCause })
.enforcing()
.loadManyByFieldEqualingAsync(fieldName, entity.getID());
}
+326
View File
@@ -0,0 +1,326 @@
import { Result, asyncResult, result } from '@expo/results';
import invariant from 'invariant';
import { IEntityClass } from './Entity';
import EntityConfiguration from './EntityConfiguration';
import {
FieldEqualityCondition,
QuerySelectionModifiers,
isSingleValueFieldEqualityCondition,
} from './EntityDatabaseAdapter';
import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy';
import { EntityQueryContext } from './EntityQueryContext';
import ReadonlyEntity from './ReadonlyEntity';
import ViewerContext from './ViewerContext';
import EntityInvalidFieldValueError from './errors/EntityInvalidFieldValueError';
import EntityNotFoundError from './errors/EntityNotFoundError';
import EntityDataManager from './internal/EntityDataManager';
import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter';
import { mapMap, mapMapAsync } from './utils/collections/maps';
/**
* Result-based view on an entity loader. All loads through this loader will wrap their results in Result.
*/
export default class EntityResultLoader<
TFields,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TPrivacyPolicy extends EntityPrivacyPolicy<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>,
TSelectedFields extends keyof TFields
> {
constructor(
private readonly viewerContext: TViewerContext,
private readonly queryContext: EntityQueryContext,
private readonly privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext,
private readonly entityConfiguration: EntityConfiguration<TFields>,
private readonly entityClass: IEntityClass<
TFields,
TID,
TViewerContext,
TEntity,
TPrivacyPolicy,
TSelectedFields
>,
private readonly privacyPolicy: TPrivacyPolicy,
private readonly dataManager: EntityDataManager<TFields>,
protected readonly metricsAdapter: IEntityMetricsAdapter
) {}
/**
* Load many entity results where fieldName is one of fieldValues.
* @param fieldName - entity field being queried
* @param fieldValues - fieldName field values being queried
* @returns map from fieldValue to entity results that match the query for that fieldValue,
* where result errors can be UnauthorizedError
*/
async loadManyByFieldEqualingManyAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
): Promise<ReadonlyMap<NonNullable<TFields[N]>, readonly Result<TEntity>[]>> {
this.validateFieldValues(fieldName, fieldValues);
const fieldValuesToFieldObjects = await this.dataManager.loadManyByFieldEqualingAsync(
this.queryContext,
fieldName,
fieldValues
);
return await this.constructAndAuthorizeEntitiesAsync(fieldValuesToFieldObjects);
}
/**
* Load many entity results where fieldName equals fieldValue.
* @param fieldName - entity field being queried
* @param fieldValue - fieldName field value being queried
* @returns array of entity results that match the query for fieldValue, where result error can be UnauthorizedError
*/
async loadManyByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValue: NonNullable<TFields[N]>
): Promise<readonly Result<TEntity>[]> {
const entityResults = await this.loadManyByFieldEqualingManyAsync(fieldName, [fieldValue]);
const entityResultsForFieldValue = entityResults.get(fieldValue);
invariant(
entityResultsForFieldValue !== undefined,
`${fieldValue} should be guaranteed to be present in returned map of entities`
);
return entityResultsForFieldValue!;
}
/**
* Load an entity result where fieldName equals fieldValue, or null if no entity exists.
* @param uniqueFieldName - entity field being queried
* @param fieldValue - uniqueFieldName field value being queried
* @returns entity result where uniqueFieldName equals fieldValue, or null if no entity matches the condition.
* @throws when multiple entities match the condition
*/
async loadByFieldEqualingAsync<N extends keyof Pick<TFields, TSelectedFields>>(
uniqueFieldName: N,
fieldValue: NonNullable<TFields[N]>
): Promise<Result<TEntity> | null> {
const entityResults = await this.loadManyByFieldEqualingAsync(uniqueFieldName, fieldValue);
invariant(
entityResults.length <= 1,
`loadByFieldEqualing: Multiple entities of type ${this.entityClass.name} found for ${uniqueFieldName}=${fieldValue}`
);
return entityResults[0] ?? null;
}
/**
* Loads an entityresult by a specified ID.
* @param id - ID of the entity
* @returns entity result for matching ID, where result error can be UnauthorizedError or EntityNotFoundError.
*/
async loadByIDAsync(id: TID): Promise<Result<TEntity>> {
const entityResults = await this.loadManyByIDsAsync([id]);
const entityResult = entityResults.get(id);
if (entityResult === undefined) {
return result(
new EntityNotFoundError(this.entityClass, this.entityConfiguration.idField, id)
);
}
return entityResult;
}
/**
* Load an entity result by a specified ID, or return null if non-existent.
* @param id - ID of the entity
* @returns entity result for matching ID, or null if no entity exists for ID.
*/
async loadByIDNullableAsync(id: TID): Promise<Result<TEntity> | null> {
return await this.loadByFieldEqualingAsync(
this.entityConfiguration.idField as TSelectedFields,
id
);
}
/**
* Loads many entity results for a list of IDs.
* @param viewerContext - viewer context of loading user
* @param ids - IDs of the entities to load
* @returns map from ID to corresponding entity result, where result error can be UnauthorizedError or EntityNotFoundError.
*/
async loadManyByIDsAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, Result<TEntity>>> {
const entityResults = (await this.loadManyByFieldEqualingManyAsync(
this.entityConfiguration.idField as TSelectedFields,
ids
)) as ReadonlyMap<TID, readonly Result<TEntity>[]>;
return mapMap(entityResults, (entityResultsForId, id) => {
const entityResult = entityResultsForId[0];
return (
entityResult ??
result(new EntityNotFoundError(this.entityClass, this.entityConfiguration.idField, id))
);
});
}
/**
* Loads many entity results matching the conjunction of WHERE clauses constructed from specified operands.
* Entities loaded using this method are not batched or cached.
*
* @example
* fieldEqualityOperands:
* `[{fieldName: 'hello', fieldValue: 1}, {fieldName: 'world', fieldValues: [2, 3]}]`
* Entities returned:
* `WHERE hello = 1 AND world = ANY({2, 3})`
*
* @param fieldEqualityOperands - list of field equality WHERE clause operand specifications
* @param querySelectionModifiers - limit, offset, and orderBy for the query
* @returns array of entity results that match the query, where result error can be UnauthorizedError
*/
async loadManyByFieldEqualityConjunctionAsync<N extends keyof Pick<TFields, TSelectedFields>>(
fieldEqualityOperands: FieldEqualityCondition<TFields, N>[],
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
): Promise<readonly Result<TEntity>[]> {
for (const fieldEqualityOperand of fieldEqualityOperands) {
const fieldValues = isSingleValueFieldEqualityCondition(fieldEqualityOperand)
? [fieldEqualityOperand.fieldValue]
: fieldEqualityOperand.fieldValues;
this.validateFieldValues(fieldEqualityOperand.fieldName, fieldValues);
}
const fieldObjects = await this.dataManager.loadManyByFieldEqualityConjunctionAsync(
this.queryContext,
fieldEqualityOperands,
querySelectionModifiers
);
const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
return await Promise.all(
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
if (!uncheckedEntityResult.ok) {
return uncheckedEntityResult;
}
return await asyncResult(
this.privacyPolicy.authorizeReadAsync(
this.viewerContext,
this.queryContext,
this.privacyPolicyEvaluationContext,
uncheckedEntityResult.value,
this.metricsAdapter
)
);
})
);
}
/**
* Loads many entity results matching the raw WHERE clause. Corresponds to the knex `whereRaw` argument format.
*
* @remarks
* Important notes:
* - Fields in clause are database column names instead of transformed entity field names.
* - Entities loaded using this method are not batched or cached.
* - Not all database adapters implement the ability to execute this method of fetching entities.
*
* @example
* rawWhereClause: `id = ?`
* bindings: `[1]`
* Entites returned `WHERE id = 1`
*
* {@link http://knexjs.org/#Builder-whereRaw}
* {@link http://knexjs.org/#Raw-Bindings}
*
* @param rawWhereClause - parameterized SQL WHERE clause with positional binding placeholders or named binding placeholders
* @param bindings - array of positional bindings or object of named bindings
* @param querySelectionModifiers - limit, offset, and orderBy for the query
* @returns array of entity results that match the query, where result error can be UnauthorizedError
* @throws Error when rawWhereClause or bindings are invalid
*
* @deprecated prefer caching loaders
*/
async loadManyByRawWhereClauseAsync(
rawWhereClause: string,
bindings: any[] | object,
querySelectionModifiers: QuerySelectionModifiers<TFields> = {}
): Promise<readonly Result<TEntity>[]> {
const fieldObjects = await this.dataManager.loadManyByRawWhereClauseAsync(
this.queryContext,
rawWhereClause,
bindings,
querySelectionModifiers
);
const uncheckedEntityResults = this.tryConstructEntities(fieldObjects);
return await Promise.all(
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
if (!uncheckedEntityResult.ok) {
return uncheckedEntityResult;
}
return await asyncResult(
this.privacyPolicy.authorizeReadAsync(
this.viewerContext,
this.queryContext,
this.privacyPolicyEvaluationContext,
uncheckedEntityResult.value,
this.metricsAdapter
)
);
})
);
}
private tryConstructEntities(fieldsObjects: readonly TFields[]): readonly Result<TEntity>[] {
return fieldsObjects.map((fieldsObject) => {
try {
return result(new this.entityClass(this.viewerContext, fieldsObject));
} catch (e) {
if (!(e instanceof Error)) {
throw e;
}
return result(e);
}
});
}
/**
* Construct and authorize entities from fields map, returning error results for entities that fail
* to construct or fail to authorize.
*
* @param map - map from an arbitrary key type to an array of entity field objects
*/
public async constructAndAuthorizeEntitiesAsync<K>(
map: ReadonlyMap<K, readonly Readonly<TFields>[]>
): Promise<ReadonlyMap<K, readonly Result<TEntity>[]>> {
const uncheckedEntityResultsMap = mapMap(map, (fieldObjects) =>
this.tryConstructEntities(fieldObjects)
);
return await mapMapAsync(uncheckedEntityResultsMap, async (uncheckedEntityResults) => {
return await Promise.all(
uncheckedEntityResults.map(async (uncheckedEntityResult) => {
if (!uncheckedEntityResult.ok) {
return uncheckedEntityResult;
}
return await asyncResult(
this.privacyPolicy.authorizeReadAsync(
this.viewerContext,
this.queryContext,
this.privacyPolicyEvaluationContext,
uncheckedEntityResult.value,
this.metricsAdapter
)
);
})
);
});
}
private validateFieldValues<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValues: readonly TFields[N][]
): void {
const fieldDefinition = this.entityConfiguration.schema.get(fieldName);
invariant(fieldDefinition, `must have field definition for field = ${fieldName}`);
for (const fieldValue of fieldValues) {
const isInputValid = fieldDefinition.validateInputValue(fieldValue);
if (!isInputValid) {
throw new EntityInvalidFieldValueError(this.entityClass, fieldName, fieldValue);
}
}
}
}
@@ -1,15 +1,17 @@
import { result } from '@expo/results';
import { mock, instance, when, anything } from 'ts-mockito';
import EnforcingEntityLoader from '../EnforcingEntityLoader';
import EntityLoader from '../EntityLoader';
import EnforcingEntityLoader from '../EntityResultLoader';
describe(EnforcingEntityLoader, () => {
describe('loadManyByFieldEqualingManyAsync', () => {
it('throws when result is unsuccessful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const rejection = new Error();
when(entityLoaderMock.loadManyByFieldEqualingManyAsync(anything(), anything())).thenResolve(
when(
entityLoaderMock.resultLoader.loadManyByFieldEqualingManyAsync(anything(), anything())
).thenResolve(
new Map(
Object.entries({
hello: [result(rejection)],
@@ -26,7 +28,9 @@ describe(EnforcingEntityLoader, () => {
it('returns value when result is successful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = {};
when(entityLoaderMock.loadManyByFieldEqualingManyAsync(anything(), anything())).thenResolve(
when(
entityLoaderMock.resultLoader.loadManyByFieldEqualingManyAsync(anything(), anything())
).thenResolve(
new Map(
Object.entries({
hello: [result(resolved)],
@@ -51,9 +55,9 @@ describe(EnforcingEntityLoader, () => {
it('throws when result is unsuccessful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const rejection = new Error();
when(entityLoaderMock.loadManyByFieldEqualingAsync(anything(), anything())).thenResolve([
result(rejection),
]);
when(
entityLoaderMock.resultLoader.loadManyByFieldEqualingAsync(anything(), anything())
).thenResolve([result(rejection)]);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(
@@ -64,9 +68,9 @@ describe(EnforcingEntityLoader, () => {
it('returns value when result is successful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = {};
when(entityLoaderMock.loadManyByFieldEqualingAsync(anything(), anything())).thenResolve([
result(resolved),
]);
when(
entityLoaderMock.resultLoader.loadManyByFieldEqualingAsync(anything(), anything())
).thenResolve([result(resolved)]);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(
@@ -79,9 +83,9 @@ describe(EnforcingEntityLoader, () => {
it('throws when result is unsuccessful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const rejection = new Error();
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenResolve(
result(rejection)
);
when(
entityLoaderMock.resultLoader.loadByFieldEqualingAsync(anything(), anything())
).thenResolve(result(rejection));
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(
@@ -92,9 +96,9 @@ describe(EnforcingEntityLoader, () => {
it('returns value when result is successful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = {};
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenResolve(
result(resolved)
);
when(
entityLoaderMock.resultLoader.loadByFieldEqualingAsync(anything(), anything())
).thenResolve(result(resolved));
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(
@@ -105,9 +109,9 @@ describe(EnforcingEntityLoader, () => {
it('returns null when result is successful and no entity is found', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = null;
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenResolve(
result(resolved)
);
when(
entityLoaderMock.resultLoader.loadByFieldEqualingAsync(anything(), anything())
).thenResolve(result(resolved));
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(
@@ -118,9 +122,9 @@ describe(EnforcingEntityLoader, () => {
it('throws when multiple matching entities are found', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const multipleEntitiesError = new Error();
when(entityLoaderMock.loadByFieldEqualingAsync(anything(), anything())).thenReject(
multipleEntitiesError
);
when(
entityLoaderMock.resultLoader.loadByFieldEqualingAsync(anything(), anything())
).thenReject(multipleEntitiesError);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(
@@ -133,7 +137,7 @@ describe(EnforcingEntityLoader, () => {
it('throws when result is unsuccessful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const rejection = new Error();
when(entityLoaderMock.loadByIDAsync(anything())).thenResolve(result(rejection));
when(entityLoaderMock.resultLoader.loadByIDAsync(anything())).thenResolve(result(rejection));
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(enforcingEntityLoader.loadByIDAsync(anything())).rejects.toThrow(rejection);
@@ -142,7 +146,7 @@ describe(EnforcingEntityLoader, () => {
it('returns value when result is successful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = {};
when(entityLoaderMock.loadByIDAsync(anything())).thenResolve(result(resolved));
when(entityLoaderMock.resultLoader.loadByIDAsync(anything())).thenResolve(result(resolved));
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(enforcingEntityLoader.loadByIDAsync(anything())).resolves.toEqual(resolved);
@@ -153,7 +157,9 @@ describe(EnforcingEntityLoader, () => {
it('throws when result is unsuccessful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const rejection = new Error();
when(entityLoaderMock.loadByIDNullableAsync(anything())).thenResolve(result(rejection));
when(entityLoaderMock.resultLoader.loadByIDNullableAsync(anything())).thenResolve(
result(rejection)
);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(enforcingEntityLoader.loadByIDNullableAsync(anything())).rejects.toThrow(
@@ -164,7 +170,9 @@ describe(EnforcingEntityLoader, () => {
it('returns value when result is successful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = {};
when(entityLoaderMock.loadByIDNullableAsync(anything())).thenResolve(result(resolved));
when(entityLoaderMock.resultLoader.loadByIDNullableAsync(anything())).thenResolve(
result(resolved)
);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(enforcingEntityLoader.loadByIDNullableAsync(anything())).resolves.toEqual(
@@ -175,7 +183,9 @@ describe(EnforcingEntityLoader, () => {
it('returns null when non-existent object', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = null;
when(entityLoaderMock.loadByIDNullableAsync(anything())).thenResolve(result(resolved));
when(entityLoaderMock.resultLoader.loadByIDNullableAsync(anything())).thenResolve(
result(resolved)
);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
await expect(enforcingEntityLoader.loadByIDNullableAsync(anything())).resolves.toEqual(
@@ -188,7 +198,7 @@ describe(EnforcingEntityLoader, () => {
it('throws when result is unsuccessful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const rejection = new Error();
when(entityLoaderMock.loadManyByIDsAsync(anything())).thenResolve(
when(entityLoaderMock.resultLoader.loadManyByIDsAsync(anything())).thenResolve(
new Map(
Object.entries({
hello: result(rejection),
@@ -203,7 +213,7 @@ describe(EnforcingEntityLoader, () => {
it('returns value when result is successful', async () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = {};
when(entityLoaderMock.loadManyByIDsAsync(anything())).thenResolve(
when(entityLoaderMock.resultLoader.loadManyByIDsAsync(anything())).thenResolve(
new Map(
Object.entries({
hello: result(resolved),
@@ -227,7 +237,10 @@ describe(EnforcingEntityLoader, () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const rejection = new Error();
when(
entityLoaderMock.loadManyByFieldEqualityConjunctionAsync(anything(), anything())
entityLoaderMock.resultLoader.loadManyByFieldEqualityConjunctionAsync(
anything(),
anything()
)
).thenResolve([result(rejection)]);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
@@ -240,7 +253,10 @@ describe(EnforcingEntityLoader, () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = {};
when(
entityLoaderMock.loadManyByFieldEqualityConjunctionAsync(anything(), anything())
entityLoaderMock.resultLoader.loadManyByFieldEqualityConjunctionAsync(
anything(),
anything()
)
).thenResolve([result(resolved)]);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
@@ -255,7 +271,11 @@ describe(EnforcingEntityLoader, () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const rejection = new Error();
when(
entityLoaderMock.loadManyByRawWhereClauseAsync(anything(), anything(), anything())
entityLoaderMock.resultLoader.loadManyByRawWhereClauseAsync(
anything(),
anything(),
anything()
)
).thenResolve([result(rejection)]);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
@@ -268,7 +288,11 @@ describe(EnforcingEntityLoader, () => {
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
const resolved = {};
when(
entityLoaderMock.loadManyByRawWhereClauseAsync(anything(), anything(), anything())
entityLoaderMock.resultLoader.loadManyByRawWhereClauseAsync(
anything(),
anything(),
anything()
)
).thenResolve([result(resolved)]);
const entityLoader = instance(entityLoaderMock);
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
@@ -129,15 +129,15 @@ it('runs through a common workflow', async () => {
// check that two people can't read each others data
await expect(
enforceAsyncResult(BlahEntity.loader(vc1).loadByIDAsync(blahOwner2.getID()))
enforceAsyncResult(BlahEntity.loader(vc1).resultLoader.loadByIDAsync(blahOwner2.getID()))
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
await expect(
enforceAsyncResult(BlahEntity.loader(vc2).loadByIDAsync(blahOwner1.getID()))
enforceAsyncResult(BlahEntity.loader(vc2).resultLoader.loadByIDAsync(blahOwner1.getID()))
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
// check that all of owner 1's objects can be loaded
const results = await enforceResultsAsync(
BlahEntity.loader(vc1).loadManyByFieldEqualingAsync('ownerID', vc1.getUserID()!)
BlahEntity.loader(vc1).resultLoader.loadManyByFieldEqualingAsync('ownerID', vc1.getUserID()!)
);
expect(results).toHaveLength(2);
@@ -149,7 +149,10 @@ it('runs through a common workflow', async () => {
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
// check that empty load many returns nothing
const results2 = await BlahEntity.loader(vc1).loadManyByFieldEqualingManyAsync('ownerID', []);
const results2 = await BlahEntity.loader(vc1).resultLoader.loadManyByFieldEqualingManyAsync(
'ownerID',
[]
);
for (const value in results2.values) {
expect(value).toHaveLength(0);
}
@@ -374,13 +374,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
.enforceCreateAsync();
await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
).resolves.not.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
GrandChildEntity.loader(viewerContext).loadByIDNullableAsync(grandchild.getID())
).resolves.not.toBeNull();
privacyPolicyEvaluationRecords.shouldRecord = true;
@@ -388,13 +388,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
privacyPolicyEvaluationRecords.shouldRecord = false;
await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();
await expect(
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
).resolves.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
GrandChildEntity.loader(viewerContext).loadByIDNullableAsync(grandchild.getID())
).resolves.toBeNull();
// two calls for each trigger, one beforeDelete, one afterDelete
@@ -488,13 +488,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
.enforceCreateAsync();
await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
).resolves.not.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
GrandChildEntity.loader(viewerContext).loadByIDNullableAsync(grandchild.getID())
).resolves.not.toBeNull();
privacyPolicyEvaluationRecords.shouldRecord = true;
@@ -502,16 +502,16 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
privacyPolicyEvaluationRecords.shouldRecord = false;
await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();
const loadedChild = await ChildEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(child.getID());
expect(loadedChild.getField('parent_id')).toBeNull();
const loadedGrandchild = await GrandChildEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(grandchild.getID());
expect(loadedGrandchild.getField('parent_id')).toEqual(loadedChild.getID());
@@ -595,16 +595,16 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
.enforceCreateAsync();
await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_id', parent.getID())
).resolves.not.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_id', child.getID())
).resolves.not.toBeNull();
@@ -639,13 +639,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
expect(grandChildCachedAfter.get(child.getID())?.status).toEqual(CacheStatus.MISS);
await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();
await expect(
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).loadByIDNullableAsync(child.getID())
).resolves.not.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
GrandChildEntity.loader(viewerContext).loadByIDNullableAsync(grandchild.getID())
).resolves.not.toBeNull();
// two calls for each trigger, one beforeDelete, one afterDelete
@@ -165,14 +165,14 @@ describe(EntityLoader, () => {
let capturedThrownThing1: any;
try {
await entityLoader.loadByIDAsync(ID_SENTINEL_THROW_LITERAL);
await entityLoader.resultLoader.loadByIDAsync(ID_SENTINEL_THROW_LITERAL);
} catch (e) {
capturedThrownThing1 = e;
}
expect(capturedThrownThing1).not.toBeInstanceOf(Error);
expect(capturedThrownThing1).toEqual('hello');
const result = await entityLoader.loadByIDAsync(ID_SENTINEL_THROW_ERROR);
const result = await entityLoader.resultLoader.loadByIDAsync(ID_SENTINEL_THROW_ERROR);
expect(result.ok).toBe(false);
expect(result.enforceError().message).toEqual('world');
});
@@ -78,36 +78,39 @@ describe(EntityLoader, () => {
dataManager,
metricsAdapter
);
const entity = await enforceAsyncResult(entityLoader.loadByIDAsync(id1));
const entity = await enforceAsyncResult(entityLoader.resultLoader.loadByIDAsync(id1));
expect(entity.getID()).toEqual(id1);
expect(entity.getField('dateField')).toEqual(dateToInsert);
const entities = await enforceResultsAsync(
entityLoader.loadManyByFieldEqualingAsync('stringField', 'huh')
entityLoader.resultLoader.loadManyByFieldEqualingAsync('stringField', 'huh')
);
expect(entities.map((m) => m.getID())).toEqual([id1, id2]);
const entityResultNumber3 = await entityLoader.loadByFieldEqualingAsync('intField', 3);
const entityResultNumber3 = await entityLoader.resultLoader.loadByFieldEqualingAsync('intField', 3);
expect(entityResultNumber3).not.toBeNull();
expect(entityResultNumber3!.enforceValue().getID()).toEqual(id2);
const entityResultNumber4 = await entityLoader.loadByFieldEqualingAsync('intField', 4);
const entityResultNumber4 = await entityLoader.resultLoader.loadByFieldEqualingAsync('intField', 4);
expect(entityResultNumber4).toBeNull();
const entityResultDuplicateValues = await entityLoader
.enforcing()
.loadManyByFieldEqualingManyAsync('stringField', ['huh', 'huh']);
const entityResultDuplicateValues = await entityLoader.loadManyByFieldEqualingManyAsync(
'stringField',
['huh', 'huh']
);
expect(entityResultDuplicateValues.size).toBe(1);
expect(entityResultDuplicateValues.get('huh')?.map((m) => m.getID())).toEqual([id1, id2]);
await expect(entityLoader.loadByFieldEqualingAsync('stringField', 'huh')).rejects.toThrowError(
await expect(
entityLoader.resultLoader.loadByFieldEqualingAsync('stringField', 'huh')
).rejects.toThrowError(
'loadByFieldEqualing: Multiple entities of type TestEntity found for stringField=huh'
);
await expect(entityLoader.loadByIDNullableAsync(uuidv4())).resolves.toBeNull();
await expect(entityLoader.loadByIDNullableAsync(id1)).resolves.not.toBeNull();
await expect(entityLoader.resultLoader.loadByIDNullableAsync(uuidv4())).resolves.toBeNull();
await expect(entityLoader.resultLoader.loadByIDNullableAsync(id1)).resolves.not.toBeNull();
await expect(entityLoader.loadByIDAsync('not-a-uuid')).rejects.toThrowError(
await expect(entityLoader.resultLoader.loadByIDAsync('not-a-uuid')).rejects.toThrowError(
'Entity field not valid: TestEntity (customIdField = not-a-uuid)'
);
});
@@ -181,7 +184,7 @@ describe(EntityLoader, () => {
metricsAdapter
);
const entities = await enforceResultsAsync(
entityLoader.loadManyByFieldEqualityConjunctionAsync([
entityLoader.resultLoader.loadManyByFieldEqualityConjunctionAsync([
{
fieldName: 'stringField',
fieldValue: 'huh',
@@ -204,7 +207,7 @@ describe(EntityLoader, () => {
).twice();
await expect(
entityLoader.loadManyByFieldEqualityConjunctionAsync([
entityLoader.resultLoader.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'customIdField', fieldValue: 'not-a-uuid' },
])
).rejects.toThrowError('Entity field not valid: TestEntity (customIdField = not-a-uuid)');
@@ -261,7 +264,7 @@ describe(EntityLoader, () => {
dataManager,
metricsAdapter
);
const entity = await enforceAsyncResult(entityLoader.loadByIDAsync(id1));
const entity = await enforceAsyncResult(entityLoader.resultLoader.loadByIDAsync(id1));
verify(
spiedPrivacyPolicy.authorizeReadAsync(
viewerContext,
@@ -395,7 +398,7 @@ describe(EntityLoader, () => {
metricsAdapter
);
const entityResult = await entityLoader.loadByIDAsync(id1);
const entityResult = await entityLoader.resultLoader.loadByIDAsync(id1);
expect(entityResult.ok).toBe(false);
expect(entityResult.reason).toEqual(rejectionError);
expect(entityResult.value).toBe(undefined);
@@ -430,21 +433,21 @@ describe(EntityLoader, () => {
const loadByValue = uuidv4();
await expect(entityLoader.resultLoader.loadByIDAsync(loadByValue)).rejects.toEqual(error);
await expect(entityLoader.loadByIDAsync(loadByValue)).rejects.toEqual(error);
await expect(entityLoader.enforcing().loadByIDAsync(loadByValue)).rejects.toEqual(error);
await expect(entityLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
await expect(entityLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
await expect(entityLoader.resultLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
await expect(entityLoader.resultLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
await expect(
entityLoader.resultLoader.loadManyByFieldEqualingAsync('customIdField', loadByValue)
).rejects.toEqual(error);
await expect(
entityLoader.loadManyByFieldEqualingAsync('customIdField', loadByValue)
).rejects.toEqual(error);
await expect(
entityLoader.enforcing().loadManyByFieldEqualingAsync('customIdField', loadByValue)
entityLoader.resultLoader.loadManyByFieldEqualingManyAsync('customIdField', [loadByValue])
).rejects.toEqual(error);
await expect(
entityLoader.loadManyByFieldEqualingManyAsync('customIdField', [loadByValue])
).rejects.toEqual(error);
await expect(
entityLoader.enforcing().loadManyByFieldEqualingManyAsync('customIdField', [loadByValue])
).rejects.toEqual(error);
});
});
@@ -78,7 +78,7 @@ class TestNonTransactionalMutationTrigger extends EntityNonTransactionalMutation
): Promise<void> {
if (mutationInfo.type === EntityMutationType.DELETE) {
const entityLoaded = await BlahEntity.loader(viewerContext)
.enforcing()
.loadByIDNullableAsync(entity.getID());
if (entityLoaded) {
throw new Error(
@@ -97,7 +97,7 @@ describe('EntityMutator', () => {
// put it in cache
const entity = await BlahEntity.creator(viewerContext).enforceCreateAsync();
const entityLoaded = await BlahEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity.getID());
await BlahEntity.enforceDeleteAsync(entityLoaded);
@@ -530,7 +530,7 @@ describe(EntityMutatorFactory, () => {
const existingEntity = await enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id2)
.resultLoader.loadByIDAsync(id2)
);
const updatedEntity = await entityMutatorFactory
@@ -545,7 +545,7 @@ describe(EntityMutatorFactory, () => {
const reloadedEntity = await enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id2)
.resultLoader.loadByIDAsync(id2)
);
expect(reloadedEntity.getAllFields()).toMatchObject(updatedEntity.getAllFields());
});
@@ -582,7 +582,7 @@ describe(EntityMutatorFactory, () => {
const existingEntity = await enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id2)
.resultLoader.loadByIDAsync(id2)
);
await entityMutatorFactory
@@ -633,7 +633,7 @@ describe(EntityMutatorFactory, () => {
const existingEntity = await enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id2)
.resultLoader.loadByIDAsync(id2)
);
await entityMutatorFactory
@@ -687,7 +687,7 @@ describe(EntityMutatorFactory, () => {
const existingEntity = await enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id2)
.resultLoader.loadByIDAsync(id2)
);
await entityMutatorFactory
@@ -723,7 +723,7 @@ describe(EntityMutatorFactory, () => {
const existingEntity = await enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id1)
.resultLoader.loadByIDAsync(id1)
);
expect(existingEntity).toBeTruthy();
@@ -735,7 +735,7 @@ describe(EntityMutatorFactory, () => {
enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id1)
.resultLoader.loadByIDAsync(id1)
)
).rejects.toBeInstanceOf(Error);
});
@@ -763,7 +763,7 @@ describe(EntityMutatorFactory, () => {
const existingEntity = await enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id1)
.resultLoader.loadByIDAsync(id1)
);
await entityMutatorFactory
@@ -804,7 +804,7 @@ describe(EntityMutatorFactory, () => {
const existingEntity = await enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id1)
.resultLoader.loadByIDAsync(id1)
);
await entityMutatorFactory
@@ -849,7 +849,7 @@ describe(EntityMutatorFactory, () => {
const existingEntity = await enforceAsyncResult(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadByIDAsync(id1)
.resultLoader.loadByIDAsync(id1)
);
await entityMutatorFactory
@@ -882,7 +882,7 @@ describe(EntityMutatorFactory, () => {
const entites1 = await enforceResultsAsync(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadManyByFieldEqualingAsync('stringField', 'huh')
.resultLoader.loadManyByFieldEqualingAsync('stringField', 'huh')
);
expect(entites1).toHaveLength(1);
@@ -896,7 +896,7 @@ describe(EntityMutatorFactory, () => {
const entities2 = await enforceResultsAsync(
entityLoaderFactory
.forLoad(viewerContext, queryContext, privacyPolicyEvaluationContext)
.loadManyByFieldEqualingAsync('stringField', 'huh')
.resultLoader.loadManyByFieldEqualingAsync('stringField', 'huh')
);
expect(entities2).toHaveLength(2);
});
@@ -98,25 +98,25 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE', () => {
.enforceCreateAsync();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subSubCategory.getID())
).resolves.not.toBeNull();
await CategoryEntity.enforceDeleteAsync(parentCategory);
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
).resolves.toBeNull();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subCategory.getID())
).resolves.toBeNull();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subSubCategory.getID())
).resolves.toBeNull();
});
@@ -137,10 +137,10 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE', () => {
await CategoryEntity.enforceDeleteAsync(categoryA);
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(categoryA.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(categoryA.getID())
).resolves.toBeNull();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(categoryB.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(categoryB.getID())
).resolves.toBeNull();
});
});
@@ -161,29 +161,29 @@ describe('EntityEdgeDeletionBehavior.SET_NULL', () => {
.enforceCreateAsync();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subSubCategory.getID())
).resolves.not.toBeNull();
await CategoryEntity.enforceDeleteAsync(parentCategory);
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
).resolves.toBeNull();
const loadedSubCategory = await CategoryEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(subCategory.getID());
const loadedSubCategory = await CategoryEntity.loader(viewerContext).loadByIDAsync(
subCategory.getID()
);
expect(loadedSubCategory.getField('parent_category_id')).toBeNull();
const loadedSubSubCategory = await CategoryEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(subSubCategory.getID());
const loadedSubSubCategory = await CategoryEntity.loader(viewerContext).loadByIDAsync(
subSubCategory.getID()
);
expect(loadedSubSubCategory.getField('parent_category_id')).not.toBeNull();
});
@@ -203,9 +203,9 @@ describe('EntityEdgeDeletionBehavior.SET_NULL', () => {
await CategoryEntity.enforceDeleteAsync(categoryA);
const loadedCategoryB = await CategoryEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(categoryB.getID());
const loadedCategoryB = await CategoryEntity.loader(viewerContext).loadByIDAsync(
categoryB.getID()
);
expect(loadedCategoryB.getField('parent_category_id')).toBeNull();
});
});
@@ -228,17 +228,19 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
.enforceCreateAsync();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_category_id', parentCategory.getID())
CategoryEntity.loader(viewerContext).loadByFieldEqualingAsync(
'parent_category_id',
parentCategory.getID()
)
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_category_id', subCategory.getID())
CategoryEntity.loader(viewerContext).loadByFieldEqualingAsync(
'parent_category_id',
subCategory.getID()
)
).resolves.not.toBeNull();
const categoryCacheAdapter = viewerContext.getViewerScopedEntityCompanionForClass(
@@ -271,13 +273,13 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
expect(subSubCategoryCachedAfter.get(subCategory.getID())?.status).toEqual(CacheStatus.MISS);
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(parentCategory.getID())
).resolves.toBeNull();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
CategoryEntity.loader(viewerContext).loadByIDNullableAsync(subSubCategory.getID())
).resolves.not.toBeNull();
});
@@ -298,14 +300,16 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
.enforceUpdateAsync();
await expect(
CategoryEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_category_id', categoryA.getID())
CategoryEntity.loader(viewerContext).loadByFieldEqualingAsync(
'parent_category_id',
categoryA.getID()
)
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_category_id', categoryB.getID())
CategoryEntity.loader(viewerContext).loadByFieldEqualingAsync(
'parent_category_id',
categoryB.getID()
)
).resolves.not.toBeNull();
const categoryCacheAdapter = viewerContext.getViewerScopedEntityCompanionForClass(
@@ -28,17 +28,16 @@ describe('Two entities backed by the same table', () => {
expect(two).toBeInstanceOf(TwoTestEntity);
await expect(
TwoTestEntity.loader(viewerContext).enforcing().loadByIDAsync(one.getID())
TwoTestEntity.loader(viewerContext).loadByIDAsync(one.getID())
).rejects.toThrowError('TwoTestEntity must be instantiated with two data');
await expect(
OneTestEntity.loader(viewerContext).enforcing().loadByIDAsync(two.getID())
OneTestEntity.loader(viewerContext).loadByIDAsync(two.getID())
).rejects.toThrowError('OneTestEntity must be instantiated with one data');
const manyResults = await OneTestEntity.loader(viewerContext).loadManyByFieldEqualingAsync(
'common_other_field',
'wat'
);
const manyResults = await OneTestEntity.loader(
viewerContext
).resultLoader.loadManyByFieldEqualingAsync('common_other_field', 'wat');
const successfulManyResults = successfulResults(manyResults);
const failedManyResults = failedResults(manyResults);
@@ -52,7 +51,7 @@ describe('Two entities backed by the same table', () => {
const fieldEqualityConjunctionResults = await OneTestEntity.loader(
viewerContext
).loadManyByFieldEqualityConjunctionAsync([
).resultLoader.loadManyByFieldEqualityConjunctionAsync([
{
fieldName: 'common_other_field',
fieldValue: 'wat',
@@ -18,7 +18,7 @@ describe('Two entities backed by the same table', () => {
expect(entity1).toBeInstanceOf(OneTestEntity);
const entity2 = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1.getID());
expect(entity2).toBeInstanceOf(TwoTestEntity);
@@ -33,7 +33,7 @@ describe('Two entities backed by the same table', () => {
});
const loaded1 = await OneTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1.getID());
expect(loaded1.getAllFields()).toMatchObject({
id: updated2.getID(),
@@ -51,7 +51,7 @@ describe('Two entities backed by the same table', () => {
.enforceCreateAsync();
const loadedEntity = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('other_field', 'huh');
expect(loadedEntity?.getAllFields()).toMatchObject({
id: entity.getID(),
@@ -60,12 +60,12 @@ describe('Two entities backed by the same table', () => {
});
const loaded1 = await OneTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity.getID());
await OneTestEntity.updater(loaded1).setField('fake_field', 'world').enforceUpdateAsync();
const loaded2 = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('other_field', 'huh');
expect(loaded2?.getAllFields()).toMatchObject({
id: entity.getID(),
@@ -74,7 +74,7 @@ describe('Two entities backed by the same table', () => {
});
const loaded22 = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('fake_field', 'world');
expect(loaded22?.getAllFields()).toMatchObject({
id: entity.getID(),
+1 -1
View File
@@ -7,7 +7,7 @@
export { default as ComposedEntityCacheAdapter } from './ComposedEntityCacheAdapter';
export { default as ComposedSecondaryEntityCache } from './ComposedSecondaryEntityCache';
export { default as GenericSecondaryEntityCache } from './GenericSecondaryEntityCache';
export { default as EnforcingEntityLoader } from './EnforcingEntityLoader';
export { default as EntityResultLoader } from './EntityResultLoader';
export { default as Entity } from './Entity';
export * from './Entity';
export { default as EntityAssociationLoader } from './EntityAssociationLoader';