diff --git a/packages/entity-secondary-cache-local-memory/src/__tests__/LocalMemorySecondaryEntityCache-test.ts b/packages/entity-secondary-cache-local-memory/src/__tests__/LocalMemorySecondaryEntityCache-test.ts index 0f7a72603..0f39776d3 100644 --- a/packages/entity-secondary-cache-local-memory/src/__tests__/LocalMemorySecondaryEntityCache-test.ts +++ b/packages/entity-secondary-cache-local-memory/src/__tests__/LocalMemorySecondaryEntityCache-test.ts @@ -1,9 +1,11 @@ import { AlwaysAllowPrivacyPolicyRule, + AuthorizationResultBasedEntityLoader, Entity, EntityCompanionDefinition, EntityCompanionProvider, EntityConfiguration, + EntityConstructionUtils, EntityPrivacyPolicy, EntitySecondaryCacheLoader, IEntityMetricsAdapter, @@ -161,6 +163,32 @@ class TestSecondaryLocalMemoryCacheLoader extends EntitySecondaryCacheLoader< > { public databaseLoadCount = 0; + constructor( + secondaryEntityCache: LocalMemorySecondaryEntityCache< + LocalMemoryTestEntityFields, + 'id', + TestLoadParams + >, + constructionUtils: EntityConstructionUtils< + LocalMemoryTestEntityFields, + 'id', + TestViewerContext, + LocalMemoryTestEntity, + LocalMemoryTestEntityPrivacyPolicy, + keyof LocalMemoryTestEntityFields + >, + private readonly entityLoader: AuthorizationResultBasedEntityLoader< + LocalMemoryTestEntityFields, + 'id', + TestViewerContext, + LocalMemoryTestEntity, + LocalMemoryTestEntityPrivacyPolicy, + keyof LocalMemoryTestEntityFields + >, + ) { + super(secondaryEntityCache, constructionUtils); + } + protected async fetchObjectsFromDatabaseAsync( loadParamsArray: readonly Readonly[], ): Promise, Readonly | null>> { @@ -193,6 +221,10 @@ describe(LocalMemorySecondaryEntityCache, () => { localMemoryTestEntityConfiguration, createTTLCache(), ), + EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass( + LocalMemoryTestEntity, + viewerContext, + ), LocalMemoryTestEntity.loaderWithAuthorizationResults(viewerContext), ); @@ -229,6 +261,10 @@ describe(LocalMemorySecondaryEntityCache, () => { localMemoryTestEntityConfiguration, createTTLCache(), ), + EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass( + LocalMemoryTestEntity, + viewerContext, + ), LocalMemoryTestEntity.loaderWithAuthorizationResults(viewerContext), ); diff --git a/packages/entity-secondary-cache-redis/src/__integration-tests__/RedisSecondaryEntityCache-integration-test.ts b/packages/entity-secondary-cache-redis/src/__integration-tests__/RedisSecondaryEntityCache-integration-test.ts index f7f1de006..65441ab2a 100644 --- a/packages/entity-secondary-cache-redis/src/__integration-tests__/RedisSecondaryEntityCache-integration-test.ts +++ b/packages/entity-secondary-cache-redis/src/__integration-tests__/RedisSecondaryEntityCache-integration-test.ts @@ -1,4 +1,10 @@ -import { EntitySecondaryCacheLoader, mapMapAsync, ViewerContext } from '@expo/entity'; +import { + AuthorizationResultBasedEntityLoader, + EntityConstructionUtils, + EntitySecondaryCacheLoader, + mapMapAsync, + ViewerContext, +} from '@expo/entity'; import { GenericRedisCacheContext, RedisCacheInvalidationStrategy, @@ -33,6 +39,28 @@ class TestSecondaryRedisCacheLoader extends EntitySecondaryCacheLoader< > { public databaseLoadCount = 0; + constructor( + secondaryEntityCache: RedisSecondaryEntityCache, + constructionUtils: EntityConstructionUtils< + RedisTestEntityFields, + 'id', + TestViewerContext, + RedisTestEntity, + RedisTestEntityPrivacyPolicy, + keyof RedisTestEntityFields + >, + private readonly entityLoader: AuthorizationResultBasedEntityLoader< + RedisTestEntityFields, + 'id', + TestViewerContext, + RedisTestEntity, + RedisTestEntityPrivacyPolicy, + keyof RedisTestEntityFields + >, + ) { + super(secondaryEntityCache, constructionUtils); + } + protected async fetchObjectsFromDatabaseAsync( loadParamsArray: readonly Readonly[], ): Promise, Readonly | null>> { @@ -93,6 +121,7 @@ describe(RedisSecondaryEntityCache, () => { genericRedisCacheContext, (loadParams) => `test-key-${loadParams.id}`, ), + EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(RedisTestEntity, viewerContext), RedisTestEntity.loaderWithAuthorizationResults(viewerContext), ); @@ -132,6 +161,7 @@ describe(RedisSecondaryEntityCache, () => { genericRedisCacheContext, (loadParams) => `test-key-${loadParams.id}`, ), + EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(RedisTestEntity, viewerContext), RedisTestEntity.loaderWithAuthorizationResults(viewerContext), ); diff --git a/packages/entity/src/AuthorizationResultBasedEntityLoader.ts b/packages/entity/src/AuthorizationResultBasedEntityLoader.ts index 4bf0bdf23..227e06098 100644 --- a/packages/entity/src/AuthorizationResultBasedEntityLoader.ts +++ b/packages/entity/src/AuthorizationResultBasedEntityLoader.ts @@ -9,7 +9,6 @@ import { EntityConfiguration, } from './EntityConfiguration'; import { EntityConstructionUtils } from './EntityConstructionUtils'; -import { EntityInvalidationUtils } from './EntityInvalidationUtils'; import { EntityPrivacyPolicy } from './EntityPrivacyPolicy'; import { EntityQueryContext } from './EntityQueryContext'; import { ReadonlyEntity } from './ReadonlyEntity'; @@ -19,7 +18,6 @@ import { CompositeFieldHolder, CompositeFieldValueHolder } from './internal/Comp import { CompositeFieldValueMap } from './internal/CompositeFieldValueMap'; import { EntityDataManager } from './internal/EntityDataManager'; import { SingleFieldHolder, SingleFieldValueHolder } from './internal/SingleFieldHolder'; -import { IEntityMetricsAdapter } from './metrics/IEntityMetricsAdapter'; import { mapKeys, mapMap } from './utils/collections/maps'; import { areSetsEqual } from './utils/collections/sets'; @@ -55,16 +53,7 @@ export class AuthorizationResultBasedEntityLoader< TSelectedFields >, private readonly dataManager: EntityDataManager, - protected readonly metricsAdapter: IEntityMetricsAdapter, - public readonly invalidationUtils: EntityInvalidationUtils< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - >, - public readonly constructionUtils: EntityConstructionUtils< + private readonly constructionUtils: EntityConstructionUtils< TFields, TIDField, TViewerContext, diff --git a/packages/entity/src/AuthorizationResultBasedEntityMutator.ts b/packages/entity/src/AuthorizationResultBasedEntityMutator.ts index 9190e056d..0924dad60 100644 --- a/packages/entity/src/AuthorizationResultBasedEntityMutator.ts +++ b/packages/entity/src/AuthorizationResultBasedEntityMutator.ts @@ -333,8 +333,17 @@ export class AuthorizationResultBasedCreateMutator< previousValue: null, cascadingDeleteCause: null, }); + const invalidationUtils = this.entityLoaderFactory.invalidationUtils(); + const constructionUtils = this.entityLoaderFactory.constructionUtils( + this.viewerContext, + queryContext, + { + previousValue: null, + cascadingDeleteCause: null, + }, + ); - const temporaryEntityForPrivacyCheck = entityLoader.constructionUtils.constructEntity({ + const temporaryEntityForPrivacyCheck = constructionUtils.constructEntity({ [this.entityConfiguration.idField]: '00000000-0000-0000-0000-000000000000', // zero UUID ...this.fieldsForEntity, } as unknown as TFields); @@ -376,14 +385,13 @@ export class AuthorizationResultBasedCreateMutator< // Invalidate all caches for the new entity so that any previously-negatively-cached loads // are removed from the caches. queryContext.appendPostCommitInvalidationCallback(async () => { - entityLoader.invalidationUtils.invalidateFieldsForTransaction(queryContext, insertResult); - await entityLoader.invalidationUtils.invalidateFieldsAsync(insertResult); + invalidationUtils.invalidateFieldsForTransaction(queryContext, insertResult); + await invalidationUtils.invalidateFieldsAsync(insertResult); }); - entityLoader.invalidationUtils.invalidateFieldsForTransaction(queryContext, insertResult); + invalidationUtils.invalidateFieldsForTransaction(queryContext, insertResult); - const unauthorizedEntityAfterInsert = - entityLoader.constructionUtils.constructEntity(insertResult); + const unauthorizedEntityAfterInsert = constructionUtils.constructEntity(insertResult); const newEntity = await enforceAsyncResult( entityLoader.loadByIDAsync(unauthorizedEntityAfterInsert.getID()), ); @@ -542,10 +550,17 @@ export class AuthorizationResultBasedUpdateMutator< previousValue: this.originalEntity, cascadingDeleteCause: this.cascadingDeleteCause, }); - - const entityAboutToBeUpdated = entityLoader.constructionUtils.constructEntity( - this.fieldsForEntity, + const invalidationUtils = this.entityLoaderFactory.invalidationUtils(); + const constructionUtils = this.entityLoaderFactory.constructionUtils( + this.viewerContext, + queryContext, + { + previousValue: this.originalEntity, + cascadingDeleteCause: this.cascadingDeleteCause, + }, ); + + const entityAboutToBeUpdated = constructionUtils.constructEntity(this.fieldsForEntity); const authorizeUpdateResult = await asyncResult( this.privacyPolicy.authorizeUpdateAsync( this.viewerContext, @@ -609,30 +624,22 @@ export class AuthorizationResultBasedUpdateMutator< // version of the entity. queryContext.appendPostCommitInvalidationCallback(async () => { - entityLoader.invalidationUtils.invalidateFieldsForTransaction( + invalidationUtils.invalidateFieldsForTransaction( queryContext, this.originalEntity.getAllDatabaseFields(), ); - entityLoader.invalidationUtils.invalidateFieldsForTransaction( - queryContext, - this.fieldsForEntity, - ); + invalidationUtils.invalidateFieldsForTransaction(queryContext, this.fieldsForEntity); await Promise.all([ - entityLoader.invalidationUtils.invalidateFieldsAsync( - this.originalEntity.getAllDatabaseFields(), - ), - entityLoader.invalidationUtils.invalidateFieldsAsync(this.fieldsForEntity), + invalidationUtils.invalidateFieldsAsync(this.originalEntity.getAllDatabaseFields()), + invalidationUtils.invalidateFieldsAsync(this.fieldsForEntity), ]); }); - entityLoader.invalidationUtils.invalidateFieldsForTransaction( + invalidationUtils.invalidateFieldsForTransaction( queryContext, this.originalEntity.getAllDatabaseFields(), ); - entityLoader.invalidationUtils.invalidateFieldsForTransaction( - queryContext, - this.fieldsForEntity, - ); + invalidationUtils.invalidateFieldsForTransaction(queryContext, this.fieldsForEntity); const updatedEntity = await enforceAsyncResult( entityLoader.loadByIDAsync(entityAboutToBeUpdated.getID()), @@ -850,23 +857,18 @@ export class AuthorizationResultBasedDeleteMutator< ); } - const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext, { - previousValue: null, - cascadingDeleteCause: this.cascadingDeleteCause, - }); + const invalidationUtils = this.entityLoaderFactory.invalidationUtils(); // Invalidate all caches for the entity so that any previously-cached loads // are removed from the caches. queryContext.appendPostCommitInvalidationCallback(async () => { - entityLoader.invalidationUtils.invalidateFieldsForTransaction( + invalidationUtils.invalidateFieldsForTransaction( queryContext, this.entity.getAllDatabaseFields(), ); - await entityLoader.invalidationUtils.invalidateFieldsAsync( - this.entity.getAllDatabaseFields(), - ); + await invalidationUtils.invalidateFieldsAsync(this.entity.getAllDatabaseFields()); }); - entityLoader.invalidationUtils.invalidateFieldsForTransaction( + invalidationUtils.invalidateFieldsForTransaction( queryContext, this.entity.getAllDatabaseFields(), ); diff --git a/packages/entity/src/EntityLoader.ts b/packages/entity/src/EntityLoader.ts index 51c32ccbc..532f1f144 100644 --- a/packages/entity/src/EntityLoader.ts +++ b/packages/entity/src/EntityLoader.ts @@ -1,8 +1,6 @@ import { AuthorizationResultBasedEntityLoader } from './AuthorizationResultBasedEntityLoader'; import { EnforcingEntityLoader } from './EnforcingEntityLoader'; import { IEntityClass } from './Entity'; -import { EntityConstructionUtils } from './EntityConstructionUtils'; -import { EntityInvalidationUtils } from './EntityInvalidationUtils'; import { EntityPrivacyPolicy } from './EntityPrivacyPolicy'; import { EntityQueryContext } from './EntityQueryContext'; import { ReadonlyEntity } from './ReadonlyEntity'; @@ -74,34 +72,4 @@ export class EntityLoader< .getLoaderFactory() .forLoad(this.queryContext, { previousValue: null, cascadingDeleteCause: null }); } - - /** - * Entity cache invalidation utilities. - * Calling into these should only be necessary in rare cases. - */ - public invalidationUtils(): EntityInvalidationUtils< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - > { - return this.withAuthorizationResults().invalidationUtils; - } - - /** - * Entity construction and validation utilities. - * Calling into these should only be necessary in rare cases. - */ - public constructionUtils(): EntityConstructionUtils< - TFields, - TIDField, - TViewerContext, - TEntity, - TPrivacyPolicy, - TSelectedFields - > { - return this.withAuthorizationResults().constructionUtils; - } } diff --git a/packages/entity/src/EntityLoaderFactory.ts b/packages/entity/src/EntityLoaderFactory.ts index c6f931868..f759c97f3 100644 --- a/packages/entity/src/EntityLoaderFactory.ts +++ b/packages/entity/src/EntityLoaderFactory.ts @@ -39,6 +39,52 @@ export class EntityLoaderFactory< protected readonly metricsAdapter: IEntityMetricsAdapter, ) {} + invalidationUtils(): EntityInvalidationUtils< + TFields, + TIDField, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + > { + return new EntityInvalidationUtils( + this.entityCompanion.entityCompanionDefinition.entityConfiguration, + this.entityCompanion.entityCompanionDefinition.entityClass, + this.dataManager, + this.metricsAdapter, + ); + } + + constructionUtils( + viewerContext: TViewerContext, + queryContext: EntityQueryContext, + privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TIDField, + TViewerContext, + TEntity, + TSelectedFields + >, + ): EntityConstructionUtils< + TFields, + TIDField, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + > { + return new EntityConstructionUtils( + viewerContext, + queryContext, + privacyPolicyEvaluationContext, + this.entityCompanion.entityCompanionDefinition.entityConfiguration, + this.entityCompanion.entityCompanionDefinition.entityClass, + this.entityCompanion.entityCompanionDefinition.entitySelectedFields, + this.entityCompanion.privacyPolicy, + this.metricsAdapter, + ); + } + /** * Vend loader for loading an entity in a given query context. * @param viewerContext - viewer context of loading user @@ -62,21 +108,10 @@ export class EntityLoaderFactory< TPrivacyPolicy, TSelectedFields > { - const invalidationUtils = new EntityInvalidationUtils( - this.entityCompanion.entityCompanionDefinition.entityConfiguration, - this.entityCompanion.entityCompanionDefinition.entityClass, - this.dataManager, - this.metricsAdapter, - ); - const constructionUtils = new EntityConstructionUtils( + const constructionUtils = this.constructionUtils( viewerContext, queryContext, privacyPolicyEvaluationContext, - this.entityCompanion.entityCompanionDefinition.entityConfiguration, - this.entityCompanion.entityCompanionDefinition.entityClass, - this.entityCompanion.entityCompanionDefinition.entitySelectedFields, - this.entityCompanion.privacyPolicy, - this.metricsAdapter, ); return new AuthorizationResultBasedEntityLoader( @@ -84,8 +119,6 @@ export class EntityLoaderFactory< this.entityCompanion.entityCompanionDefinition.entityConfiguration, this.entityCompanion.entityCompanionDefinition.entityClass, this.dataManager, - this.metricsAdapter, - invalidationUtils, constructionUtils, ); } diff --git a/packages/entity/src/EntitySecondaryCacheLoader.ts b/packages/entity/src/EntitySecondaryCacheLoader.ts index f8bb30b5d..5a801cd87 100644 --- a/packages/entity/src/EntitySecondaryCacheLoader.ts +++ b/packages/entity/src/EntitySecondaryCacheLoader.ts @@ -1,7 +1,9 @@ import { Result } from '@expo/results'; -import { AuthorizationResultBasedEntityLoader } from './AuthorizationResultBasedEntityLoader'; +import { IEntityClass } from './Entity'; +import { EntityConstructionUtils } from './EntityConstructionUtils'; import { EntityPrivacyPolicy } from './EntityPrivacyPolicy'; +import { EntityQueryContext } from './EntityQueryContext'; import { ReadonlyEntity } from './ReadonlyEntity'; import { ViewerContext } from './ViewerContext'; import { mapMap } from './utils/collections/maps'; @@ -60,7 +62,7 @@ export abstract class EntitySecondaryCacheLoader< > { constructor( private readonly secondaryEntityCache: ISecondaryEntityCache, - protected readonly entityLoader: AuthorizationResultBasedEntityLoader< + private readonly constructionUtils: EntityConstructionUtils< TFields, TIDField, TViewerContext, @@ -84,10 +86,9 @@ export abstract class EntitySecondaryCacheLoader< ); // convert value to and from array to reuse complex code - const entitiesMap = - await this.entityLoader.constructionUtils.constructAndAuthorizeEntitiesAsync( - mapMap(loadParamsToFieldObjects, (fieldObject) => (fieldObject ? [fieldObject] : [])), - ); + const entitiesMap = await this.constructionUtils.constructAndAuthorizeEntitiesAsync( + mapMap(loadParamsToFieldObjects, (fieldObject) => (fieldObject ? [fieldObject] : [])), + ); return mapMap(entitiesMap, (fieldObjects) => fieldObjects[0] ?? null); } @@ -110,4 +111,53 @@ export abstract class EntitySecondaryCacheLoader< protected abstract fetchObjectsFromDatabaseAsync( loadParamsArray: readonly Readonly[], ): Promise>, Readonly | null>>; + + /** + * Helper to get construction utils for instantiating a EntitySecondaryCacheLoader. + * + * @param entityClass - the entity class for which to get construction utils for + * @param viewerContext - the viewer context to use for construction utils + * @param queryContext - query context to use for construction utils + * @returns construction utils for the given entity class and viewer context + */ + public static getConstructionUtilsForEntityClass< + TFields extends Record, + TIDField extends keyof NonNullable>, + TViewerContext extends ViewerContext, + TEntity extends ReadonlyEntity, + TPrivacyPolicy extends EntityPrivacyPolicy< + TFields, + TIDField, + TViewerContext, + TEntity, + TSelectedFields + >, + TSelectedFields extends keyof TFields = keyof TFields, + >( + entityClass: IEntityClass< + TFields, + TIDField, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + >, + viewerContext: TViewerContext, + queryContext: EntityQueryContext = viewerContext + .getViewerScopedEntityCompanionForClass(entityClass) + .getQueryContextProvider() + .getQueryContext(), + ): EntityConstructionUtils< + TFields, + TIDField, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + > { + return viewerContext + .getViewerScopedEntityCompanionForClass(entityClass) + .getLoaderFactory() + .constructionUtils(queryContext, { previousValue: null, cascadingDeleteCause: null }); + } } diff --git a/packages/entity/src/ReadonlyEntity.ts b/packages/entity/src/ReadonlyEntity.ts index ad1cf5583..f955b74c3 100644 --- a/packages/entity/src/ReadonlyEntity.ts +++ b/packages/entity/src/ReadonlyEntity.ts @@ -6,7 +6,6 @@ import { EnforcingEntityAssociationLoader } from './EnforcingEntityAssociationLo import { EnforcingEntityLoader } from './EnforcingEntityLoader'; import { IEntityClass } from './Entity'; import { EntityAssociationLoader } from './EntityAssociationLoader'; -import { EntityConstructionUtils } from './EntityConstructionUtils'; import { EntityInvalidationUtils } from './EntityInvalidationUtils'; import { EntityLoader } from './EntityLoader'; import { EntityPrivacyPolicy } from './EntityPrivacyPolicy'; @@ -232,7 +231,7 @@ export abstract class ReadonlyEntity< /** * Utilities for entity invalidation. - * Calling into these should only be necessary in rare cases. + * Call these manually to keep entity cache consistent when performing operations outside of the entity framework. */ static invalidationUtils< TMFields extends object, @@ -258,10 +257,6 @@ export abstract class ReadonlyEntity< TMSelectedFields >, viewerContext: TMViewerContext2, - queryContext: EntityQueryContext = viewerContext - .getViewerScopedEntityCompanionForClass(this) - .getQueryContextProvider() - .getQueryContext(), ): EntityInvalidationUtils< TMFields, TMIDField, @@ -270,49 +265,9 @@ export abstract class ReadonlyEntity< TMPrivacyPolicy, TMSelectedFields > { - return new EntityLoader(viewerContext, queryContext, this).invalidationUtils(); - } - - /** - * Utilities for entity construction. - * Calling into these should only be necessary in rare cases. - */ - static constructionUtils< - TMFields extends object, - TMIDField extends keyof NonNullable>, - TMViewerContext extends ViewerContext, - TMViewerContext2 extends TMViewerContext, - TMEntity extends ReadonlyEntity, - TMPrivacyPolicy extends EntityPrivacyPolicy< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMSelectedFields - >, - TMSelectedFields extends keyof TMFields = keyof TMFields, - >( - this: IEntityClass< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - >, - viewerContext: TMViewerContext2, - queryContext: EntityQueryContext = viewerContext + return viewerContext .getViewerScopedEntityCompanionForClass(this) - .getQueryContextProvider() - .getQueryContext(), - ): EntityConstructionUtils< - TMFields, - TMIDField, - TMViewerContext, - TMEntity, - TMPrivacyPolicy, - TMSelectedFields - > { - return new EntityLoader(viewerContext, queryContext, this).constructionUtils(); + .getLoaderFactory() + .invalidationUtils(); } } diff --git a/packages/entity/src/ViewerScopedEntityLoaderFactory.ts b/packages/entity/src/ViewerScopedEntityLoaderFactory.ts index 38d2355ad..11def1388 100644 --- a/packages/entity/src/ViewerScopedEntityLoaderFactory.ts +++ b/packages/entity/src/ViewerScopedEntityLoaderFactory.ts @@ -1,4 +1,6 @@ import { AuthorizationResultBasedEntityLoader } from './AuthorizationResultBasedEntityLoader'; +import { EntityConstructionUtils } from './EntityConstructionUtils'; +import { EntityInvalidationUtils } from './EntityInvalidationUtils'; import { EntityLoaderFactory } from './EntityLoaderFactory'; import { EntityPrivacyPolicy, EntityPrivacyPolicyEvaluationContext } from './EntityPrivacyPolicy'; import { EntityQueryContext } from './EntityQueryContext'; @@ -34,6 +36,41 @@ export class ViewerScopedEntityLoaderFactory< private readonly viewerContext: TViewerContext, ) {} + invalidationUtils(): EntityInvalidationUtils< + TFields, + TIDField, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + > { + return this.entityLoaderFactory.invalidationUtils(); + } + + constructionUtils( + queryContext: EntityQueryContext, + privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext< + TFields, + TIDField, + TViewerContext, + TEntity, + TSelectedFields + >, + ): EntityConstructionUtils< + TFields, + TIDField, + TViewerContext, + TEntity, + TPrivacyPolicy, + TSelectedFields + > { + return this.entityLoaderFactory.constructionUtils( + this.viewerContext, + queryContext, + privacyPolicyEvaluationContext, + ); + } + forLoad( queryContext: EntityQueryContext, privacyPolicyEvaluationContext: EntityPrivacyPolicyEvaluationContext< diff --git a/packages/entity/src/__tests__/AuthorizationResultBasedEntityLoader-constructor-test.ts b/packages/entity/src/__tests__/AuthorizationResultBasedEntityLoader-constructor-test.ts index f5d3dfac2..d01fe8260 100644 --- a/packages/entity/src/__tests__/AuthorizationResultBasedEntityLoader-constructor-test.ts +++ b/packages/entity/src/__tests__/AuthorizationResultBasedEntityLoader-constructor-test.ts @@ -7,7 +7,6 @@ import { EntityCompanionDefinition } from '../EntityCompanionProvider'; import { EntityConfiguration } from '../EntityConfiguration'; import { EntityConstructionUtils } from '../EntityConstructionUtils'; import { StringField } from '../EntityFields'; -import { EntityInvalidationUtils } from '../EntityInvalidationUtils'; import { EntityPrivacyPolicy, EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy'; import { ViewerContext } from '../ViewerContext'; import { EntityDataManager } from '../internal/EntityDataManager'; @@ -165,12 +164,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { metricsAdapter, TestEntity.name, ); - const invalidationUtils = new EntityInvalidationUtils( - testEntityConfiguration, - TestEntity, - dataManager, - metricsAdapter, - ); const constructionUtils = new EntityConstructionUtils( viewerContext, queryContext, @@ -186,8 +179,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { testEntityConfiguration, TestEntity, dataManager, - metricsAdapter, - invalidationUtils, constructionUtils, ); diff --git a/packages/entity/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts b/packages/entity/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts index 4eff36633..b79dbdd64 100644 --- a/packages/entity/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts +++ b/packages/entity/src/__tests__/AuthorizationResultBasedEntityLoader-test.ts @@ -90,12 +90,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { instance(mock()), TestEntity.name, ); - const invalidationUtils = new EntityInvalidationUtils( - testEntityConfiguration, - TestEntity, - dataManager, - metricsAdapter, - ); const constructionUtils = new EntityConstructionUtils( viewerContext, queryContext, @@ -111,8 +105,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { testEntityConfiguration, TestEntity, dataManager, - metricsAdapter, - invalidationUtils, constructionUtils, ); @@ -240,12 +232,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { instance(mock()), TestEntity.name, ); - const invalidationUtils = new EntityInvalidationUtils( - testEntityConfiguration, - TestEntity, - dataManager, - metricsAdapter, - ); const constructionUtils = new EntityConstructionUtils( viewerContext, queryContext, @@ -261,8 +247,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { testEntityConfiguration, TestEntity, dataManager, - metricsAdapter, - invalidationUtils, constructionUtils, ); @@ -385,12 +369,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { instance(mock()), TestEntity.name, ); - const invalidationUtils = new EntityInvalidationUtils( - testEntityConfiguration, - TestEntity, - dataManager, - metricsAdapter, - ); const constructionUtils = new EntityConstructionUtils( viewerContext, queryContext, @@ -406,8 +384,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { testEntityConfiguration, TestEntity, dataManager, - metricsAdapter, - invalidationUtils, constructionUtils, ); const entity = await enforceAsyncResult(entityLoader.loadByIDAsync(id1)); @@ -423,21 +399,7 @@ describe(AuthorizationResultBasedEntityLoader, () => { }); it('invalidates upon invalidate one', async () => { - const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = - instance( - mock< - EntityPrivacyPolicyEvaluationContext< - TestFields, - 'customIdField', - ViewerContext, - TestEntity - > - >(), - ); const metricsAdapter = instance(mock()); - const queryContext = new StubQueryContextProvider().getQueryContext(); - const privacyPolicy = instance(mock(TestEntityPrivacyPolicy)); const dataManagerMock = mock>(); const dataManagerInstance = instance(dataManagerMock); @@ -448,29 +410,10 @@ describe(AuthorizationResultBasedEntityLoader, () => { dataManagerInstance, metricsAdapter, ); - const constructionUtils = new EntityConstructionUtils( - viewerContext, - queryContext, - privacyPolicyEvaluationContext, - testEntityConfiguration, - TestEntity, - /* entitySelectedFields */ undefined, - privacyPolicy, - metricsAdapter, - ); - const entityLoader = new AuthorizationResultBasedEntityLoader( - queryContext, - testEntityConfiguration, - TestEntity, - dataManagerInstance, - metricsAdapter, - invalidationUtils, - constructionUtils, - ); const date = new Date(); - await entityLoader.invalidationUtils.invalidateFieldsAsync({ + await invalidationUtils.invalidateFieldsAsync({ customIdField: id1, testIndexedField: 'h1', intField: 5, @@ -522,21 +465,7 @@ describe(AuthorizationResultBasedEntityLoader, () => { }); it('invalidates upon invalidate by entity', async () => { - const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = - instance( - mock< - EntityPrivacyPolicyEvaluationContext< - TestFields, - 'customIdField', - ViewerContext, - TestEntity - > - >(), - ); const metricsAdapter = instance(mock()); - const queryContext = new StubQueryContextProvider().getQueryContext(); - const privacyPolicy = instance(mock(TestEntityPrivacyPolicy)); const dataManagerMock = mock>(); const dataManagerInstance = instance(dataManagerMock); @@ -560,26 +489,8 @@ describe(AuthorizationResultBasedEntityLoader, () => { dataManagerInstance, metricsAdapter, ); - const constructionUtils = new EntityConstructionUtils( - viewerContext, - queryContext, - privacyPolicyEvaluationContext, - testEntityConfiguration, - TestEntity, - /* entitySelectedFields */ undefined, - privacyPolicy, - metricsAdapter, - ); - const entityLoader = new AuthorizationResultBasedEntityLoader( - queryContext, - testEntityConfiguration, - TestEntity, - dataManagerInstance, - metricsAdapter, - invalidationUtils, - constructionUtils, - ); - await entityLoader.invalidationUtils.invalidateEntityAsync(entityInstance); + + await invalidationUtils.invalidateEntityAsync(entityInstance); verify(dataManagerMock.invalidateKeyValuePairsAsync(anything())).once(); verify( @@ -624,21 +535,8 @@ describe(AuthorizationResultBasedEntityLoader, () => { }); it('invalidates upon invalidate by entity within transaction', async () => { - const viewerContext = instance(mock(ViewerContext)); - const privacyPolicyEvaluationContext = - instance( - mock< - EntityPrivacyPolicyEvaluationContext< - TestFields, - 'customIdField', - ViewerContext, - TestEntity - > - >(), - ); const metricsAdapter = instance(mock()); - const privacyPolicy = instance(mock(TestEntityPrivacyPolicy)); const dataManagerMock = mock>(); const dataManagerInstance = instance(dataManagerMock); @@ -663,26 +561,7 @@ describe(AuthorizationResultBasedEntityLoader, () => { dataManagerInstance, metricsAdapter, ); - const constructionUtils = new EntityConstructionUtils( - viewerContext, - queryContext, - privacyPolicyEvaluationContext, - testEntityConfiguration, - TestEntity, - /* entitySelectedFields */ undefined, - privacyPolicy, - metricsAdapter, - ); - const entityLoader = new AuthorizationResultBasedEntityLoader( - queryContext, - testEntityConfiguration, - TestEntity, - dataManagerInstance, - metricsAdapter, - invalidationUtils, - constructionUtils, - ); - entityLoader.invalidationUtils.invalidateEntityForTransaction(queryContext, entityInstance); + invalidationUtils.invalidateEntityForTransaction(queryContext, entityInstance); verify( dataManagerMock.invalidateKeyValuePairsForTransaction(queryContext, anything()), @@ -781,12 +660,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { const privacyPolicy = instance(privacyPolicyMock); const dataManagerInstance = instance(dataManagerMock); - const invalidationUtils = new EntityInvalidationUtils( - testEntityConfiguration, - TestEntity, - dataManagerInstance, - metricsAdapter, - ); const constructionUtils = new EntityConstructionUtils( viewerContext, queryContext, @@ -802,8 +675,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { testEntityConfiguration, TestEntity, dataManagerInstance, - metricsAdapter, - invalidationUtils, constructionUtils, ); @@ -839,12 +710,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { const dataManagerInstance = instance(dataManagerMock); - const invalidationUtils = new EntityInvalidationUtils( - testEntityConfiguration, - TestEntity, - dataManagerInstance, - metricsAdapter, - ); const constructionUtils = new EntityConstructionUtils( viewerContext, queryContext, @@ -860,8 +725,6 @@ describe(AuthorizationResultBasedEntityLoader, () => { testEntityConfiguration, TestEntity, dataManagerInstance, - metricsAdapter, - invalidationUtils, constructionUtils, ); diff --git a/packages/entity/src/__tests__/EntityLoader-test.ts b/packages/entity/src/__tests__/EntityLoader-test.ts index 373802905..6b02c674e 100644 --- a/packages/entity/src/__tests__/EntityLoader-test.ts +++ b/packages/entity/src/__tests__/EntityLoader-test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from '@jest/globals'; import { AuthorizationResultBasedEntityLoader } from '../AuthorizationResultBasedEntityLoader'; import { EnforcingEntityLoader } from '../EnforcingEntityLoader'; -import { EntityConstructionUtils } from '../EntityConstructionUtils'; import { EntityInvalidationUtils } from '../EntityInvalidationUtils'; import { EntityLoader } from '../EntityLoader'; import { ViewerContext } from '../ViewerContext'; @@ -37,14 +36,4 @@ describe(EntityLoader, () => { ); }); }); - - describe('constructionUtils', () => { - it('returns a instance of EntityConstructionUtils', async () => { - const companionProvider = createUnitTestEntityCompanionProvider(); - const viewerContext = new ViewerContext(companionProvider); - expect(SimpleTestEntity.constructionUtils(viewerContext)).toBeInstanceOf( - EntityConstructionUtils, - ); - }); - }); }); diff --git a/packages/entity/src/__tests__/EntityMutator-test.ts b/packages/entity/src/__tests__/EntityMutator-test.ts index 81c8bc176..9edcf5a48 100644 --- a/packages/entity/src/__tests__/EntityMutator-test.ts +++ b/packages/entity/src/__tests__/EntityMutator-test.ts @@ -1511,7 +1511,6 @@ describe(EntityMutatorFactory, () => { > >(EntityConstructionUtils); when(entityConstructionUtilsMock.constructEntity(anything())).thenReturn(fakeEntity); - when(entityLoaderMock.constructionUtils).thenReturn(instance(entityConstructionUtilsMock)); const entityLoader = instance(entityLoaderMock); const entityLoaderFactoryMock = @@ -1532,6 +1531,13 @@ describe(EntityMutatorFactory, () => { anything(), ), ).thenReturn(entityLoader); + when( + entityLoaderFactoryMock.constructionUtils( + viewerContext, + anyOfClass(EntityTransactionalQueryContext), + anything(), + ), + ).thenReturn(instance(entityConstructionUtilsMock)); const entityLoaderFactory = instance(entityLoaderFactoryMock); const rejectionError = new Error(); @@ -1646,7 +1652,6 @@ describe(EntityMutatorFactory, () => { > >(EntityConstructionUtils); when(entityConstructionUtilsMock.constructEntity(anything())).thenReturn(fakeEntity); - when(entityLoaderMock.constructionUtils).thenReturn(instance(entityConstructionUtilsMock)); const entityLoader = instance(entityLoaderMock); const entityLoaderFactoryMock = @@ -1667,6 +1672,13 @@ describe(EntityMutatorFactory, () => { anything(), ), ).thenReturn(entityLoader); + when( + entityLoaderFactoryMock.constructionUtils( + viewerContext, + anyOfClass(EntityTransactionalQueryContext), + anything(), + ), + ).thenReturn(instance(entityConstructionUtilsMock)); const entityLoaderFactory = instance(entityLoaderFactoryMock); const rejectionError = new Error(); diff --git a/packages/entity/src/__tests__/EntitySecondaryCacheLoader-test.ts b/packages/entity/src/__tests__/EntitySecondaryCacheLoader-test.ts index b8998199a..df149fc76 100644 --- a/packages/entity/src/__tests__/EntitySecondaryCacheLoader-test.ts +++ b/packages/entity/src/__tests__/EntitySecondaryCacheLoader-test.ts @@ -46,7 +46,7 @@ describe(EntitySecondaryCacheLoader, () => { const secondaryCacheLoader = new TestSecondaryRedisCacheLoader( secondaryEntityCache, - SimpleTestEntity.loaderWithAuthorizationResults(vc1), + EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(SimpleTestEntity, vc1), ); await secondaryCacheLoader.loadManyAsync([loadParams]); @@ -70,8 +70,11 @@ describe(EntitySecondaryCacheLoader, () => { const secondaryEntityCache = instance(secondaryEntityCacheMock); const loader = SimpleTestEntity.loaderWithAuthorizationResults(vc1); - const spiedPrivacyPolicy = spy(loader.constructionUtils['privacyPolicy']); - const secondaryCacheLoader = new TestSecondaryRedisCacheLoader(secondaryEntityCache, loader); + const spiedPrivacyPolicy = spy(loader['constructionUtils']['privacyPolicy']); + const secondaryCacheLoader = new TestSecondaryRedisCacheLoader( + secondaryEntityCache, + EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(SimpleTestEntity, vc1), + ); const result = await secondaryCacheLoader.loadManyAsync([loadParams]); expect(result.get(loadParams)?.enforceValue().getID()).toEqual(createdEntity.getID()); @@ -98,8 +101,10 @@ describe(EntitySecondaryCacheLoader, () => { const secondaryEntityCacheMock = mock>(); const secondaryEntityCache = instance(secondaryEntityCacheMock); - const loader = SimpleTestEntity.loaderWithAuthorizationResults(vc1); - const secondaryCacheLoader = new TestSecondaryRedisCacheLoader(secondaryEntityCache, loader); + const secondaryCacheLoader = new TestSecondaryRedisCacheLoader( + secondaryEntityCache, + EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(SimpleTestEntity, vc1), + ); await secondaryCacheLoader.invalidateManyAsync([loadParams]); verify(secondaryEntityCacheMock.invalidateManyAsync(deepEqual([loadParams]))).once(); diff --git a/packages/entity/src/__tests__/GenericSecondaryEntityCache-test.ts b/packages/entity/src/__tests__/GenericSecondaryEntityCache-test.ts index ebc9c993d..1ad6d9d40 100644 --- a/packages/entity/src/__tests__/GenericSecondaryEntityCache-test.ts +++ b/packages/entity/src/__tests__/GenericSecondaryEntityCache-test.ts @@ -1,6 +1,8 @@ import { describe, it, expect } from '@jest/globals'; import nullthrows from 'nullthrows'; +import { AuthorizationResultBasedEntityLoader } from '../AuthorizationResultBasedEntityLoader'; +import { EntityConstructionUtils } from '../EntityConstructionUtils'; import { EntitySecondaryCacheLoader } from '../EntitySecondaryCacheLoader'; import { GenericSecondaryEntityCache } from '../GenericSecondaryEntityCache'; import { IEntityGenericCacher } from '../IEntityGenericCacher'; @@ -100,6 +102,28 @@ class TestSecondaryCacheLoader extends EntitySecondaryCacheLoader< > { public databaseLoadCount = 0; + constructor( + secondaryEntityCache: TestSecondaryEntityCache, + constructionUtils: EntityConstructionUtils< + TestFields, + 'customIdField', + ViewerContext, + TestEntity, + TestEntityPrivacyPolicy, + keyof TestFields + >, + private readonly entityLoader: AuthorizationResultBasedEntityLoader< + TestFields, + 'customIdField', + ViewerContext, + TestEntity, + TestEntityPrivacyPolicy, + keyof TestFields + >, + ) { + super(secondaryEntityCache, constructionUtils); + } + protected override async fetchObjectsFromDatabaseAsync( loadParamsArray: readonly Readonly[], ): Promise>, Readonly | null>> { @@ -129,6 +153,7 @@ describe(GenericSecondaryEntityCache, () => { new TestGenericCacher(), (params) => `intValue.${params.intValue}`, ), + EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(TestEntity, viewerContext), TestEntity.loaderWithAuthorizationResults(viewerContext), ); @@ -165,6 +190,7 @@ describe(GenericSecondaryEntityCache, () => { new TestGenericCacher(), (params) => `intValue.${params.intValue}`, ), + EntitySecondaryCacheLoader.getConstructionUtilsForEntityClass(TestEntity, viewerContext), TestEntity.loaderWithAuthorizationResults(viewerContext), ); diff --git a/packages/entity/src/__tests__/ReadonlyEntity-test.ts b/packages/entity/src/__tests__/ReadonlyEntity-test.ts index 2180eb4df..b96f6b8d2 100644 --- a/packages/entity/src/__tests__/ReadonlyEntity-test.ts +++ b/packages/entity/src/__tests__/ReadonlyEntity-test.ts @@ -5,7 +5,6 @@ import { AuthorizationResultBasedEntityAssociationLoader } from '../Authorizatio import { AuthorizationResultBasedEntityLoader } from '../AuthorizationResultBasedEntityLoader'; import { EnforcingEntityAssociationLoader } from '../EnforcingEntityAssociationLoader'; import { EnforcingEntityLoader } from '../EnforcingEntityLoader'; -import { EntityConstructionUtils } from '../EntityConstructionUtils'; import { EntityInvalidationUtils } from '../EntityInvalidationUtils'; import { ReadonlyEntity } from '../ReadonlyEntity'; import { ViewerContext } from '../ViewerContext'; @@ -224,14 +223,4 @@ describe(ReadonlyEntity, () => { ); }); }); - - describe('constructionUtils', () => { - it('creates a new EntityConstructionUtils', async () => { - const companionProvider = createUnitTestEntityCompanionProvider(); - const viewerContext = new ViewerContext(companionProvider); - expect(SimpleTestEntity.constructionUtils(viewerContext)).toBeInstanceOf( - EntityConstructionUtils, - ); - }); - }); }); diff --git a/packages/entity/src/utils/EntityPrivacyUtils.ts b/packages/entity/src/utils/EntityPrivacyUtils.ts index dfcfde9a5..f3226543e 100644 --- a/packages/entity/src/utils/EntityPrivacyUtils.ts +++ b/packages/entity/src/utils/EntityPrivacyUtils.ts @@ -355,7 +355,12 @@ async function canViewerDeleteInternalAsync< .entityConfiguration; const entityCompanion = viewerContext.getViewerScopedEntityCompanionForClass(inboundEdge); - const loader = entityCompanion.getLoaderFactory().forLoad(queryContext, { + const loaderFactory = entityCompanion.getLoaderFactory(); + const loader = loaderFactory.forLoad(queryContext, { + previousValue: null, + cascadingDeleteCause: newCascadingDeleteCause, + }); + const constructionUtils = loaderFactory.constructionUtils(queryContext, { previousValue: null, cascadingDeleteCause: newCascadingDeleteCause, }); @@ -443,14 +448,6 @@ async function canViewerDeleteInternalAsync< // privacy policy as it would be after the cascading SET NULL operation const previousAndSyntheticEntitiesForInboundEdge = entitiesForInboundEdge.map( (entity) => { - const entityLoader = viewerContext - .getViewerScopedEntityCompanionForClass(inboundEdge) - .getLoaderFactory() - .forLoad(queryContext, { - previousValue: entity, - cascadingDeleteCause: newCascadingDeleteCause, - }); - const allFields = entity.getAllDatabaseFields(); const syntheticFields = { ...allFields, @@ -459,8 +456,7 @@ async function canViewerDeleteInternalAsync< return { previousValue: entity, - syntheticallyUpdatedValue: - entityLoader.constructionUtils.constructEntity(syntheticFields), + syntheticallyUpdatedValue: constructionUtils.constructEntity(syntheticFields), }; }, );