Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: entity mutation validators #67

Merged
merged 3 commits into from
Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
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<void> {
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')],
});
10 changes: 9 additions & 1 deletion packages/entity/src/EntityCompanion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IEntityClass } from './Entity';
import EntityLoaderFactory from './EntityLoaderFactory';
import { EntityMutationTriggerConfiguration } from './EntityMutationTrigger';
import { EntityMutationTriggerConfiguration, EntityMutationTrigger } from './EntityMutationTrigger';
import EntityMutatorFactory from './EntityMutatorFactory';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import IEntityQueryContextProvider from './IEntityQueryContextProvider';
Expand Down Expand Up @@ -58,6 +58,13 @@ export default class EntityCompanion<
>,
private readonly tableDataCoordinator: EntityTableDataCoordinator<TFields>,
PrivacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>,
mutationValidators: EntityMutationTrigger<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>[],
mutationTriggers: EntityMutationTriggerConfiguration<
TFields,
TID,
Expand All @@ -78,6 +85,7 @@ export default class EntityCompanion<
tableDataCoordinator.entityConfiguration,
entityClass,
privacyPolicy,
mutationValidators,
mutationTriggers,
this.entityLoaderFactory,
tableDataCoordinator.databaseAdapter,
Expand Down
19 changes: 18 additions & 1 deletion packages/entity/src/EntityCompanionProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IEntityClass } from './Entity';
import EntityCompanion, { IPrivacyPolicyClass } from './EntityCompanion';
import EntityConfiguration from './EntityConfiguration';
import { EntityMutationTriggerConfiguration } from './EntityMutationTrigger';
import { EntityMutationTriggerConfiguration, EntityMutationTrigger } from './EntityMutationTrigger';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import IEntityCacheAdapterProvider from './IEntityCacheAdapterProvider';
import IEntityDatabaseAdapterProvider from './IEntityDatabaseAdapterProvider';
Expand Down Expand Up @@ -81,6 +81,13 @@ export class EntityCompanionDefinition<
>;
readonly entityConfiguration: EntityConfiguration<TFields>;
readonly privacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>;
readonly mutationValidators: EntityMutationTrigger<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>[];
readonly mutationTriggers: EntityMutationTriggerConfiguration<
TFields,
TID,
Expand All @@ -94,6 +101,7 @@ export class EntityCompanionDefinition<
entityClass,
entityConfiguration,
privacyPolicyClass,
mutationValidators = [],
mutationTriggers = {},
entitySelectedFields = Array.from(entityConfiguration.schema.keys()) as TSelectedFields[],
}: {
Expand All @@ -107,6 +115,13 @@ export class EntityCompanionDefinition<
>;
entityConfiguration: EntityConfiguration<TFields>;
privacyPolicyClass: IPrivacyPolicyClass<TPrivacyPolicy>;
mutationValidators?: EntityMutationTrigger<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>[];
mutationTriggers?: EntityMutationTriggerConfiguration<
TFields,
TID,
Expand All @@ -119,6 +134,7 @@ export class EntityCompanionDefinition<
this.entityClass = entityClass;
this.entityConfiguration = entityConfiguration;
this.privacyPolicyClass = privacyPolicyClass;
this.mutationValidators = mutationValidators;
this.mutationTriggers = mutationTriggers;
this.entitySelectedFields = entitySelectedFields;
}
Expand Down Expand Up @@ -201,6 +217,7 @@ export default class EntityCompanionProvider {
entityCompanionDefinition.entityClass,
tableDataCoordinator,
entityCompanionDefinition.privacyPolicyClass,
entityCompanionDefinition.mutationValidators,
entityCompanionDefinition.mutationTriggers,
this.metricsAdapter
);
Expand Down
31 changes: 30 additions & 1 deletion packages/entity/src/EntityMutator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import EntityConfiguration from './EntityConfiguration';
import EntityDatabaseAdapter from './EntityDatabaseAdapter';
import { EntityEdgeDeletionBehavior } from './EntityFields';
import EntityLoaderFactory from './EntityLoaderFactory';
import { EntityMutationTriggerConfiguration, EntityMutationTrigger } from './EntityMutationTrigger';
import { EntityMutationTrigger, EntityMutationTriggerConfiguration } from './EntityMutationTrigger';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import { EntityQueryContext, EntityTransactionalQueryContext } from './EntityQueryContext';
import ReadonlyEntity from './ReadonlyEntity';
Expand Down Expand Up @@ -42,6 +42,13 @@ abstract class BaseMutator<
TSelectedFields
>,
protected readonly privacyPolicy: TPrivacyPolicy,
protected readonly mutationValidators: EntityMutationTrigger<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>[],
protected readonly mutationTriggers: EntityMutationTriggerConfiguration<
TFields,
TID,
Expand Down Expand Up @@ -158,6 +165,11 @@ export class CreateMutator<
return authorizeCreateResult;
}

await this.executeTriggers(
this.mutationValidators,
queryContext,
temporaryEntityForPrivacyCheck
);
await this.executeTriggers(
this.mutationTriggers.beforeAll,
queryContext,
Expand Down Expand Up @@ -220,6 +232,13 @@ export class UpdateMutator<
TSelectedFields
>,
privacyPolicy: TPrivacyPolicy,
mutationValidators: EntityMutationTrigger<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>[],
mutationTriggers: EntityMutationTriggerConfiguration<
TFields,
TID,
Expand All @@ -245,6 +264,7 @@ export class UpdateMutator<
entityConfiguration,
entityClass,
privacyPolicy,
mutationValidators,
mutationTriggers,
entityLoaderFactory,
databaseAdapter,
Expand Down Expand Up @@ -314,6 +334,7 @@ export class UpdateMutator<
return authorizeUpdateResult;
}

await this.executeTriggers(this.mutationValidators, queryContext, entityAboutToBeUpdated);
await this.executeTriggers(
this.mutationTriggers.beforeAll,
queryContext,
Expand Down Expand Up @@ -379,6 +400,13 @@ export class DeleteMutator<
TSelectedFields
>,
privacyPolicy: TPrivacyPolicy,
mutationValidators: EntityMutationTrigger<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>[],
mutationTriggers: EntityMutationTriggerConfiguration<
TFields,
TID,
Expand All @@ -404,6 +432,7 @@ export class DeleteMutator<
entityConfiguration,
entityClass,
privacyPolicy,
mutationValidators,
mutationTriggers,
entityLoaderFactory,
databaseAdapter,
Expand Down
Loading