From c4d40e470b6aa0afec49402797d04d1a0e34c511 Mon Sep 17 00:00:00 2001 From: Quinlan Jung Date: Wed, 22 Jul 2020 17:58:32 -0700 Subject: [PATCH 1/3] entity mutation validators --- packages/entity/src/EntityCompanion.ts | 13 ++++- .../entity/src/EntityCompanionProvider.ts | 22 +++++++- packages/entity/src/EntityMutationTrigger.ts | 13 +++++ packages/entity/src/EntityMutator.ts | 52 ++++++++++++++++++- packages/entity/src/EntityMutatorFactory.ts | 10 ++++ .../src/__tests__/EntityCompanion-test.ts | 1 + .../src/__tests__/EntityMutator-test.ts | 15 ++++++ 7 files changed, 123 insertions(+), 3 deletions(-) diff --git a/packages/entity/src/EntityCompanion.ts b/packages/entity/src/EntityCompanion.ts index edf348e7..5c7a7b7d 100644 --- a/packages/entity/src/EntityCompanion.ts +++ b/packages/entity/src/EntityCompanion.ts @@ -1,6 +1,9 @@ import { IEntityClass } from './Entity'; import EntityLoaderFactory from './EntityLoaderFactory'; -import { EntityMutationTriggerConfiguration } from './EntityMutationTrigger'; +import { + EntityMutationTriggerConfiguration, + EntityMutationValidatorConfiguration, +} from './EntityMutationTrigger'; import EntityMutatorFactory from './EntityMutatorFactory'; import EntityPrivacyPolicy from './EntityPrivacyPolicy'; import IEntityQueryContextProvider from './IEntityQueryContextProvider'; @@ -58,6 +61,13 @@ export default class EntityCompanion< >, private readonly tableDataCoordinator: EntityTableDataCoordinator, PrivacyPolicyClass: IPrivacyPolicyClass, + mutationValidators: EntityMutationValidatorConfiguration< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -78,6 +88,7 @@ export default class EntityCompanion< tableDataCoordinator.entityConfiguration, entityClass, privacyPolicy, + mutationValidators, mutationTriggers, this.entityLoaderFactory, tableDataCoordinator.databaseAdapter, diff --git a/packages/entity/src/EntityCompanionProvider.ts b/packages/entity/src/EntityCompanionProvider.ts index 7325e15a..e45eed99 100644 --- a/packages/entity/src/EntityCompanionProvider.ts +++ b/packages/entity/src/EntityCompanionProvider.ts @@ -1,7 +1,10 @@ import { IEntityClass } from './Entity'; import EntityCompanion, { IPrivacyPolicyClass } from './EntityCompanion'; import EntityConfiguration from './EntityConfiguration'; -import { EntityMutationTriggerConfiguration } from './EntityMutationTrigger'; +import { + EntityMutationTriggerConfiguration, + EntityMutationValidatorConfiguration, +} from './EntityMutationTrigger'; import EntityPrivacyPolicy from './EntityPrivacyPolicy'; import IEntityCacheAdapterProvider from './IEntityCacheAdapterProvider'; import IEntityDatabaseAdapterProvider from './IEntityDatabaseAdapterProvider'; @@ -81,6 +84,13 @@ export class EntityCompanionDefinition< >; readonly entityConfiguration: EntityConfiguration; readonly privacyPolicyClass: IPrivacyPolicyClass; + readonly mutationValidators: EntityMutationValidatorConfiguration< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >; readonly mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -94,6 +104,7 @@ export class EntityCompanionDefinition< entityClass, entityConfiguration, privacyPolicyClass, + mutationValidators = {}, mutationTriggers = {}, entitySelectedFields = Array.from(entityConfiguration.schema.keys()) as TSelectedFields[], }: { @@ -107,6 +118,13 @@ export class EntityCompanionDefinition< >; entityConfiguration: EntityConfiguration; privacyPolicyClass: IPrivacyPolicyClass; + mutationValidators?: EntityMutationValidatorConfiguration< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >; mutationTriggers?: EntityMutationTriggerConfiguration< TFields, TID, @@ -119,6 +137,7 @@ export class EntityCompanionDefinition< this.entityClass = entityClass; this.entityConfiguration = entityConfiguration; this.privacyPolicyClass = privacyPolicyClass; + this.mutationValidators = mutationValidators; this.mutationTriggers = mutationTriggers; this.entitySelectedFields = entitySelectedFields; } @@ -201,6 +220,7 @@ export default class EntityCompanionProvider { entityCompanionDefinition.entityClass, tableDataCoordinator, entityCompanionDefinition.privacyPolicyClass, + entityCompanionDefinition.mutationValidators, entityCompanionDefinition.mutationTriggers, this.metricsAdapter ); diff --git a/packages/entity/src/EntityMutationTrigger.ts b/packages/entity/src/EntityMutationTrigger.ts index 4bad44a3..a1138111 100644 --- a/packages/entity/src/EntityMutationTrigger.ts +++ b/packages/entity/src/EntityMutationTrigger.ts @@ -2,6 +2,19 @@ import { EntityQueryContext } from './EntityQueryContext'; import ReadonlyEntity from './ReadonlyEntity'; import ViewerContext from './ViewerContext'; +export interface EntityMutationValidatorConfiguration< + TFields, + TID, + TViewerContext extends ViewerContext, + TEntity extends ReadonlyEntity, + TSelectedFields extends keyof TFields = keyof TFields +> { + beforeCreate?: EntityMutationTrigger[]; + beforeUpdate?: EntityMutationTrigger[]; + beforeDelete?: EntityMutationTrigger[]; + beforeAll?: EntityMutationTrigger[]; +} + /** * Interface to define trigger behavior for entities. */ diff --git a/packages/entity/src/EntityMutator.ts b/packages/entity/src/EntityMutator.ts index 763a6c67..9e6a3641 100644 --- a/packages/entity/src/EntityMutator.ts +++ b/packages/entity/src/EntityMutator.ts @@ -6,7 +6,11 @@ import EntityConfiguration from './EntityConfiguration'; import EntityDatabaseAdapter from './EntityDatabaseAdapter'; import { EntityEdgeDeletionBehavior } from './EntityFields'; import EntityLoaderFactory from './EntityLoaderFactory'; -import { EntityMutationTriggerConfiguration, EntityMutationTrigger } from './EntityMutationTrigger'; +import { + EntityMutationTrigger, + EntityMutationTriggerConfiguration, + EntityMutationValidatorConfiguration, +} from './EntityMutationTrigger'; import EntityPrivacyPolicy from './EntityPrivacyPolicy'; import { EntityQueryContext, EntityTransactionalQueryContext } from './EntityQueryContext'; import ReadonlyEntity from './ReadonlyEntity'; @@ -42,6 +46,13 @@ abstract class BaseMutator< TSelectedFields >, protected readonly privacyPolicy: TPrivacyPolicy, + protected readonly mutationValidators: EntityMutationValidatorConfiguration< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, protected readonly mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -158,6 +169,16 @@ export class CreateMutator< return authorizeCreateResult; } + await this.executeTriggers( + this.mutationValidators.beforeAll, + queryContext, + temporaryEntityForPrivacyCheck + ); + await this.executeTriggers( + this.mutationValidators.beforeCreate, + queryContext, + temporaryEntityForPrivacyCheck + ); await this.executeTriggers( this.mutationTriggers.beforeAll, queryContext, @@ -220,6 +241,13 @@ export class UpdateMutator< TSelectedFields >, privacyPolicy: TPrivacyPolicy, + mutationValidators: EntityMutationValidatorConfiguration< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -245,6 +273,7 @@ export class UpdateMutator< entityConfiguration, entityClass, privacyPolicy, + mutationValidators, mutationTriggers, entityLoaderFactory, databaseAdapter, @@ -314,6 +343,16 @@ export class UpdateMutator< return authorizeUpdateResult; } + await this.executeTriggers( + this.mutationValidators.beforeAll, + queryContext, + entityAboutToBeUpdated + ); + await this.executeTriggers( + this.mutationValidators.beforeUpdate, + queryContext, + entityAboutToBeUpdated + ); await this.executeTriggers( this.mutationTriggers.beforeAll, queryContext, @@ -379,6 +418,13 @@ export class DeleteMutator< TSelectedFields >, privacyPolicy: TPrivacyPolicy, + mutationValidators: EntityMutationValidatorConfiguration< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -404,6 +450,7 @@ export class DeleteMutator< entityConfiguration, entityClass, privacyPolicy, + mutationValidators, mutationTriggers, entityLoaderFactory, databaseAdapter, @@ -460,6 +507,9 @@ export class DeleteMutator< processedEntityIdentifiersFromTransitiveDeletions ); + await this.executeTriggers(this.mutationValidators.beforeAll, queryContext, this.entity); + await this.executeTriggers(this.mutationValidators.beforeDelete, queryContext, this.entity); + await this.executeTriggers(this.mutationTriggers.beforeAll, queryContext, this.entity); await this.executeTriggers(this.mutationTriggers.beforeDelete, queryContext, this.entity); diff --git a/packages/entity/src/EntityMutatorFactory.ts b/packages/entity/src/EntityMutatorFactory.ts index 9694ce0e..c286c49c 100644 --- a/packages/entity/src/EntityMutatorFactory.ts +++ b/packages/entity/src/EntityMutatorFactory.ts @@ -37,6 +37,13 @@ export default class EntityMutatorFactory< TSelectedFields >, private readonly privacyPolicy: TPrivacyPolicy, + private readonly mutationValidators: EntityMutationTriggerConfiguration< + TFields, + TID, + TViewerContext, + TEntity, + TSelectedFields + >, private readonly mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -72,6 +79,7 @@ export default class EntityMutatorFactory< this.entityConfiguration, this.entityClass, this.privacyPolicy, + this.mutationValidators, this.mutationTriggers, this.entityLoaderFactory, this.databaseAdapter, @@ -95,6 +103,7 @@ export default class EntityMutatorFactory< this.entityConfiguration, this.entityClass, this.privacyPolicy, + this.mutationValidators, this.mutationTriggers, this.entityLoaderFactory, this.databaseAdapter, @@ -118,6 +127,7 @@ export default class EntityMutatorFactory< this.entityConfiguration, this.entityClass, this.privacyPolicy, + this.mutationValidators, this.mutationTriggers, this.entityLoaderFactory, this.databaseAdapter, diff --git a/packages/entity/src/__tests__/EntityCompanion-test.ts b/packages/entity/src/__tests__/EntityCompanion-test.ts index 5aafd716..bf1bdf10 100644 --- a/packages/entity/src/__tests__/EntityCompanion-test.ts +++ b/packages/entity/src/__tests__/EntityCompanion-test.ts @@ -20,6 +20,7 @@ describe(EntityCompanion, () => { instance(tableDataCoordinatorMock), TestEntityPrivacyPolicy, {}, + {}, instance(mock()) ); expect(companion.getLoaderFactory()).toBeInstanceOf(EntityLoaderFactory); diff --git a/packages/entity/src/__tests__/EntityMutator-test.ts b/packages/entity/src/__tests__/EntityMutator-test.ts index 5e4bd199..d14eedbd 100644 --- a/packages/entity/src/__tests__/EntityMutator-test.ts +++ b/packages/entity/src/__tests__/EntityMutator-test.ts @@ -185,6 +185,18 @@ const createEntityMutatorFactory = ( keyof TestFields >; } => { + const mutatationValidators: EntityMutationTriggerConfiguration< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + > = { + beforeCreate: [new TestMutationTrigger()], + beforeUpdate: [new TestMutationTrigger()], + beforeDelete: [new TestMutationTrigger()], + beforeAll: [new TestMutationTrigger()], + }; const mutationTriggers: EntityMutationTriggerConfiguration< TestFields, string, @@ -230,6 +242,7 @@ const createEntityMutatorFactory = ( testEntityConfiguration, TestEntity, privacyPolicy, + mutatationValidators, mutationTriggers, entityLoaderFactory, databaseAdapter, @@ -657,6 +670,7 @@ describe(EntityMutatorFactory, () => { SimpleTestEntity, instance(privacyPolicyMock), {}, + {}, entityLoaderFactory, databaseAdapter, metricsAdapter @@ -733,6 +747,7 @@ describe(EntityMutatorFactory, () => { SimpleTestEntity, privacyPolicy, {}, + {}, entityLoaderFactory, instance(databaseAdapterMock), metricsAdapter From 79f3e899cbf2242b32483ba8c8bc28d68686eba1 Mon Sep 17 00:00:00 2001 From: Quinlan Jung Date: Wed, 22 Jul 2020 18:41:16 -0700 Subject: [PATCH 2/3] pr feedback --- packages/entity/src/EntityCompanion.ts | 9 ++--- .../entity/src/EntityCompanionProvider.ts | 15 +++---- packages/entity/src/EntityMutationTrigger.ts | 13 ------- packages/entity/src/EntityMutator.ts | 39 +++++-------------- packages/entity/src/EntityMutatorFactory.ts | 6 +-- .../src/__tests__/EntityCompanion-test.ts | 2 +- .../src/__tests__/EntityMutator-test.ts | 13 ++----- 7 files changed, 26 insertions(+), 71 deletions(-) diff --git a/packages/entity/src/EntityCompanion.ts b/packages/entity/src/EntityCompanion.ts index 5c7a7b7d..eeda9f8d 100644 --- a/packages/entity/src/EntityCompanion.ts +++ b/packages/entity/src/EntityCompanion.ts @@ -1,9 +1,6 @@ import { IEntityClass } from './Entity'; import EntityLoaderFactory from './EntityLoaderFactory'; -import { - EntityMutationTriggerConfiguration, - EntityMutationValidatorConfiguration, -} from './EntityMutationTrigger'; +import { EntityMutationTriggerConfiguration, EntityMutationTrigger } from './EntityMutationTrigger'; import EntityMutatorFactory from './EntityMutatorFactory'; import EntityPrivacyPolicy from './EntityPrivacyPolicy'; import IEntityQueryContextProvider from './IEntityQueryContextProvider'; @@ -61,13 +58,13 @@ export default class EntityCompanion< >, private readonly tableDataCoordinator: EntityTableDataCoordinator, PrivacyPolicyClass: IPrivacyPolicyClass, - mutationValidators: EntityMutationValidatorConfiguration< + mutationValidators: EntityMutationTrigger< TFields, TID, TViewerContext, TEntity, TSelectedFields - >, + >[], mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, diff --git a/packages/entity/src/EntityCompanionProvider.ts b/packages/entity/src/EntityCompanionProvider.ts index e45eed99..02aa0797 100644 --- a/packages/entity/src/EntityCompanionProvider.ts +++ b/packages/entity/src/EntityCompanionProvider.ts @@ -1,10 +1,7 @@ import { IEntityClass } from './Entity'; import EntityCompanion, { IPrivacyPolicyClass } from './EntityCompanion'; import EntityConfiguration from './EntityConfiguration'; -import { - EntityMutationTriggerConfiguration, - EntityMutationValidatorConfiguration, -} from './EntityMutationTrigger'; +import { EntityMutationTriggerConfiguration, EntityMutationTrigger } from './EntityMutationTrigger'; import EntityPrivacyPolicy from './EntityPrivacyPolicy'; import IEntityCacheAdapterProvider from './IEntityCacheAdapterProvider'; import IEntityDatabaseAdapterProvider from './IEntityDatabaseAdapterProvider'; @@ -84,13 +81,13 @@ export class EntityCompanionDefinition< >; readonly entityConfiguration: EntityConfiguration; readonly privacyPolicyClass: IPrivacyPolicyClass; - readonly mutationValidators: EntityMutationValidatorConfiguration< + readonly mutationValidators: EntityMutationTrigger< TFields, TID, TViewerContext, TEntity, TSelectedFields - >; + >[]; readonly mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -104,7 +101,7 @@ export class EntityCompanionDefinition< entityClass, entityConfiguration, privacyPolicyClass, - mutationValidators = {}, + mutationValidators = [], mutationTriggers = {}, entitySelectedFields = Array.from(entityConfiguration.schema.keys()) as TSelectedFields[], }: { @@ -118,13 +115,13 @@ export class EntityCompanionDefinition< >; entityConfiguration: EntityConfiguration; privacyPolicyClass: IPrivacyPolicyClass; - mutationValidators?: EntityMutationValidatorConfiguration< + mutationValidators?: EntityMutationTrigger< TFields, TID, TViewerContext, TEntity, TSelectedFields - >; + >[]; mutationTriggers?: EntityMutationTriggerConfiguration< TFields, TID, diff --git a/packages/entity/src/EntityMutationTrigger.ts b/packages/entity/src/EntityMutationTrigger.ts index a1138111..4bad44a3 100644 --- a/packages/entity/src/EntityMutationTrigger.ts +++ b/packages/entity/src/EntityMutationTrigger.ts @@ -2,19 +2,6 @@ import { EntityQueryContext } from './EntityQueryContext'; import ReadonlyEntity from './ReadonlyEntity'; import ViewerContext from './ViewerContext'; -export interface EntityMutationValidatorConfiguration< - TFields, - TID, - TViewerContext extends ViewerContext, - TEntity extends ReadonlyEntity, - TSelectedFields extends keyof TFields = keyof TFields -> { - beforeCreate?: EntityMutationTrigger[]; - beforeUpdate?: EntityMutationTrigger[]; - beforeDelete?: EntityMutationTrigger[]; - beforeAll?: EntityMutationTrigger[]; -} - /** * Interface to define trigger behavior for entities. */ diff --git a/packages/entity/src/EntityMutator.ts b/packages/entity/src/EntityMutator.ts index 9e6a3641..68a5f08b 100644 --- a/packages/entity/src/EntityMutator.ts +++ b/packages/entity/src/EntityMutator.ts @@ -6,11 +6,7 @@ import EntityConfiguration from './EntityConfiguration'; import EntityDatabaseAdapter from './EntityDatabaseAdapter'; import { EntityEdgeDeletionBehavior } from './EntityFields'; import EntityLoaderFactory from './EntityLoaderFactory'; -import { - EntityMutationTrigger, - EntityMutationTriggerConfiguration, - EntityMutationValidatorConfiguration, -} from './EntityMutationTrigger'; +import { EntityMutationTrigger, EntityMutationTriggerConfiguration } from './EntityMutationTrigger'; import EntityPrivacyPolicy from './EntityPrivacyPolicy'; import { EntityQueryContext, EntityTransactionalQueryContext } from './EntityQueryContext'; import ReadonlyEntity from './ReadonlyEntity'; @@ -46,13 +42,13 @@ abstract class BaseMutator< TSelectedFields >, protected readonly privacyPolicy: TPrivacyPolicy, - protected readonly mutationValidators: EntityMutationValidatorConfiguration< + protected readonly mutationValidators: EntityMutationTrigger< TFields, TID, TViewerContext, TEntity, TSelectedFields - >, + >[], protected readonly mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -170,12 +166,7 @@ export class CreateMutator< } await this.executeTriggers( - this.mutationValidators.beforeAll, - queryContext, - temporaryEntityForPrivacyCheck - ); - await this.executeTriggers( - this.mutationValidators.beforeCreate, + this.mutationValidators, queryContext, temporaryEntityForPrivacyCheck ); @@ -241,13 +232,13 @@ export class UpdateMutator< TSelectedFields >, privacyPolicy: TPrivacyPolicy, - mutationValidators: EntityMutationValidatorConfiguration< + mutationValidators: EntityMutationTrigger< TFields, TID, TViewerContext, TEntity, TSelectedFields - >, + >[], mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -343,16 +334,7 @@ export class UpdateMutator< return authorizeUpdateResult; } - await this.executeTriggers( - this.mutationValidators.beforeAll, - queryContext, - entityAboutToBeUpdated - ); - await this.executeTriggers( - this.mutationValidators.beforeUpdate, - queryContext, - entityAboutToBeUpdated - ); + await this.executeTriggers(this.mutationValidators, queryContext, entityAboutToBeUpdated); await this.executeTriggers( this.mutationTriggers.beforeAll, queryContext, @@ -418,13 +400,13 @@ export class DeleteMutator< TSelectedFields >, privacyPolicy: TPrivacyPolicy, - mutationValidators: EntityMutationValidatorConfiguration< + mutationValidators: EntityMutationTrigger< TFields, TID, TViewerContext, TEntity, TSelectedFields - >, + >[], mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, @@ -507,9 +489,6 @@ export class DeleteMutator< processedEntityIdentifiersFromTransitiveDeletions ); - await this.executeTriggers(this.mutationValidators.beforeAll, queryContext, this.entity); - await this.executeTriggers(this.mutationValidators.beforeDelete, queryContext, this.entity); - await this.executeTriggers(this.mutationTriggers.beforeAll, queryContext, this.entity); await this.executeTriggers(this.mutationTriggers.beforeDelete, queryContext, this.entity); diff --git a/packages/entity/src/EntityMutatorFactory.ts b/packages/entity/src/EntityMutatorFactory.ts index c286c49c..0cbb0d49 100644 --- a/packages/entity/src/EntityMutatorFactory.ts +++ b/packages/entity/src/EntityMutatorFactory.ts @@ -2,7 +2,7 @@ import Entity, { IEntityClass } from './Entity'; import EntityConfiguration from './EntityConfiguration'; import EntityDatabaseAdapter from './EntityDatabaseAdapter'; import EntityLoaderFactory from './EntityLoaderFactory'; -import { EntityMutationTriggerConfiguration } from './EntityMutationTrigger'; +import { EntityMutationTriggerConfiguration, EntityMutationTrigger } from './EntityMutationTrigger'; import { CreateMutator, UpdateMutator, DeleteMutator } from './EntityMutator'; import EntityPrivacyPolicy from './EntityPrivacyPolicy'; import { EntityQueryContext } from './EntityQueryContext'; @@ -37,13 +37,13 @@ export default class EntityMutatorFactory< TSelectedFields >, private readonly privacyPolicy: TPrivacyPolicy, - private readonly mutationValidators: EntityMutationTriggerConfiguration< + private readonly mutationValidators: EntityMutationTrigger< TFields, TID, TViewerContext, TEntity, TSelectedFields - >, + >[], private readonly mutationTriggers: EntityMutationTriggerConfiguration< TFields, TID, diff --git a/packages/entity/src/__tests__/EntityCompanion-test.ts b/packages/entity/src/__tests__/EntityCompanion-test.ts index bf1bdf10..4d5f9d02 100644 --- a/packages/entity/src/__tests__/EntityCompanion-test.ts +++ b/packages/entity/src/__tests__/EntityCompanion-test.ts @@ -19,7 +19,7 @@ describe(EntityCompanion, () => { TestEntity, instance(tableDataCoordinatorMock), TestEntityPrivacyPolicy, - {}, + [], {}, instance(mock()) ); diff --git a/packages/entity/src/__tests__/EntityMutator-test.ts b/packages/entity/src/__tests__/EntityMutator-test.ts index d14eedbd..75652290 100644 --- a/packages/entity/src/__tests__/EntityMutator-test.ts +++ b/packages/entity/src/__tests__/EntityMutator-test.ts @@ -185,18 +185,13 @@ const createEntityMutatorFactory = ( keyof TestFields >; } => { - const mutatationValidators: EntityMutationTriggerConfiguration< + const mutatationValidators: EntityMutationTrigger< TestFields, string, ViewerContext, TestEntity, keyof TestFields - > = { - beforeCreate: [new TestMutationTrigger()], - beforeUpdate: [new TestMutationTrigger()], - beforeDelete: [new TestMutationTrigger()], - beforeAll: [new TestMutationTrigger()], - }; + >[] = []; const mutationTriggers: EntityMutationTriggerConfiguration< TestFields, string, @@ -669,7 +664,7 @@ describe(EntityMutatorFactory, () => { simpleTestEntityConfiguration, SimpleTestEntity, instance(privacyPolicyMock), - {}, + [], {}, entityLoaderFactory, databaseAdapter, @@ -746,7 +741,7 @@ describe(EntityMutatorFactory, () => { simpleTestEntityConfiguration, SimpleTestEntity, privacyPolicy, - {}, + [], {}, entityLoaderFactory, instance(databaseAdapterMock), From c518bac24afa3c7c3515cfa855a7a4db45bc848a Mon Sep 17 00:00:00 2001 From: Quinlan Jung Date: Thu, 23 Jul 2020 00:58:43 -0700 Subject: [PATCH 3/3] tests --- .../PostgresEntityIntegration-test.ts | 60 +++++++ .../PostgresValidatorTestEntity.ts | 147 ++++++++++++++++++ .../src/__tests__/EntityMutator-test.ts | 144 ++++++++++++++++- 3 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 packages/entity-database-adapter-knex/src/testfixtures/PostgresValidatorTestEntity.ts diff --git a/packages/entity-database-adapter-knex/src/__integration-tests__/PostgresEntityIntegration-test.ts b/packages/entity-database-adapter-knex/src/__integration-tests__/PostgresEntityIntegration-test.ts index b542f561..b413931e 100644 --- a/packages/entity-database-adapter-knex/src/__integration-tests__/PostgresEntityIntegration-test.ts +++ b/packages/entity-database-adapter-knex/src/__integration-tests__/PostgresEntityIntegration-test.ts @@ -9,6 +9,7 @@ import Knex from 'knex'; import PostgresTestEntity from '../testfixtures/PostgresTestEntity'; import PostgresTriggerTestEntity from '../testfixtures/PostgresTriggerTestEntity'; +import PostgresValidatorTestEntity from '../testfixtures/PostgresValidatorTestEntity'; import { createKnexIntegrationTestEntityCompanionProvider } from '../testfixtures/createKnexIntegrationTestEntityCompanionProvider'; describe('postgres entity integration', () => { @@ -485,5 +486,64 @@ describe('postgres entity integration', () => { ).resolves.not.toBeNull(); }); }); + describe('validation transaction behavior', () => { + describe('create', () => { + it('rolls back transaction when trigger throws ', async () => { + const vc1 = new ViewerContext( + createKnexIntegrationTestEntityCompanionProvider(knexInstance) + ); + + await expect( + PostgresValidatorTestEntity.creator(vc1) + .setField('name', 'beforeCreateAndBeforeUpdate') + .enforceCreateAsync() + ).rejects.toThrowError('name cannot have value beforeCreateAndBeforeUpdate'); + await expect( + PostgresValidatorTestEntity.loader(vc1) + .enforcing() + .loadByFieldEqualingAsync('name', 'beforeCreateAndBeforeUpdate') + ).resolves.toBeNull(); + }); + }); + describe('update', () => { + it('rolls back transaction when trigger throws ', async () => { + const vc1 = new ViewerContext( + createKnexIntegrationTestEntityCompanionProvider(knexInstance) + ); + + const entity = await PostgresValidatorTestEntity.creator(vc1) + .setField('name', 'blah') + .enforceCreateAsync(); + + await expect( + PostgresValidatorTestEntity.updater(entity) + .setField('name', 'beforeCreateAndBeforeUpdate') + .enforceUpdateAsync() + ).rejects.toThrowError('name cannot have value beforeCreateAndBeforeUpdate'); + await expect( + PostgresValidatorTestEntity.loader(vc1) + .enforcing() + .loadByFieldEqualingAsync('name', 'beforeCreateAndBeforeUpdate') + ).resolves.toBeNull(); + }); + }); + describe('delete', () => { + it('validation should not run on a delete mutation', async () => { + const vc1 = new ViewerContext( + createKnexIntegrationTestEntityCompanionProvider(knexInstance) + ); + + const entityToDelete = await PostgresValidatorTestEntity.creator(vc1) + .setField('name', 'shouldBeDeleted') + .enforceCreateAsync(); + await PostgresValidatorTestEntity.enforceDeleteAsync(entityToDelete); + await expect( + PostgresValidatorTestEntity.loader(vc1) + .enforcing() + .loadByFieldEqualingAsync('name', 'shouldBeDeleted') + ).resolves.toBeNull(); + }); + }); + }); }); }); diff --git a/packages/entity-database-adapter-knex/src/testfixtures/PostgresValidatorTestEntity.ts b/packages/entity-database-adapter-knex/src/testfixtures/PostgresValidatorTestEntity.ts new file mode 100644 index 00000000..cce9b54e --- /dev/null +++ b/packages/entity-database-adapter-knex/src/testfixtures/PostgresValidatorTestEntity.ts @@ -0,0 +1,147 @@ +import { + AlwaysAllowPrivacyPolicyRule, + EntityPrivacyPolicy, + ViewerContext, + UUIDField, + StringField, + EntityConfiguration, + DatabaseAdapterFlavor, + CacheAdapterFlavor, + EntityCompanionDefinition, + Entity, + EntityMutationTrigger, + EntityQueryContext, +} from '@expo/entity'; +import Knex from 'knex'; + +type PostgresValidatorTestEntityFields = { + id: string; + name: string | null; +}; + +export default class PostgresValidatorTestEntity extends Entity< + PostgresValidatorTestEntityFields, + string, + ViewerContext +> { + static getCompanionDefinition(): EntityCompanionDefinition< + PostgresValidatorTestEntityFields, + string, + ViewerContext, + PostgresValidatorTestEntity, + PostgresValidatorTestEntityPrivacyPolicy + > { + return postgresTestEntityCompanionDefinition; + } + + public static async createOrTruncatePostgresTable(knex: Knex): Promise { + await knex.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); // for uuid_generate_v4() + + const tableName = this.getCompanionDefinition().entityConfiguration.tableName; + const hasTable = await knex.schema.hasTable(tableName); + if (!hasTable) { + await knex.schema.createTable(tableName, (table) => { + table.uuid('id').defaultTo(knex.raw('uuid_generate_v4()')).primary(); + table.string('name'); + }); + } + await knex.into(tableName).truncate(); + } + + public static async dropPostgresTable(knex: Knex): Promise { + const tableName = this.getCompanionDefinition().entityConfiguration.tableName; + const hasTable = await knex.schema.hasTable(tableName); + if (hasTable) { + await knex.schema.dropTable(tableName); + } + } +} + +class PostgresValidatorTestEntityPrivacyPolicy extends EntityPrivacyPolicy< + PostgresValidatorTestEntityFields, + string, + ViewerContext, + PostgresValidatorTestEntity +> { + protected readonly createRules = [ + new AlwaysAllowPrivacyPolicyRule< + PostgresValidatorTestEntityFields, + string, + ViewerContext, + PostgresValidatorTestEntity + >(), + ]; + protected readonly readRules = [ + new AlwaysAllowPrivacyPolicyRule< + PostgresValidatorTestEntityFields, + string, + ViewerContext, + PostgresValidatorTestEntity + >(), + ]; + protected readonly updateRules = [ + new AlwaysAllowPrivacyPolicyRule< + PostgresValidatorTestEntityFields, + string, + ViewerContext, + PostgresValidatorTestEntity + >(), + ]; + protected readonly deleteRules = [ + new AlwaysAllowPrivacyPolicyRule< + PostgresValidatorTestEntityFields, + string, + ViewerContext, + PostgresValidatorTestEntity + >(), + ]; +} + +class ThrowConditionallyTrigger extends EntityMutationTrigger< + PostgresValidatorTestEntityFields, + string, + ViewerContext, + PostgresValidatorTestEntity +> { + constructor( + private fieldName: keyof PostgresValidatorTestEntityFields, + private badValue: string + ) { + super(); + } + + async executeAsync( + _viewerContext: ViewerContext, + _queryContext: EntityQueryContext, + entity: PostgresValidatorTestEntity + ): Promise { + if (entity.getField(this.fieldName) === this.badValue) { + throw new Error(`${this.fieldName} cannot have value ${this.badValue}`); + } + } +} + +export const postgresTestEntityConfiguration = new EntityConfiguration< + PostgresValidatorTestEntityFields +>({ + idField: 'id', + tableName: 'postgres_test_entities', + schema: { + id: new UUIDField({ + columnName: 'id', + cache: true, + }), + name: new StringField({ + columnName: 'name', + }), + }, + databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES, + cacheAdapterFlavor: CacheAdapterFlavor.REDIS, +}); + +const postgresTestEntityCompanionDefinition = new EntityCompanionDefinition({ + entityClass: PostgresValidatorTestEntity, + entityConfiguration: postgresTestEntityConfiguration, + privacyPolicyClass: PostgresValidatorTestEntityPrivacyPolicy, + mutationValidators: [new ThrowConditionallyTrigger('name', 'beforeCreateAndBeforeUpdate')], +}); diff --git a/packages/entity/src/__tests__/EntityMutator-test.ts b/packages/entity/src/__tests__/EntityMutator-test.ts index 75652290..404a5eec 100644 --- a/packages/entity/src/__tests__/EntityMutator-test.ts +++ b/packages/entity/src/__tests__/EntityMutator-test.ts @@ -56,6 +56,40 @@ class TestMutationTrigger extends EntityMutationTrigger< ): Promise {} } +const setUpMutationValidatorSpies = ( + mutationValidators: EntityMutationTrigger< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + >[] +): EntityMutationTrigger[] => { + return mutationValidators.map((validator) => spy(validator)); +}; + +const verifyValidatorCounts = ( + viewerContext: ViewerContext, + mutationValidatorSpies: EntityMutationTrigger< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + >[], + expectedCalls: number +): void => { + for (const validator of mutationValidatorSpies) { + verify( + validator.executeAsync( + viewerContext, + anyOfClass(EntityTransactionalQueryContext), + anyOfClass(TestEntity) + ) + ).times(expectedCalls); + } +}; + const setUpMutationTriggerSpies = ( mutationTriggers: EntityMutationTriggerConfiguration< TestFields, @@ -177,6 +211,13 @@ const createEntityMutatorFactory = ( TestEntityPrivacyPolicy >; metricsAdapter: IEntityMetricsAdapter; + mutationValidators: EntityMutationTrigger< + TestFields, + string, + ViewerContext, + TestEntity, + keyof TestFields + >[]; mutationTriggers: EntityMutationTriggerConfiguration< TestFields, string, @@ -185,13 +226,13 @@ const createEntityMutatorFactory = ( keyof TestFields >; } => { - const mutatationValidators: EntityMutationTrigger< + const mutationValidators: EntityMutationTrigger< TestFields, string, ViewerContext, TestEntity, keyof TestFields - >[] = []; + >[] = [new TestMutationTrigger()]; const mutationTriggers: EntityMutationTriggerConfiguration< TestFields, string, @@ -237,7 +278,7 @@ const createEntityMutatorFactory = ( testEntityConfiguration, TestEntity, privacyPolicy, - mutatationValidators, + mutationValidators, mutationTriggers, entityLoaderFactory, databaseAdapter, @@ -248,6 +289,7 @@ const createEntityMutatorFactory = ( entityLoaderFactory, entityMutatorFactory, metricsAdapter, + mutationValidators, mutationTriggers, }; }; @@ -352,6 +394,36 @@ describe(EntityMutatorFactory, () => { afterDelete: false, }); }); + + it('executes validators', async () => { + const viewerContext = mock(); + const queryContext = StubQueryContextProvider.getQueryContext(); + const { mutationValidators, entityMutatorFactory } = createEntityMutatorFactory([ + { + customIdField: 'hello', + stringField: 'huh', + testIndexedField: '4', + numberField: 1, + dateField: new Date(), + }, + { + customIdField: 'world', + stringField: 'huh', + testIndexedField: '5', + numberField: 1, + dateField: new Date(), + }, + ]); + + const validatorSpies = setUpMutationValidatorSpies(mutationValidators); + + await entityMutatorFactory + .forCreate(viewerContext, queryContext) + .setField('stringField', 'huh') + .enforceCreateAsync(); + + verifyValidatorCounts(viewerContext, validatorSpies, 1); + }); }); describe('forUpdate', () => { @@ -482,6 +554,44 @@ describe(EntityMutatorFactory, () => { afterDelete: false, }); }); + it('executes validators', async () => { + const viewerContext = mock(); + const queryContext = StubQueryContextProvider.getQueryContext(); + + const { + mutationValidators, + entityMutatorFactory, + entityLoaderFactory, + } = createEntityMutatorFactory([ + { + customIdField: 'hello', + stringField: 'huh', + testIndexedField: '3', + numberField: 3, + dateField: new Date(), + }, + { + customIdField: 'world', + stringField: 'huh', + testIndexedField: '4', + numberField: 3, + dateField: new Date(), + }, + ]); + + const validatorSpies = setUpMutationValidatorSpies(mutationValidators); + + const existingEntity = await enforceAsyncResult( + entityLoaderFactory.forLoad(viewerContext, queryContext).loadByIDAsync('world') + ); + + await entityMutatorFactory + .forUpdate(existingEntity, queryContext) + .setField('stringField', 'huh2') + .enforceUpdateAsync(); + + verifyValidatorCounts(viewerContext, validatorSpies, 1); + }); }); describe('forDelete', () => { @@ -580,6 +690,34 @@ describe(EntityMutatorFactory, () => { afterDelete: true, }); }); + + it('does not execute validators', async () => { + const viewerContext = mock(); + const queryContext = StubQueryContextProvider.getQueryContext(); + const { + mutationValidators, + entityMutatorFactory, + entityLoaderFactory, + } = createEntityMutatorFactory([ + { + customIdField: 'world', + stringField: 'huh', + testIndexedField: '3', + numberField: 3, + dateField: new Date(), + }, + ]); + + const validatorSpies = setUpMutationValidatorSpies(mutationValidators); + + const existingEntity = await enforceAsyncResult( + entityLoaderFactory.forLoad(viewerContext, queryContext).loadByIDAsync('world') + ); + + await entityMutatorFactory.forDelete(existingEntity, queryContext).enforceDeleteAsync(); + + verifyValidatorCounts(viewerContext, validatorSpies, 0); + }); }); it('invalidates cache for fields upon create', async () => {