diff --git a/packages/entity/src/EntityMutationTriggerConfiguration.ts b/packages/entity/src/EntityMutationTriggerConfiguration.ts index bb0399a8..15ef9f80 100644 --- a/packages/entity/src/EntityMutationTriggerConfiguration.ts +++ b/packages/entity/src/EntityMutationTriggerConfiguration.ts @@ -1,3 +1,4 @@ +import { EntityMutationInfo } from './EntityMutator'; import { EntityQueryContext } from './EntityQueryContext'; import ReadonlyEntity from './ReadonlyEntity'; import ViewerContext from './ViewerContext'; @@ -78,7 +79,8 @@ export abstract class EntityMutationTrigger< abstract executeAsync( viewerContext: TViewerContext, queryContext: EntityQueryContext, - entity: TEntity + entity: TEntity, + mutationInfo: EntityMutationInfo ): Promise; } @@ -93,5 +95,9 @@ export abstract class EntityNonTransactionalMutationTrigger< TEntity extends ReadonlyEntity, TSelectedFields extends keyof TFields = keyof TFields > { - abstract executeAsync(viewerContext: TViewerContext, entity: TEntity): Promise; + abstract executeAsync( + viewerContext: TViewerContext, + entity: TEntity, + mutationInfo: EntityMutationInfo + ): Promise; } diff --git a/packages/entity/src/EntityMutationValidator.ts b/packages/entity/src/EntityMutationValidator.ts index 69219bc8..77ca4504 100644 --- a/packages/entity/src/EntityMutationValidator.ts +++ b/packages/entity/src/EntityMutationValidator.ts @@ -1,3 +1,4 @@ +import { EntityMutationInfo } from './EntityMutator'; import { EntityQueryContext } from './EntityQueryContext'; import ReadonlyEntity from './ReadonlyEntity'; import ViewerContext from './ViewerContext'; @@ -16,6 +17,7 @@ export default abstract class EntityMutationValidator< abstract executeAsync( viewerContext: TViewerContext, queryContext: EntityQueryContext, - entity: TEntity + entity: TEntity, + mutationInfo: EntityMutationInfo ): Promise; } diff --git a/packages/entity/src/EntityMutator.ts b/packages/entity/src/EntityMutator.ts index b584364a..770bd5ec 100644 --- a/packages/entity/src/EntityMutator.ts +++ b/packages/entity/src/EntityMutator.ts @@ -21,6 +21,30 @@ import { timeAndLogMutationEventAsync } from './metrics/EntityMetricsUtils'; import IEntityMetricsAdapter, { EntityMetricsMutationType } from './metrics/IEntityMetricsAdapter'; import { mapMapAsync } from './utils/collections/maps'; +export enum EntityMutationType { + CREATE, + UPDATE, + DELETE, +} + +export type EntityMutationInfo< + TFields, + TID extends NonNullable, + TViewerContext extends ViewerContext, + TEntity extends Entity, + TSelectedFields extends keyof TFields +> = + | { + type: EntityMutationType.CREATE; + } + | { + type: EntityMutationType.UPDATE; + previousValue: TEntity; + } + | { + type: EntityMutationType.DELETE; + }; + abstract class BaseMutator< TFields, TID extends NonNullable, @@ -92,14 +116,15 @@ abstract class BaseMutator< | EntityMutationValidator[] | undefined, queryContext: EntityQueryContext, - entity: TEntity + entity: TEntity, + mutationInfo: EntityMutationInfo ): Promise { if (!triggersOrValidators) { return; } await Promise.all( triggersOrValidators.map((triggerOrValidator) => - triggerOrValidator.executeAsync(this.viewerContext, queryContext, entity) + triggerOrValidator.executeAsync(this.viewerContext, queryContext, entity, mutationInfo) ) ); } @@ -114,12 +139,15 @@ abstract class BaseMutator< TSelectedFields >[] | undefined, - entity: TEntity + entity: TEntity, + mutationInfo: EntityMutationInfo ): Promise { if (!triggers) { return; } - await Promise.all(triggers.map((trigger) => trigger.executeAsync(this.viewerContext, entity))); + await Promise.all( + triggers.map((trigger) => trigger.executeAsync(this.viewerContext, entity, mutationInfo)) + ); } } @@ -202,17 +230,20 @@ export class CreateMutator< await this.executeMutationTriggersOrValidatorsAsync( this.mutationValidators, queryContext, - temporaryEntityForPrivacyCheck + temporaryEntityForPrivacyCheck, + { type: EntityMutationType.CREATE } ); await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.beforeAll, queryContext, - temporaryEntityForPrivacyCheck + temporaryEntityForPrivacyCheck, + { type: EntityMutationType.CREATE } ); await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.beforeCreate, queryContext, - temporaryEntityForPrivacyCheck + temporaryEntityForPrivacyCheck, + { type: EntityMutationType.CREATE } ); const insertResult = await this.databaseAdapter.insertAsync(queryContext, this.fieldsForEntity); @@ -230,19 +261,22 @@ export class CreateMutator< await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.afterCreate, queryContext, - newEntity + newEntity, + { type: EntityMutationType.CREATE } ); await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.afterAll, queryContext, - newEntity + newEntity, + { type: EntityMutationType.CREATE } ); queryContext.appendPostCommitCallback( this.executeNonTransactionalMutationTriggersOrValidatorsAsync.bind( this, this.mutationTriggers.afterCommit, - newEntity + newEntity, + { type: EntityMutationType.CREATE } ) ); @@ -267,7 +301,7 @@ export class UpdateMutator< >, TSelectedFields extends keyof TFields > extends BaseMutator { - private readonly originalFieldsForEntity: Readonly; + private readonly originalEntity: TEntity; private readonly fieldsForEntity: TFields; private readonly updatedFields: Partial = {}; @@ -308,7 +342,7 @@ export class UpdateMutator< >, databaseAdapter: EntityDatabaseAdapter, metricsAdapter: IEntityMetricsAdapter, - fieldsForEntity: Readonly + originalEntity: TEntity ) { super( viewerContext, @@ -322,8 +356,8 @@ export class UpdateMutator< databaseAdapter, metricsAdapter ); - this.originalFieldsForEntity = { ...fieldsForEntity }; - this.fieldsForEntity = { ...fieldsForEntity }; + this.originalEntity = originalEntity; + this.fieldsForEntity = { ...originalEntity.getAllDatabaseFields() }; } /** @@ -384,17 +418,20 @@ export class UpdateMutator< await this.executeMutationTriggersOrValidatorsAsync( this.mutationValidators, queryContext, - entityAboutToBeUpdated + entityAboutToBeUpdated, + { type: EntityMutationType.UPDATE, previousValue: this.originalEntity } ); await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.beforeAll, queryContext, - entityAboutToBeUpdated + entityAboutToBeUpdated, + { type: EntityMutationType.UPDATE, previousValue: this.originalEntity } ); await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.beforeUpdate, queryContext, - entityAboutToBeUpdated + entityAboutToBeUpdated, + { type: EntityMutationType.UPDATE, previousValue: this.originalEntity } ); const updateResult = await this.databaseAdapter.updateAsync( @@ -407,7 +444,10 @@ export class UpdateMutator< const entityLoader = this.entityLoaderFactory.forLoad(this.viewerContext, queryContext); queryContext.appendPostCommitCallback( - entityLoader.invalidateFieldsAsync.bind(entityLoader, this.originalFieldsForEntity) + entityLoader.invalidateFieldsAsync.bind( + entityLoader, + this.originalEntity.getAllDatabaseFields() + ) ); queryContext.appendPostCommitCallback( entityLoader.invalidateFieldsAsync.bind(entityLoader, this.fieldsForEntity) @@ -421,19 +461,22 @@ export class UpdateMutator< await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.afterUpdate, queryContext, - updatedEntity + updatedEntity, + { type: EntityMutationType.UPDATE, previousValue: this.originalEntity } ); await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.afterAll, queryContext, - updatedEntity + updatedEntity, + { type: EntityMutationType.UPDATE, previousValue: this.originalEntity } ); queryContext.appendPostCommitCallback( this.executeNonTransactionalMutationTriggersOrValidatorsAsync.bind( this, this.mutationTriggers.afterCommit, - updatedEntity + updatedEntity, + { type: EntityMutationType.UPDATE, previousValue: this.originalEntity } ) ); @@ -564,12 +607,14 @@ export class DeleteMutator< await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.beforeAll, queryContext, - this.entity + this.entity, + { type: EntityMutationType.DELETE } ); await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.beforeDelete, queryContext, - this.entity + this.entity, + { type: EntityMutationType.DELETE } ); if (!skipDatabaseDeletion) { @@ -588,19 +633,22 @@ export class DeleteMutator< await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.afterDelete, queryContext, - this.entity + this.entity, + { type: EntityMutationType.DELETE } ); await this.executeMutationTriggersOrValidatorsAsync( this.mutationTriggers.afterAll, queryContext, - this.entity + this.entity, + { type: EntityMutationType.DELETE } ); queryContext.appendPostCommitCallback( this.executeNonTransactionalMutationTriggersOrValidatorsAsync.bind( this, this.mutationTriggers.afterCommit, - this.entity + this.entity, + { type: EntityMutationType.DELETE } ) ); diff --git a/packages/entity/src/EntityMutatorFactory.ts b/packages/entity/src/EntityMutatorFactory.ts index 8fb80cc3..750c6987 100644 --- a/packages/entity/src/EntityMutatorFactory.ts +++ b/packages/entity/src/EntityMutatorFactory.ts @@ -109,7 +109,7 @@ export default class EntityMutatorFactory< this.entityLoaderFactory, this.databaseAdapter, this.metricsAdapter, - existingEntity.getAllDatabaseFields() + existingEntity ); } diff --git a/packages/entity/src/__tests__/EntityMutator-test.ts b/packages/entity/src/__tests__/EntityMutator-test.ts index f26d8cd8..8c54d07f 100644 --- a/packages/entity/src/__tests__/EntityMutator-test.ts +++ b/packages/entity/src/__tests__/EntityMutator-test.ts @@ -8,6 +8,7 @@ import { when, anything, objectContaining, + deepEqual, } from 'ts-mockito'; import { v4 as uuidv4 } from 'uuid'; @@ -18,6 +19,7 @@ import EntityMutationTriggerConfiguration, { EntityNonTransactionalMutationTrigger, } from '../EntityMutationTriggerConfiguration'; import EntityMutationValidator from '../EntityMutationValidator'; +import { EntityMutationInfo, EntityMutationType } from '../EntityMutator'; import EntityMutatorFactory from '../EntityMutatorFactory'; import { EntityTransactionalQueryContext, EntityQueryContext } from '../EntityQueryContext'; import ViewerContext from '../ViewerContext'; @@ -50,7 +52,14 @@ class TestMutationTrigger extends EntityMutationTrigger< async executeAsync( _viewerContext: ViewerContext, _queryContext: EntityQueryContext, - _entity: TestEntity + _entity: TestEntity, + _mutationInfo: EntityMutationInfo< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > ): Promise {} } @@ -85,14 +94,16 @@ const verifyValidatorCounts = ( TestEntity, keyof TestFields >[], - expectedCalls: number + expectedCalls: number, + mutationInfo: EntityMutationInfo ): void => { for (const validator of mutationValidatorSpies) { verify( validator.executeAsync( viewerContext, anyOfClass(EntityTransactionalQueryContext), - anyOfClass(TestEntity) + anyOfClass(TestEntity), + deepEqual(mutationInfo) ) ).times(expectedCalls); } @@ -152,7 +163,8 @@ const verifyTriggerCounts = ( | 'afterDelete' >, boolean - > + >, + mutationInfo: EntityMutationInfo ): void => { Object.keys(executed).forEach((s) => { if ((executed as any)[s]) { @@ -160,7 +172,8 @@ const verifyTriggerCounts = ( (mutationTriggerSpies as any)[s]![0].executeAsync( viewerContext, anyOfClass(EntityTransactionalQueryContext), - anyOfClass(TestEntity) + anyOfClass(TestEntity), + deepEqual(mutationInfo) ) ).once(); } else { @@ -168,7 +181,8 @@ const verifyTriggerCounts = ( (mutationTriggerSpies as any)[s]![0].executeAsync( viewerContext, anyOfClass(EntityTransactionalQueryContext), - anyOfClass(TestEntity) + anyOfClass(TestEntity), + deepEqual(mutationInfo) ) ).never(); } @@ -178,7 +192,8 @@ const verifyTriggerCounts = ( mutationTriggerSpies.beforeAll![0].executeAsync( viewerContext, anyOfClass(EntityTransactionalQueryContext), - anyOfClass(TestEntity) + anyOfClass(TestEntity), + deepEqual(mutationInfo) ) ).once(); @@ -186,12 +201,17 @@ const verifyTriggerCounts = ( mutationTriggerSpies.afterAll![0].executeAsync( viewerContext, anyOfClass(EntityTransactionalQueryContext), - anyOfClass(TestEntity) + anyOfClass(TestEntity), + deepEqual(mutationInfo) ) ).once(); verify( - mutationTriggerSpies.afterCommit![0].executeAsync(viewerContext, anyOfClass(TestEntity)) + mutationTriggerSpies.afterCommit![0].executeAsync( + viewerContext, + anyOfClass(TestEntity), + deepEqual(mutationInfo) + ) ).once(); }; @@ -405,14 +425,19 @@ describe(EntityMutatorFactory, () => { .setField('stringField', 'huh') .enforceCreateAsync(); - verifyTriggerCounts(viewerContext, triggerSpies, { - beforeCreate: true, - afterCreate: true, - beforeUpdate: false, - afterUpdate: false, - beforeDelete: false, - afterDelete: false, - }); + verifyTriggerCounts( + viewerContext, + triggerSpies, + { + beforeCreate: true, + afterCreate: true, + beforeUpdate: false, + afterUpdate: false, + beforeDelete: false, + afterDelete: false, + }, + { type: EntityMutationType.CREATE } + ); }); it('executes validators', async () => { @@ -447,7 +472,7 @@ describe(EntityMutatorFactory, () => { .setField('stringField', 'huh') .enforceCreateAsync(); - verifyValidatorCounts(viewerContext, validatorSpies, 1); + verifyValidatorCounts(viewerContext, validatorSpies, 1, { type: EntityMutationType.CREATE }); }); }); @@ -585,14 +610,19 @@ describe(EntityMutatorFactory, () => { .setField('stringField', 'huh2') .enforceUpdateAsync(); - verifyTriggerCounts(viewerContext, triggerSpies, { - beforeCreate: false, - afterCreate: false, - beforeUpdate: true, - afterUpdate: true, - beforeDelete: false, - afterDelete: false, - }); + verifyTriggerCounts( + viewerContext, + triggerSpies, + { + beforeCreate: false, + afterCreate: false, + beforeUpdate: true, + afterUpdate: true, + beforeDelete: false, + afterDelete: false, + }, + { type: EntityMutationType.UPDATE, previousValue: existingEntity } + ); }); it('executes validators', async () => { const viewerContext = mock(); @@ -634,7 +664,10 @@ describe(EntityMutatorFactory, () => { .setField('stringField', 'huh2') .enforceUpdateAsync(); - verifyValidatorCounts(viewerContext, validatorSpies, 1); + verifyValidatorCounts(viewerContext, validatorSpies, 1, { + type: EntityMutationType.UPDATE, + previousValue: existingEntity, + }); }); }); @@ -734,14 +767,19 @@ describe(EntityMutatorFactory, () => { await entityMutatorFactory.forDelete(existingEntity, queryContext).enforceDeleteAsync(); - verifyTriggerCounts(viewerContext, triggerSpies, { - beforeCreate: false, - afterCreate: false, - beforeUpdate: false, - afterUpdate: false, - beforeDelete: true, - afterDelete: true, - }); + verifyTriggerCounts( + viewerContext, + triggerSpies, + { + beforeCreate: false, + afterCreate: false, + beforeUpdate: false, + afterUpdate: false, + beforeDelete: true, + afterDelete: true, + }, + { type: EntityMutationType.DELETE } + ); }); it('does not execute validators', async () => { @@ -772,7 +810,7 @@ describe(EntityMutatorFactory, () => { await entityMutatorFactory.forDelete(existingEntity, queryContext).enforceDeleteAsync(); - verifyValidatorCounts(viewerContext, validatorSpies, 0); + verifyValidatorCounts(viewerContext, validatorSpies, 0, { type: EntityMutationType.DELETE }); }); });