diff --git a/packages/entity-cache-adapter-local-memory/src/__tests__/GenericLocalMemoryCacher-full-test.ts b/packages/entity-cache-adapter-local-memory/src/__tests__/GenericLocalMemoryCacher-full-test.ts index d79fb8cd..9387d56d 100644 --- a/packages/entity-cache-adapter-local-memory/src/__tests__/GenericLocalMemoryCacher-full-test.ts +++ b/packages/entity-cache-adapter-local-memory/src/__tests__/GenericLocalMemoryCacher-full-test.ts @@ -83,7 +83,7 @@ describe(GenericLocalMemoryCacher, () => { // invalidate from cache to ensure it invalidates correctly await LocalMemoryTestEntity.loader(viewerContext) - .withAuthorizationResults() + .utils() .invalidateFieldsAsync(entity1.getAllFields()); const cachedResultMiss = await entitySpecificGenericCacher.loadManyAsync([ cacheKeyMaker('id', entity1.getID()), diff --git a/packages/entity-cache-adapter-redis/src/__integration-tests__/BatchedRedisCacheAdapter-integration-test.ts b/packages/entity-cache-adapter-redis/src/__integration-tests__/BatchedRedisCacheAdapter-integration-test.ts index e9d68446..e1da8656 100644 --- a/packages/entity-cache-adapter-redis/src/__integration-tests__/BatchedRedisCacheAdapter-integration-test.ts +++ b/packages/entity-cache-adapter-redis/src/__integration-tests__/BatchedRedisCacheAdapter-integration-test.ts @@ -160,7 +160,7 @@ describe(GenericRedisCacher, () => { // invalidate from cache to ensure it invalidates correctly in both caches await RedisTestEntity.loader(viewerContext) - .withAuthorizationResults() + .utils() .invalidateFieldsAsync(entity1.getAllFields()); await expect(redis.get(cacheKeyEntity1)).resolves.toBeNull(); await expect(redis.get(cacheKeyEntity1NameField)).resolves.toBeNull(); diff --git a/packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-full-integration-test.ts b/packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-full-integration-test.ts index 24b24e4c..c0ca3917 100644 --- a/packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-full-integration-test.ts +++ b/packages/entity-cache-adapter-redis/src/__integration-tests__/GenericRedisCacher-full-integration-test.ts @@ -85,7 +85,7 @@ describe(GenericRedisCacher, () => { // invalidate from cache to ensure it invalidates correctly await RedisTestEntity.loader(viewerContext) - .withAuthorizationResults() + .utils() .invalidateFieldsAsync(entity1.getAllFields()); const cachedValueNull = await (genericRedisCacheContext.redisClient as Redis).get( cacheKeyMaker('id', entity1.getID()), diff --git a/packages/entity/src/AuthorizationResultBasedEntityLoader.ts b/packages/entity/src/AuthorizationResultBasedEntityLoader.ts index 6e5f5a7c..42ea1a65 100644 --- a/packages/entity/src/AuthorizationResultBasedEntityLoader.ts +++ b/packages/entity/src/AuthorizationResultBasedEntityLoader.ts @@ -1,4 +1,4 @@ -import { Result, asyncResult, result } from '@expo/results'; +import { Result, result } from '@expo/results'; import invariant from 'invariant'; import nullthrows from 'nullthrows'; @@ -10,16 +10,16 @@ import { isSingleValueFieldEqualityCondition, QuerySelectionModifiersWithOrderByRaw, } from './EntityDatabaseAdapter'; -import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy'; +import EntityLoaderUtils from './EntityLoaderUtils'; +import EntityPrivacyPolicy from './EntityPrivacyPolicy'; import { EntityQueryContext } from './EntityQueryContext'; import ReadonlyEntity from './ReadonlyEntity'; import ViewerContext from './ViewerContext'; -import { pick } from './entityUtils'; 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'; /** * Authorization-result-based entity loader. All normal loads are batched, @@ -42,17 +42,19 @@ export default class AuthorizationResultBasedEntityLoader< TSelectedFields extends keyof TFields, > { constructor( - private readonly viewerContext: TViewerContext, private readonly queryContext: EntityQueryContext, - private readonly privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext< + private readonly entityConfiguration: EntityConfiguration, + private readonly entityClass: IEntityClass< TFields, TID, TViewerContext, TEntity, + TPrivacyPolicy, TSelectedFields >, - private readonly entityConfiguration: EntityConfiguration, - private readonly entityClass: IEntityClass< + private readonly dataManager: EntityDataManager, + protected readonly metricsAdapter: IEntityMetricsAdapter, + private readonly utils: EntityLoaderUtils< TFields, TID, TViewerContext, @@ -60,10 +62,6 @@ export default class AuthorizationResultBasedEntityLoader< TPrivacyPolicy, TSelectedFields >, - private readonly entitySelectedFields: TSelectedFields[] | undefined, - private readonly privacyPolicy: TPrivacyPolicy, - private readonly dataManager: EntityDataManager, - protected readonly metricsAdapter: IEntityMetricsAdapter, ) {} /** @@ -85,7 +83,7 @@ export default class AuthorizationResultBasedEntityLoader< fieldValues, ); - return await this.constructAndAuthorizeEntitiesAsync(fieldValuesToFieldObjects); + return await this.utils.constructAndAuthorizeEntitiesAsync(fieldValuesToFieldObjects); } /** @@ -243,7 +241,7 @@ export default class AuthorizationResultBasedEntityLoader< fieldEqualityOperands, querySelectionModifiers, ); - return await this.constructAndAuthorizeEntitiesArrayAsync(fieldObjects); + return await this.utils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects); } /** @@ -280,87 +278,7 @@ export default class AuthorizationResultBasedEntityLoader< bindings, querySelectionModifiers, ); - return await this.constructAndAuthorizeEntitiesArrayAsync(fieldObjects); - } - - /** - * Invalidate all caches for an entity's fields. Exposed primarily for internal use by EntityMutator. - * @param objectFields - entity data object to be invalidated - */ - async invalidateFieldsAsync(objectFields: Readonly): Promise { - await this.dataManager.invalidateObjectFieldsAsync(objectFields); - } - - /** - * Invalidate all caches for an entity. One potential use case would be to keep the entity - * framework in sync with changes made to data outside of the framework. - * @param entity - entity to be invalidated - */ - async invalidateEntityAsync(entity: TEntity): Promise { - await this.invalidateFieldsAsync(entity.getAllDatabaseFields()); - } - - private tryConstructEntities(fieldsObjects: readonly TFields[]): readonly Result[] { - return fieldsObjects.map((fieldsObject) => { - try { - return result(this.constructEntity(fieldsObject)); - } catch (e) { - if (!(e instanceof Error)) { - throw e; - } - return result(e); - } - }); - } - - public constructEntity(fieldsObject: TFields): TEntity { - const idField = this.entityConfiguration.idField; - const id = nullthrows(fieldsObject[idField], 'must provide ID to create an entity'); - const entitySelectedFields = - this.entitySelectedFields ?? Array.from(this.entityConfiguration.schema.keys()); - const selectedFields = pick(fieldsObject, entitySelectedFields); - return new this.entityClass({ - viewerContext: this.viewerContext, - id: id as TID, - databaseFields: fieldsObject, - selectedFields, - }); - } - - /** - * 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( - map: ReadonlyMap[]>, - ): Promise[]>> { - return await mapMapAsync(map, async (fieldObjects) => { - return await this.constructAndAuthorizeEntitiesArrayAsync(fieldObjects); - }); - } - - private async constructAndAuthorizeEntitiesArrayAsync( - fieldObjects: readonly Readonly[], - ): Promise[]> { - 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 await this.utils.constructAndAuthorizeEntitiesArrayAsync(fieldObjects); } private validateFieldValues>( diff --git a/packages/entity/src/EntityLoader.ts b/packages/entity/src/EntityLoader.ts index 636af2f3..157b9c31 100644 --- a/packages/entity/src/EntityLoader.ts +++ b/packages/entity/src/EntityLoader.ts @@ -2,6 +2,7 @@ import AuthorizationResultBasedEntityLoader from './AuthorizationResultBasedEnti import EnforcingEntityLoader from './EnforcingEntityLoader'; import { IEntityClass } from './Entity'; import EntityConfiguration from './EntityConfiguration'; +import EntityLoaderUtils from './EntityLoaderUtils'; import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy'; import { EntityQueryContext } from './EntityQueryContext'; import ReadonlyEntity from './ReadonlyEntity'; @@ -27,6 +28,15 @@ export default class EntityLoader< >, TSelectedFields extends keyof TFields, > { + private readonly utilsPrivate: EntityLoaderUtils< + TFields, + TID, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + >; + constructor( private readonly viewerContext: TViewerContext, private readonly queryContext: EntityQueryContext, @@ -50,7 +60,19 @@ export default class EntityLoader< private readonly privacyPolicy: TPrivacyPolicy, private readonly dataManager: EntityDataManager, protected readonly metricsAdapter: IEntityMetricsAdapter, - ) {} + ) { + this.utilsPrivate = new EntityLoaderUtils( + this.viewerContext, + this.queryContext, + this.privacyPolicyEvaluationContext, + this.entityConfiguration, + this.entityClass, + this.entitySelectedFields, + this.privacyPolicy, + this.dataManager, + this.metricsAdapter, + ); + } /** * Enforcing entity loader. All loads through this loader are @@ -82,15 +104,27 @@ export default class EntityLoader< TSelectedFields > { return new AuthorizationResultBasedEntityLoader( - this.viewerContext, this.queryContext, - this.privacyPolicyEvaluationContext, this.entityConfiguration, this.entityClass, - this.entitySelectedFields, - this.privacyPolicy, this.dataManager, this.metricsAdapter, + this.utilsPrivate, ); } + + /** + * Entity loader utilities for things like cache invalidation, entity construction, and authorization. + * Calling into these should only be necessary in rare cases. + */ + public utils(): EntityLoaderUtils< + TFields, + TID, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + > { + return this.utilsPrivate; + } } diff --git a/packages/entity/src/EntityLoaderUtils.ts b/packages/entity/src/EntityLoaderUtils.ts new file mode 100644 index 00000000..10a55310 --- /dev/null +++ b/packages/entity/src/EntityLoaderUtils.ts @@ -0,0 +1,149 @@ +import { Result, asyncResult, result } from '@expo/results'; +import nullthrows from 'nullthrows'; + +import { IEntityClass } from './Entity'; +import EntityConfiguration from './EntityConfiguration'; +import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy'; +import { EntityQueryContext } from './EntityQueryContext'; +import ReadonlyEntity from './ReadonlyEntity'; +import ViewerContext from './ViewerContext'; +import { pick } from './entityUtils'; +import EntityDataManager from './internal/EntityDataManager'; +import IEntityMetricsAdapter from './metrics/IEntityMetricsAdapter'; +import { mapMapAsync } from './utils/collections/maps'; + +/** + * Entity loader utilities for things like invalidation, entity construction, and authorization. + * Methods are exposed publicly since in rare cases they may need to be called manually. + */ +export default class EntityLoaderUtils< + TFields extends object, + TID extends NonNullable, + TViewerContext extends ViewerContext, + TEntity extends ReadonlyEntity, + 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< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, + private readonly entityConfiguration: EntityConfiguration, + private readonly entityClass: IEntityClass< + TFields, + TID, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + >, + private readonly entitySelectedFields: TSelectedFields[] | undefined, + private readonly privacyPolicy: TPrivacyPolicy, + private readonly dataManager: EntityDataManager, + protected readonly metricsAdapter: IEntityMetricsAdapter, + ) {} + + /** + * Invalidate all caches for an entity's fields. Exposed primarily for internal use by EntityMutator. + * @param objectFields - entity data object to be invalidated + */ + async invalidateFieldsAsync(objectFields: Readonly): Promise { + await this.dataManager.invalidateObjectFieldsAsync(objectFields); + } + + /** + * Invalidate all caches for an entity. One potential use case would be to keep the entity + * framework in sync with changes made to data outside of the framework. + * @param entity - entity to be invalidated + */ + async invalidateEntityAsync(entity: TEntity): Promise { + await this.invalidateFieldsAsync(entity.getAllDatabaseFields()); + } + + /** + * Construct an entity from a fields object (applying field selection if applicable), + * checking that the ID field is specified. + * + * @param fieldsObject - fields object + */ + public constructEntity(fieldsObject: TFields): TEntity { + const idField = this.entityConfiguration.idField; + const id = nullthrows(fieldsObject[idField], 'must provide ID to create an entity'); + const entitySelectedFields = + this.entitySelectedFields ?? Array.from(this.entityConfiguration.schema.keys()); + const selectedFields = pick(fieldsObject, entitySelectedFields); + return new this.entityClass({ + viewerContext: this.viewerContext, + id: id as TID, + databaseFields: fieldsObject, + selectedFields, + }); + } + + /** + * 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( + map: ReadonlyMap[]>, + ): Promise[]>> { + return await mapMapAsync(map, async (fieldObjects) => { + return await this.constructAndAuthorizeEntitiesArrayAsync(fieldObjects); + }); + } + + /** + * Construct and authorize entities from field objects array, returning error results for entities that fail + * to construct or fail to authorize. + * + * @param fieldObjects - array of field objects + */ + public async constructAndAuthorizeEntitiesArrayAsync( + fieldObjects: readonly Readonly[], + ): Promise[]> { + 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[] { + return fieldsObjects.map((fieldsObject) => { + try { + return result(this.constructEntity(fieldsObject)); + } catch (e) { + if (!(e instanceof Error)) { + throw e; + } + return result(e); + } + }); + } +} diff --git a/packages/entity/src/EntityMutator.ts b/packages/entity/src/EntityMutator.ts index 87659550..463068d5 100644 --- a/packages/entity/src/EntityMutator.ts +++ b/packages/entity/src/EntityMutator.ts @@ -216,7 +216,7 @@ export class CreateMutator< cascadingDeleteCause: null, }); - const temporaryEntityForPrivacyCheck = entityLoader.withAuthorizationResults().constructEntity({ + const temporaryEntityForPrivacyCheck = entityLoader.utils().constructEntity({ [this.entityConfiguration.idField]: '00000000-0000-0000-0000-000000000000', // zero UUID ...this.fieldsForEntity, } as unknown as TFields); @@ -256,14 +256,10 @@ export class CreateMutator< const insertResult = await this.databaseAdapter.insertAsync(queryContext, this.fieldsForEntity); queryContext.appendPostCommitInvalidationCallback( - entityLoader - .withAuthorizationResults() - .invalidateFieldsAsync.bind(entityLoader, insertResult), + entityLoader.utils().invalidateFieldsAsync.bind(entityLoader, insertResult), ); - const unauthorizedEntityAfterInsert = entityLoader - .withAuthorizationResults() - .constructEntity(insertResult); + const unauthorizedEntityAfterInsert = entityLoader.utils().constructEntity(insertResult); const newEntity = await entityLoader .enforcing() .loadByIDAsync(unauthorizedEntityAfterInsert.getID()); @@ -433,9 +429,7 @@ export class UpdateMutator< cascadingDeleteCause, }); - const entityAboutToBeUpdated = entityLoader - .withAuthorizationResults() - .constructEntity(this.fieldsForEntity); + const entityAboutToBeUpdated = entityLoader.utils().constructEntity(this.fieldsForEntity); const authorizeUpdateResult = await asyncResult( this.privacyPolicy.authorizeUpdateAsync( this.viewerContext, @@ -480,13 +474,11 @@ export class UpdateMutator< queryContext.appendPostCommitInvalidationCallback( entityLoader - .withAuthorizationResults() + .utils() .invalidateFieldsAsync.bind(entityLoader, this.originalEntity.getAllDatabaseFields()), ); queryContext.appendPostCommitInvalidationCallback( - entityLoader - .withAuthorizationResults() - .invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity), + entityLoader.utils().invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity), ); const updatedEntity = await entityLoader @@ -690,7 +682,7 @@ export class DeleteMutator< }); queryContext.appendPostCommitInvalidationCallback( entityLoader - .withAuthorizationResults() + .utils() .invalidateFieldsAsync.bind(entityLoader, this.entity.getAllDatabaseFields()), ); diff --git a/packages/entity/src/EntitySecondaryCacheLoader.ts b/packages/entity/src/EntitySecondaryCacheLoader.ts index d3caf43b..fef6a4f9 100644 --- a/packages/entity/src/EntitySecondaryCacheLoader.ts +++ b/packages/entity/src/EntitySecondaryCacheLoader.ts @@ -85,7 +85,7 @@ export default abstract class EntitySecondaryCacheLoader< // convert value to and from array to reuse complex code const entitiesMap = await this.entityLoader - .withAuthorizationResults() + .utils() .constructAndAuthorizeEntitiesAsync( mapMap(loadParamsToFieldObjects, (fieldObject) => (fieldObject ? [fieldObject] : [])), ); diff --git a/packages/entity/src/__tests__/EnforcingEntityLoader-test.ts b/packages/entity/src/__tests__/EnforcingEntityLoader-test.ts index c83c2eb9..9bf4b4ae 100644 --- a/packages/entity/src/__tests__/EnforcingEntityLoader-test.ts +++ b/packages/entity/src/__tests__/EnforcingEntityLoader-test.ts @@ -449,15 +449,7 @@ describe(EnforcingEntityLoader, () => { ); // ensure known differences still exist for sanity check - const knownLoaderOnlyDifferences = [ - 'invalidateFieldsAsync', - 'invalidateEntityAsync', - 'tryConstructEntities', - 'validateFieldValues', - 'constructAndAuthorizeEntitiesAsync', - 'constructAndAuthorizeEntitiesArrayAsync', - 'constructEntity', - ]; + const knownLoaderOnlyDifferences = ['validateFieldValues']; expect(nonEnforcingLoaderProperties).toEqual( expect.arrayContaining(knownLoaderOnlyDifferences), ); diff --git a/packages/entity/src/__tests__/EntityLoader-test.ts b/packages/entity/src/__tests__/EntityLoader-test.ts index f3ebd2c0..7aeeefff 100644 --- a/packages/entity/src/__tests__/EntityLoader-test.ts +++ b/packages/entity/src/__tests__/EntityLoader-test.ts @@ -500,9 +500,7 @@ describe(EntityLoader, () => { dataManagerInstance, metricsAdapter, ); - await entityLoader - .withAuthorizationResults() - .invalidateFieldsAsync({ customIdField: id1 } as any); + await entityLoader.utils().invalidateFieldsAsync({ customIdField: id1 } as any); verify( dataManagerMock.invalidateObjectFieldsAsync(deepEqual({ customIdField: id1 } as any)), @@ -533,9 +531,7 @@ describe(EntityLoader, () => { dataManagerInstance, metricsAdapter, ); - await entityLoader - .withAuthorizationResults() - .invalidateFieldsAsync({ customIdField: id1 } as any); + await entityLoader.utils().invalidateFieldsAsync({ customIdField: id1 } as any); verify( dataManagerMock.invalidateObjectFieldsAsync(deepEqual({ customIdField: id1 } as any)), ).once(); @@ -569,7 +565,7 @@ describe(EntityLoader, () => { dataManagerInstance, metricsAdapter, ); - await entityLoader.withAuthorizationResults().invalidateEntityAsync(entityInstance); + await entityLoader.utils().invalidateEntityAsync(entityInstance); verify( dataManagerMock.invalidateObjectFieldsAsync(deepEqual({ customIdField: id1 } as any)), ).once(); diff --git a/packages/entity/src/__tests__/EntityMutator-test.ts b/packages/entity/src/__tests__/EntityMutator-test.ts index 2c070d08..fb971bd5 100644 --- a/packages/entity/src/__tests__/EntityMutator-test.ts +++ b/packages/entity/src/__tests__/EntityMutator-test.ts @@ -12,12 +12,12 @@ import { } from 'ts-mockito'; import { v4 as uuidv4 } from 'uuid'; -import AuthorizationResultBasedEntityLoader from '../AuthorizationResultBasedEntityLoader'; import EntityCompanionProvider from '../EntityCompanionProvider'; import EntityConfiguration from '../EntityConfiguration'; import EntityDatabaseAdapter from '../EntityDatabaseAdapter'; import EntityLoader from '../EntityLoader'; import EntityLoaderFactory from '../EntityLoaderFactory'; +import EntityLoaderUtils from '../EntityLoaderUtils'; import { EntityMutationType, EntityTriggerMutationInfo, @@ -1153,20 +1153,19 @@ describe(EntityMutatorFactory, () => { keyof SimpleTestFields > >(EntityLoader); - const nonEnforcingEntityLoaderMock = mock< - AuthorizationResultBasedEntityLoader< - SimpleTestFields, - string, - ViewerContext, - SimpleTestEntity, - SimpleTestEntityPrivacyPolicy, - keyof SimpleTestFields - > - >(AuthorizationResultBasedEntityLoader); - when(nonEnforcingEntityLoaderMock.constructEntity(anything())).thenReturn(fakeEntity); - when(entityLoaderMock.withAuthorizationResults()).thenReturn( - instance(nonEnforcingEntityLoaderMock), - ); + const entityLoaderUtilsMock = + mock< + EntityLoaderUtils< + SimpleTestFields, + string, + ViewerContext, + SimpleTestEntity, + SimpleTestEntityPrivacyPolicy, + keyof SimpleTestFields + > + >(EntityLoaderUtils); + when(entityLoaderUtilsMock.constructEntity(anything())).thenReturn(fakeEntity); + when(entityLoaderMock.utils()).thenReturn(instance(entityLoaderUtilsMock)); const entityLoader = instance(entityLoaderMock); const entityLoaderFactoryMock = @@ -1290,20 +1289,19 @@ describe(EntityMutatorFactory, () => { keyof SimpleTestFields > >(EntityLoader); - const nonEnforcingEntityLoaderMock = mock< - AuthorizationResultBasedEntityLoader< - SimpleTestFields, - string, - ViewerContext, - SimpleTestEntity, - SimpleTestEntityPrivacyPolicy, - keyof SimpleTestFields - > - >(AuthorizationResultBasedEntityLoader); - when(nonEnforcingEntityLoaderMock.constructEntity(anything())).thenReturn(fakeEntity); - when(entityLoaderMock.withAuthorizationResults()).thenReturn( - instance(nonEnforcingEntityLoaderMock), - ); + const entityLoaderUtilsMock = + mock< + EntityLoaderUtils< + SimpleTestFields, + string, + ViewerContext, + SimpleTestEntity, + SimpleTestEntityPrivacyPolicy, + keyof SimpleTestFields + > + >(EntityLoaderUtils); + when(entityLoaderUtilsMock.constructEntity(anything())).thenReturn(fakeEntity); + when(entityLoaderMock.utils()).thenReturn(instance(entityLoaderUtilsMock)); const entityLoader = instance(entityLoaderMock); const entityLoaderFactoryMock =