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!: add support for reference deletion and enclose mutations in transactions #63

Merged
merged 7 commits into from
Jul 22, 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 @@ -47,10 +47,14 @@ describe(RedisCacheAdapter, () => {
)['tableDataCoordinator']['cacheAdapter'];
const cacheKeyMaker = cacheAdapter['makeCacheKey'].bind(cacheAdapter);

// creating an entity should put it in cache (loader loads it after creation)
const entity1 = await enforceAsyncResult(
RedisTestEntity.creator(vc1).setField('name', 'blah').createAsync()
);
const entity1Created = await RedisTestEntity.creator(vc1)
.setField('name', 'blah')
.enforceCreateAsync();

// loading an entity should put it in cache
const entity1 = await RedisTestEntity.loader(vc1)
.enforcing()
.loadByIDAsync(entity1Created.getID());

const cachedJSON = await redisCacheAdapterContext.redisClient.get(
cacheKeyMaker('id', entity1.getID())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import Knex from 'knex';
export default class PostgresEntityQueryContextProvider implements IEntityQueryContextProvider {
constructor(private readonly knexInstance: Knex) {}

getRegularEntityQueryContext(): EntityNonTransactionalQueryContext {
return new EntityNonTransactionalQueryContext(this.knexInstance);
getQueryContext(): EntityNonTransactionalQueryContext {
return new EntityNonTransactionalQueryContext(this.knexInstance, this);
}

async runInTransactionAsync<T>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {
EntityPrivacyPolicy,
ViewerContext,
AlwaysAllowPrivacyPolicyRule,
Entity,
EntityCompanionDefinition,
EntityConfiguration,
DatabaseAdapterFlavor,
CacheAdapterFlavor,
UUIDField,
EntityEdgeDeletionBehavior,
} from '@expo/entity';
import Knex from 'knex';

import { createKnexIntegrationTestEntityCompanionProvider } from '../testfixtures/createKnexIntegrationTestEntityCompanionProvider';

interface ParentFields {
id: string;
}

interface ChildFields {
id: string;
parent_id: string;
}

class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<any, string, ViewerContext, any, any> {
protected readonly readRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly createRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly updateRules = [new AlwaysAllowPrivacyPolicyRule()];
protected readonly deleteRules = [new AlwaysAllowPrivacyPolicyRule()];
}

class ParentEntity extends Entity<ParentFields, string, ViewerContext> {
static getCompanionDefinition(): EntityCompanionDefinition<
ParentFields,
string,
ViewerContext,
ParentEntity,
TestEntityPrivacyPolicy
> {
return parentEntityCompanion;
}
}

class ChildEntity extends Entity<ChildFields, string, ViewerContext> {
static getCompanionDefinition(): EntityCompanionDefinition<
ChildFields,
string,
ViewerContext,
ChildEntity,
TestEntityPrivacyPolicy
> {
return childEntityCompanion;
}
}

const parentEntityConfiguration = new EntityConfiguration<ParentFields>({
idField: 'id',
tableName: 'parents',
inboundEdges: [ChildEntity],
schema: {
id: new UUIDField({
columnName: 'id',
cache: true,
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});

const childEntityConfiguration = new EntityConfiguration<ChildFields>({
idField: 'id',
tableName: 'children',
schema: {
id: new UUIDField({
columnName: 'id',
cache: true,
}),
parent_id: new UUIDField({
columnName: 'parent_id',
cache: true,
association: {
associatedEntityClass: ParentEntity,
edgeDeletionBehavior: EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE,
},
}),
},
databaseAdapterFlavor: DatabaseAdapterFlavor.POSTGRES,
cacheAdapterFlavor: CacheAdapterFlavor.REDIS,
});

const parentEntityCompanion = new EntityCompanionDefinition({
entityClass: ParentEntity,
entityConfiguration: parentEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
});

const childEntityCompanion = new EntityCompanionDefinition({
entityClass: ChildEntity,
entityConfiguration: childEntityConfiguration,
privacyPolicyClass: TestEntityPrivacyPolicy,
});

async function createOrTruncatePostgresTables(knex: Knex): Promise<void> {
await knex.raw('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'); // for uuid_generate_v4()

await knex.schema.createTable('parents', (table) => {
table.uuid('id').defaultTo(knex.raw('uuid_generate_v4()')).primary();
});
await knex.into('parents').truncate();

await knex.schema.createTable('children', (table) => {
table.uuid('id').defaultTo(knex.raw('uuid_generate_v4()')).primary();
table.uuid('parent_id').references('id').inTable('parents').onDelete('cascade').unique();
});
await knex.into('children').truncate();
}

async function dropPostgresTable(knex: Knex): Promise<void> {
if (await knex.schema.hasTable('children')) {
await knex.schema.dropTable('children');
}
if (await knex.schema.hasTable('parents')) {
await knex.schema.dropTable('parents');
}
}

describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
let knexInstance: Knex;

beforeAll(() => {
knexInstance = Knex({
client: 'pg',
connection: {
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
host: 'localhost',
port: parseInt(process.env.PGPORT!, 10),
database: process.env.PGDATABASE,
},
});
});

beforeEach(async () => {
await createOrTruncatePostgresTables(knexInstance);
});

afterAll(async () => {
await dropPostgresTable(knexInstance);
knexInstance.destroy();
});

describe('EntityEdgeDeletionBehavior.INVALIDATE_CACHE', () => {
it('invalidates the cache', async () => {
const viewerContext = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(knexInstance)
);

const parent = await ParentEntity.creator(viewerContext).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext)
.setField('parent_id', parent.getID())
.enforceCreateAsync();

await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_id', parent.getID())
).resolves.not.toBeNull();

await ParentEntity.enforceDeleteAsync(parent);

await expect(
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();

await expect(
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
).resolves.toBeNull();
});
});
});
Loading