Compare commits

...
82 changed files with 1628 additions and 1670 deletions

No files matched your search

+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
* Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
+1 -1
View File
@@ -4,5 +4,5 @@
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.34.0"
"version": "0.35.0"
}
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
* Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-cache-adapter-local-memory",
"version": "0.34.0",
"version": "0.35.0",
"description": "Cross-request local memory cache adapter for @expo/entity",
"files": [
"build",
@@ -33,6 +33,6 @@
"lru-cache": "^6.0.0"
},
"devDependencies": {
"@expo/entity": "^0.34.0"
"@expo/entity": "^0.35.0"
}
}
@@ -25,19 +25,13 @@ describe(GenericLocalMemoryCacher, () => {
const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
const date = new Date();
const entity1Created = await LocalMemoryTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.enforceCreateAsync();
// loading an entity should put it in cache
const entity1 = await LocalMemoryTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1Created.getID());
@@ -69,10 +63,9 @@ describe(GenericLocalMemoryCacher, () => {
// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();
const entityNonExistentResult = await LocalMemoryTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).loadByIDAsync(nonExistentId);
const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
nonExistentId
);
expect(entityNonExistentResult.ok).toBe(false);
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
@@ -84,16 +77,12 @@ describe(GenericLocalMemoryCacher, () => {
// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 = await LocalMemoryTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
viewerContext
).loadByIDAsync(nonExistentId);
expect(entityNonExistentResult2.ok).toBe(false);
// invalidate from cache to ensure it invalidates correctly
await LocalMemoryTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).invalidateFieldsAsync(entity1.getAllFields());
await LocalMemoryTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
const cachedResultMiss = await entitySpecificGenericCacher.loadManyAsync([
cacheKeyMaker('id', entity1.getID()),
]);
@@ -111,19 +100,13 @@ describe(GenericLocalMemoryCacher, () => {
const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
const date = new Date();
const entity1Created = await LocalMemoryTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.enforceCreateAsync();
// loading an entity will try to put it in cache but it's a noop cache, so it should be a miss
const entity1 = await LocalMemoryTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity1 = await LocalMemoryTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1Created.getID());
@@ -150,10 +133,9 @@ describe(GenericLocalMemoryCacher, () => {
// a non existent db fetch should try to write negative result ('') but it's a noop cache, so it should be a miss
const nonExistentId = uuidv4();
const entityNonExistentResult = await LocalMemoryTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).loadByIDAsync(nonExistentId);
const entityNonExistentResult = await LocalMemoryTestEntity.loader(viewerContext).loadByIDAsync(
nonExistentId
);
expect(entityNonExistentResult.ok).toBe(false);
const nonExistentCachedResult = await entitySpecificGenericCacher.loadManyAsync([
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
* Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-cache-adapter-redis",
"version": "0.34.0",
"version": "0.35.0",
"description": "Redis cache adapter for @expo/entity",
"files": [
"build",
@@ -31,7 +31,7 @@
"ioredis": ">=5"
},
"devDependencies": {
"@expo/entity": "^0.34.0",
"@expo/entity": "^0.35.0",
"ioredis": "^5.2.5"
}
}
@@ -101,10 +101,7 @@ describe(GenericRedisCacher, () => {
]['cacheAdapter']['genericCacher'];
const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
const entity1Created = await RedisTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.enforceCreateAsync();
@@ -120,22 +117,9 @@ describe(GenericRedisCacher, () => {
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext)
);
const [entity1, entity2, entity3] = await Promise.all([
RedisTestEntity.loader(
viewerContext1,
viewerContext1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.enforcing()
.loadByIDAsync(entity1Created.getID()),
RedisTestEntity.loader(
viewerContext2,
viewerContext2.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.enforcing()
.loadByIDAsync(entity1Created.getID()),
RedisTestEntity.loader(
viewerContext3,
viewerContext3.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
RedisTestEntity.loader(viewerContext1).enforcing().loadByIDAsync(entity1Created.getID()),
RedisTestEntity.loader(viewerContext2).enforcing().loadByIDAsync(entity1Created.getID()),
RedisTestEntity.loader(viewerContext3)
.enforcing()
.loadByFieldEqualingAsync('name', entity1Created.getField('name')),
]);
@@ -154,36 +138,28 @@ describe(GenericRedisCacher, () => {
});
const cacheKeyEntity1NameField = cacheKeyMaker('name', entity1Created.getField('name'));
await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
await RedisTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('name', entity1Created.getField('name'));
await expect(redis.get(cacheKeyEntity1NameField)).resolves.toEqual(cachedJSON);
// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();
const entityNonExistentResult = await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).loadByIDAsync(nonExistentId);
const entityNonExistentResult = await RedisTestEntity.loader(viewerContext).loadByIDAsync(
nonExistentId
);
expect(entityNonExistentResult.ok).toBe(false);
const cacheKeyNonExistent = cacheKeyMaker('id', nonExistentId);
const nonExistentCachedValue = await redis.get(cacheKeyNonExistent);
expect(nonExistentCachedValue).toEqual('');
// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 = await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).loadByIDAsync(nonExistentId);
const entityNonExistentResult2 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(
nonExistentId
);
expect(entityNonExistentResult2.ok).toBe(false);
// invalidate from cache to ensure it invalidates correctly in both caches
await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).invalidateFieldsAsync(entity1.getAllFields());
await RedisTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
await expect(redis.get(cacheKeyEntity1)).resolves.toBeNull();
await expect(redis.get(cacheKeyEntity1NameField)).resolves.toBeNull();
});
@@ -46,18 +46,12 @@ describe(GenericRedisCacher, () => {
]['cacheAdapter']['genericCacher'];
const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
const entity1Created = await RedisTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.enforceCreateAsync();
// loading an entity should put it in cache
const entity1 = await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity1 = await RedisTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1Created.getID());
@@ -73,10 +67,9 @@ describe(GenericRedisCacher, () => {
// simulate non existent db fetch, should write negative result ('') to cache
const nonExistentId = uuidv4();
const entityNonExistentResult = await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).loadByIDAsync(nonExistentId);
const entityNonExistentResult = await RedisTestEntity.loader(viewerContext).loadByIDAsync(
nonExistentId
);
expect(entityNonExistentResult.ok).toBe(false);
const nonExistentCachedValue = await (genericRedisCacheContext.redisClient as Redis).get(
@@ -85,17 +78,13 @@ describe(GenericRedisCacher, () => {
expect(nonExistentCachedValue).toEqual('');
// load again through entities framework to ensure it reads negative result
const entityNonExistentResult2 = await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).loadByIDAsync(nonExistentId);
const entityNonExistentResult2 = await RedisTestEntity.loader(viewerContext).loadByIDAsync(
nonExistentId
);
expect(entityNonExistentResult2.ok).toBe(false);
// invalidate from cache to ensure it invalidates correctly
await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).invalidateFieldsAsync(entity1.getAllFields());
await RedisTestEntity.loader(viewerContext).invalidateFieldsAsync(entity1.getAllFields());
const cachedValueNull = await (genericRedisCacheContext.redisClient as Redis).get(
cacheKeyMaker('id', entity1.getID())
);
@@ -108,19 +97,11 @@ describe(GenericRedisCacher, () => {
);
const date = new Date();
const entity1 = await enforceAsyncResult(
RedisTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('dateField', date)
.createAsync()
RedisTestEntity.creator(viewerContext).setField('dateField', date).createAsync()
);
expect(entity1.getField('dateField')).toEqual(date);
const entity2 = await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity2 = await RedisTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1.getID());
expect(entity2.getField('dateField')).toEqual(date);
@@ -129,12 +110,7 @@ describe(GenericRedisCacher, () => {
const vc2 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext)
);
const entity3 = await RedisTestEntity.loader(
vc2,
vc2.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.enforcing()
.loadByIDAsync(entity1.getID());
const entity3 = await RedisTestEntity.loader(vc2).enforcing().loadByIDAsync(entity1.getID());
expect(entity3.getField('dateField')).toEqual(date);
});
@@ -143,17 +119,9 @@ describe(GenericRedisCacher, () => {
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext)
);
const entity1 = await enforceAsyncResult(
RedisTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', '')
.createAsync()
RedisTestEntity.creator(viewerContext).setField('name', '').createAsync()
);
const entity2 = await RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity2 = await RedisTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('name', '');
expect(entity2?.getID()).toEqual(entity1.getID());
@@ -162,10 +130,7 @@ describe(GenericRedisCacher, () => {
const vc2 = new TestViewerContext(
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext)
);
const entity3 = await RedisTestEntity.loader(
vc2,
vc2.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity3 = await RedisTestEntity.loader(vc2)
.enforcing()
.loadByFieldEqualingAsync('name', '');
expect(entity3?.getID()).toEqual(entity1.getID());
@@ -46,10 +46,7 @@ describe(GenericRedisCacher, () => {
redisTestEntityConfiguration
);
const date = new Date();
const entity1Created = await RedisTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.enforceCreateAsync();
@@ -95,10 +92,7 @@ describe(GenericRedisCacher, () => {
redisTestEntityConfiguration
);
const date = new Date();
const entity1Created = await RedisTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity1Created = await RedisTestEntity.creator(viewerContext)
.setField('name', 'blah')
.setField('dateField', date)
.enforceCreateAsync();
@@ -40,12 +40,7 @@ describe(GenericRedisCacher, () => {
);
await expect(
RedisTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'blah')
.enforceCreateAsync()
RedisTestEntity.creator(vc1).setField('name', 'blah').enforceCreateAsync()
).rejects.toThrow(EntityCacheAdapterTransientError);
});
});
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
* Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-database-adapter-knex",
"version": "0.34.0",
"version": "0.35.0",
"description": "Knex database adapter for @expo/entity",
"files": [
"build",
@@ -33,7 +33,7 @@
"knex": "^2.4.2"
},
"devDependencies": {
"@expo/entity": "^0.34.0",
"@expo/entity": "^0.35.0",
"pg": "8.10.0"
}
}
@@ -42,34 +42,16 @@ describe('postgres entity integration', () => {
it('supports parallel partial updates', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'hello')
.createAsync()
PostgresTestEntity.creator(vc).setField('name', 'hello').createAsync()
);
// update two different fields at the same time (from the same entity)
await Promise.all([
PostgresTestEntity.updater(
entity,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('hasACat', true)
.updateAsync(),
PostgresTestEntity.updater(
entity,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('hasADog', false)
.updateAsync(),
PostgresTestEntity.updater(entity).setField('hasACat', true).updateAsync(),
PostgresTestEntity.updater(entity).setField('hasADog', false).updateAsync(),
]);
const loadedEntity = await PostgresTestEntity.loader(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const loadedEntity = await PostgresTestEntity.loader(vc)
.enforcing()
.loadByIDAsync(entity.getID());
@@ -80,49 +62,24 @@ describe('postgres entity integration', () => {
describe('empty creates and updates', () => {
it('allows empty create', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).createAsync()
);
const entity = await enforceAsyncResult(PostgresTestEntity.creator(vc).createAsync());
expect(entity.getID()).toBeTruthy();
});
it('throws knex error upon empty update', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'hello')
.createAsync()
PostgresTestEntity.creator(vc).setField('name', 'hello').createAsync()
);
await expect(
PostgresTestEntity.updater(
entity,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).updateAsync()
).rejects.toThrow();
await expect(PostgresTestEntity.updater(entity).updateAsync()).rejects.toThrow();
});
it('throws error upon empty update for stub database adapter to match behavior', async () => {
const vc = new ViewerContext(createUnitTestEntityCompanionProvider());
const entity = await enforceAsyncResult(
PostgresTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'hello')
.createAsync()
PostgresTestEntity.creator(vc).setField('name', 'hello').createAsync()
);
await expect(
PostgresTestEntity.updater(
entity,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).updateAsync()
).rejects.toThrow();
await expect(PostgresTestEntity.updater(entity).updateAsync()).rejects.toThrow();
});
});
@@ -131,20 +88,10 @@ describe('postgres entity integration', () => {
// put one in the DB
const firstEntity = await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'hello')
.createAsync()
PostgresTestEntity.creator(vc1).setField('name', 'hello').createAsync()
);
await enforceAsyncResult(
PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).loadByIDAsync(firstEntity.getID())
);
await enforceAsyncResult(PostgresTestEntity.loader(vc1).loadByIDAsync(firstEntity.getID()));
const errorToThrow = new Error('Intentional error');
@@ -160,10 +107,7 @@ describe('postgres entity integration', () => {
).rejects.toEqual(errorToThrow);
const entities = await enforceResultsAsync(
PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
).loadManyByFieldEqualingAsync('name', 'hello')
PostgresTestEntity.loader(vc1).loadManyByFieldEqualingAsync('name', 'hello')
);
expect(entities).toHaveLength(1);
});
@@ -172,12 +116,7 @@ describe('postgres entity integration', () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const firstEntity = await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'hello')
.createAsync()
PostgresTestEntity.creator(vc1).setField('name', 'hello').createAsync()
);
const loadAndUpdateAsync = async (newName: string): Promise<{ error?: Error }> => {
@@ -216,10 +155,7 @@ describe('postgres entity integration', () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('stringArray', ['hello', 'world'])
.setField('jsonArrayField', ['hello', 'world'])
.createAsync()
@@ -233,10 +169,7 @@ describe('postgres entity integration', () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity = await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('jsonObjectField', { hello: 'world' })
.createAsync()
);
@@ -248,18 +181,12 @@ describe('postgres entity integration', () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity1 = await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('maybeJsonArrayField', ['hello', 'world'])
.createAsync()
);
const entity2 = await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('maybeJsonArrayField', { hello: 'world' })
.createAsync()
);
@@ -274,32 +201,17 @@ describe('postgres entity integration', () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
let entity = await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('bigintField', '72057594037928038')
.createAsync()
PostgresTestEntity.creator(vc1).setField('bigintField', '72057594037928038').createAsync()
);
expect(entity.getField('bigintField')).toEqual('72057594037928038');
entity = await enforceAsyncResult(
PostgresTestEntity.updater(
entity,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('bigintField', '10')
.updateAsync()
PostgresTestEntity.updater(entity).setField('bigintField', '10').updateAsync()
);
expect(entity.getField('bigintField')).toEqual('10');
entity = await enforceAsyncResult(
PostgresTestEntity.updater(
entity,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('bigintField', '-10')
.updateAsync()
PostgresTestEntity.updater(entity).setField('bigintField', '-10').updateAsync()
);
expect(entity.getField('bigintField')).toEqual('-10');
});
@@ -310,10 +222,7 @@ describe('postgres entity integration', () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'hello')
.setField('hasACat', false)
.setField('hasADog', true)
@@ -321,10 +230,7 @@ describe('postgres entity integration', () => {
);
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'world')
.setField('hasACat', false)
.setField('hasADog', true)
@@ -332,20 +238,14 @@ describe('postgres entity integration', () => {
);
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'wat')
.setField('hasACat', false)
.setField('hasADog', false)
.createAsync()
);
const results = await PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([
{
@@ -360,10 +260,7 @@ describe('postgres entity integration', () => {
expect(results).toHaveLength(2);
const results2 = await PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const results2 = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([
{ fieldName: 'hasADog', fieldValues: [true, false] },
@@ -374,37 +271,13 @@ describe('postgres entity integration', () => {
it('supports query modifiers', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'a')
.createAsync()
);
await enforceAsyncResult(PostgresTestEntity.creator(vc1).setField('name', 'a').createAsync());
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'b')
.createAsync()
);
await enforceAsyncResult(PostgresTestEntity.creator(vc1).setField('name', 'b').createAsync());
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'c')
.createAsync()
);
await enforceAsyncResult(PostgresTestEntity.creator(vc1).setField('name', 'c').createAsync());
const results = await PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([], {
limit: 2,
@@ -423,55 +296,37 @@ describe('postgres entity integration', () => {
it('supports null field values', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'a')
.setField('hasADog', true)
.createAsync()
);
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'b')
.setField('hasADog', true)
.createAsync()
);
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', null)
.setField('hasADog', true)
.createAsync()
);
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', null)
.setField('hasADog', false)
.createAsync()
);
const results = await PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: null }]);
expect(results).toHaveLength(2);
expect(results[0]!.getField('name')).toBeNull();
const results2 = await PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const results2 = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync(
[
@@ -496,20 +351,14 @@ describe('postgres entity integration', () => {
it('loads by raw where clause', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'hello')
.setField('hasACat', false)
.setField('hasADog', true)
.createAsync()
);
const results = await PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByRawWhereClauseAsync('name = ?', ['hello']);
@@ -519,10 +368,7 @@ describe('postgres entity integration', () => {
it('throws with invalid where clause', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'hello')
.setField('hasACat', false)
.setField('hasADog', true)
@@ -530,10 +376,7 @@ describe('postgres entity integration', () => {
);
await expect(
PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByRawWhereClauseAsync('invalid_column = ?', ['hello'])
).rejects.toThrow();
@@ -543,39 +386,27 @@ describe('postgres entity integration', () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'a')
.setField('hasADog', true)
.createAsync()
);
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'b')
.setField('hasADog', true)
.createAsync()
);
await enforceAsyncResult(
PostgresTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTestEntity.creator(vc1)
.setField('name', 'c')
.setField('hasADog', true)
.createAsync()
);
const results = await PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
limit: 2,
@@ -591,10 +422,7 @@ describe('postgres entity integration', () => {
expect(results).toHaveLength(2);
expect(results.map((e) => e.getField('name'))).toEqual(['b', 'c']);
const resultsMultipleOrderBy = await PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const resultsMultipleOrderBy = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
orderBy: [
@@ -612,10 +440,7 @@ describe('postgres entity integration', () => {
expect(resultsMultipleOrderBy).toHaveLength(3);
expect(resultsMultipleOrderBy.map((e) => e.getField('name'))).toEqual(['c', 'b', 'a']);
const resultsOrderByRaw = await PostgresTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const resultsOrderByRaw = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByRawWhereClauseAsync('has_a_dog = ?', [true], {
orderByRaw: 'has_a_dog ASC, name DESC',
@@ -634,86 +459,52 @@ describe('postgres entity integration', () => {
);
await expect(
PostgresTriggerTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.creator(vc1)
.setField('name', 'beforeCreate')
.enforceCreateAsync()
).rejects.toThrowError('name cannot have value beforeCreate');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeCreate')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.creator(vc1)
.setField('name', 'afterCreate')
.enforceCreateAsync()
).rejects.toThrowError('name cannot have value afterCreate');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterCreate')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'beforeAll')
.enforceCreateAsync()
PostgresTriggerTestEntity.creator(vc1).setField('name', 'beforeAll').enforceCreateAsync()
).rejects.toThrowError('name cannot have value beforeAll');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeAll')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'afterAll')
.enforceCreateAsync()
PostgresTriggerTestEntity.creator(vc1).setField('name', 'afterAll').enforceCreateAsync()
).rejects.toThrowError('name cannot have value afterAll');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterAll')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.creator(vc1)
.setField('name', 'afterCommit')
.enforceCreateAsync()
).rejects.toThrowError('name cannot have value afterCommit');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterCommit')
).resolves.not.toBeNull();
@@ -726,94 +517,61 @@ describe('postgres entity integration', () => {
createKnexIntegrationTestEntityCompanionProvider(knexInstance)
);
const entity = await PostgresTriggerTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity = await PostgresTriggerTestEntity.creator(vc1)
.setField('name', 'blah')
.enforceCreateAsync();
await expect(
PostgresTriggerTestEntity.updater(
entity,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.updater(entity)
.setField('name', 'beforeUpdate')
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value beforeUpdate');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeUpdate')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.updater(
entity,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.updater(entity)
.setField('name', 'afterUpdate')
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value afterUpdate');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterUpdate')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.updater(
entity,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.updater(entity)
.setField('name', 'beforeAll')
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value beforeAll');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeAll')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.updater(
entity,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.updater(entity)
.setField('name', 'afterAll')
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value afterAll');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterAll')
).resolves.toBeNull();
await expect(
PostgresTriggerTestEntity.updater(
entity,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.updater(entity)
.setField('name', 'afterCommit')
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value afterCommit');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterCommit')
).resolves.not.toBeNull();
@@ -826,44 +584,26 @@ describe('postgres entity integration', () => {
createKnexIntegrationTestEntityCompanionProvider(knexInstance)
);
const entityBeforeDelete = await PostgresTriggerTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entityBeforeDelete = await PostgresTriggerTestEntity.creator(vc1)
.setField('name', 'beforeDelete')
.enforceCreateAsync();
await expect(
PostgresTriggerTestEntity.enforceDeleteAsync(
entityBeforeDelete,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.enforceDeleteAsync(entityBeforeDelete)
).rejects.toThrowError('name cannot have value beforeDelete');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeDelete')
).resolves.not.toBeNull();
const entityAfterDelete = await PostgresTriggerTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entityAfterDelete = await PostgresTriggerTestEntity.creator(vc1)
.setField('name', 'afterDelete')
.enforceCreateAsync();
await expect(
PostgresTriggerTestEntity.enforceDeleteAsync(
entityAfterDelete,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.enforceDeleteAsync(entityAfterDelete)
).rejects.toThrowError('name cannot have value afterDelete');
await expect(
PostgresTriggerTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresTriggerTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'afterDelete')
).resolves.not.toBeNull();
@@ -877,18 +617,12 @@ describe('postgres entity integration', () => {
);
await expect(
PostgresValidatorTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresValidatorTestEntity.creator(vc1)
.setField('name', 'beforeCreateAndBeforeUpdate')
.enforceCreateAsync()
).rejects.toThrowError('name cannot have value beforeCreateAndBeforeUpdate');
await expect(
PostgresValidatorTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresValidatorTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeCreateAndBeforeUpdate')
).resolves.toBeNull();
@@ -900,26 +634,17 @@ describe('postgres entity integration', () => {
createKnexIntegrationTestEntityCompanionProvider(knexInstance)
);
const entity = await PostgresValidatorTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entity = await PostgresValidatorTestEntity.creator(vc1)
.setField('name', 'blah')
.enforceCreateAsync();
await expect(
PostgresValidatorTestEntity.updater(
entity,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresValidatorTestEntity.updater(entity)
.setField('name', 'beforeCreateAndBeforeUpdate')
.enforceUpdateAsync()
).rejects.toThrowError('name cannot have value beforeCreateAndBeforeUpdate');
await expect(
PostgresValidatorTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresValidatorTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'beforeCreateAndBeforeUpdate')
).resolves.toBeNull();
@@ -931,21 +656,12 @@ describe('postgres entity integration', () => {
createKnexIntegrationTestEntityCompanionProvider(knexInstance)
);
const entityToDelete = await PostgresValidatorTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const entityToDelete = await PostgresValidatorTestEntity.creator(vc1)
.setField('name', 'shouldBeDeleted')
.enforceCreateAsync();
await PostgresValidatorTestEntity.enforceDeleteAsync(
entityToDelete,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
);
await PostgresValidatorTestEntity.enforceDeleteAsync(entityToDelete);
await expect(
PostgresValidatorTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
PostgresValidatorTestEntity.loader(vc1)
.enforcing()
.loadByFieldEqualingAsync('name', 'shouldBeDeleted')
).resolves.toBeNull();
@@ -34,20 +34,10 @@ describe(PostgresEntityQueryContextProvider, () => {
it('supports nested transactions', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await PostgresUniqueTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'unique')
.enforceCreateAsync();
await PostgresUniqueTestEntity.creator(vc1).setField('name', 'unique').enforceCreateAsync();
const id = (
await PostgresUniqueTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'wat')
.enforceCreateAsync()
await PostgresUniqueTestEntity.creator(vc1).setField('name', 'wat').enforceCreateAsync()
).getID();
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
@@ -79,12 +69,7 @@ describe(PostgresEntityQueryContextProvider, () => {
.enforceUpdateAsync();
});
const entityLoaded = await PostgresUniqueTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.enforcing()
.loadByIDAsync(id);
const entityLoaded = await PostgresUniqueTestEntity.loader(vc1).enforcing().loadByIDAsync(id);
expect(entityLoaded.getField('name')).toEqual('wat3');
});
@@ -92,12 +77,7 @@ describe(PostgresEntityQueryContextProvider, () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const id = (
await PostgresUniqueTestEntity.creator(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'wat')
.enforceCreateAsync()
await PostgresUniqueTestEntity.creator(vc1).setField('name', 'wat').enforceCreateAsync()
).getID();
await vc1.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', async (queryContext) => {
@@ -115,12 +95,7 @@ describe(PostgresEntityQueryContextProvider, () => {
});
});
const entityLoaded = await PostgresUniqueTestEntity.loader(
vc1,
vc1.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.enforcing()
.loadByIDAsync(id);
const entityLoaded = await PostgresUniqueTestEntity.loader(vc1).enforcing().loadByIDAsync(id);
expect(entityLoaded.getField('name')).toEqual('wat3');
});
});
@@ -35,86 +35,40 @@ describe('postgres entity integration', () => {
it('throws after deletion of multiple rows or no rows', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity1 = await enforceAsyncResult(
InvalidTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('id', 1)
.setField('name', 'hello')
.createAsync()
InvalidTestEntity.creator(vc).setField('id', 1).setField('name', 'hello').createAsync()
);
await enforceAsyncResult(
InvalidTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('id', 1)
.setField('name', 'world')
.createAsync()
InvalidTestEntity.creator(vc).setField('id', 1).setField('name', 'world').createAsync()
);
await expect(
InvalidTestEntity.deleteAsync(
entity1,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
).rejects.toThrowError('Excessive deletions from database adapter delete');
await expect(InvalidTestEntity.deleteAsync(entity1)).rejects.toThrowError(
'Excessive deletions from database adapter delete'
);
});
it('throws after update of multiple rows', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity1 = await enforceAsyncResult(
InvalidTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('id', 1)
.setField('name', 'hello')
.createAsync()
InvalidTestEntity.creator(vc).setField('id', 1).setField('name', 'hello').createAsync()
);
await enforceAsyncResult(
InvalidTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('id', 1)
.setField('name', 'world')
.createAsync()
InvalidTestEntity.creator(vc).setField('id', 1).setField('name', 'world').createAsync()
);
await expect(
InvalidTestEntity.updater(
entity1,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'blah')
.enforceUpdateAsync()
InvalidTestEntity.updater(entity1).setField('name', 'blah').enforceUpdateAsync()
).rejects.toThrowError('Excessive results from database adapter update');
});
it('throws after update of no rows', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
const entity1 = await enforceAsyncResult(
InvalidTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('id', 1)
.setField('name', 'hello')
.createAsync()
);
await InvalidTestEntity.deleteAsync(
entity1,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
InvalidTestEntity.creator(vc).setField('id', 1).setField('name', 'hello').createAsync()
);
await InvalidTestEntity.deleteAsync(entity1);
await expect(
InvalidTestEntity.updater(
entity1,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.setField('name', 'blah')
.enforceUpdateAsync()
InvalidTestEntity.updater(entity1).setField('name', 'blah').enforceUpdateAsync()
).rejects.toThrowError('Empty results from database adapter update');
});
});
@@ -41,10 +41,7 @@ describe('postgres errors', () => {
it('throws EntityDatabaseAdapterTransientError on Knex timeout', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
await ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.enforceCreateAsync();
@@ -63,24 +60,16 @@ describe('postgres errors', () => {
const vc2 = new ViewerContext(
createKnexIntegrationTestEntityCompanionProvider(shortTimeoutKnexInstance)
);
await expect(
ErrorsTestEntity.loader(
vc2,
vc2.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
.enforcing()
.loadByIDAsync(1)
).rejects.toThrow(EntityDatabaseAdapterTransientError);
await expect(ErrorsTestEntity.loader(vc2).enforcing().loadByIDAsync(1)).rejects.toThrow(
EntityDatabaseAdapterTransientError
);
await shortTimeoutKnexInstance.destroy();
});
it('throws EntityDatabaseAdapterNotNullConstraintError when not null is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', null as any)
.enforceCreateAsync()
@@ -90,10 +79,7 @@ describe('postgres errors', () => {
it('throws EntityDatabaseAdapterForeignKeyConstraintError when foreign key is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('fieldForeignKey', 2)
@@ -104,19 +90,13 @@ describe('postgres errors', () => {
it('throws EntityDatabaseAdapterUniqueConstraintError when primary key unique constraint is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
await ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.enforceCreateAsync();
await expect(
ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.enforceCreateAsync()
@@ -125,20 +105,14 @@ describe('postgres errors', () => {
it('throws EntityDatabaseAdapterUniqueConstraintError when unique constraint is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
await ErrorsTestEntity.creator(vc)
.setField('id', 2)
.setField('fieldNonNull', 'hello')
.setField('fieldUnique', 'hello')
.enforceCreateAsync();
await expect(
ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('fieldUnique', 'hello')
@@ -149,10 +123,7 @@ describe('postgres errors', () => {
it('throws EntityDatabaseAdapterCheckConstraintError when check constraint is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('checkLessThan5', 2)
@@ -160,10 +131,7 @@ describe('postgres errors', () => {
).resolves.toBeTruthy();
await expect(
ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
ErrorsTestEntity.creator(vc)
.setField('id', 2)
.setField('fieldNonNull', 'hello')
.setField('checkLessThan5', 10)
@@ -174,10 +142,7 @@ describe('postgres errors', () => {
it('throws EntityDatabaseAdapterExclusionConstraintError when exclusion constraint is violated', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('fieldExclusion', 'what')
@@ -185,10 +150,7 @@ describe('postgres errors', () => {
).resolves.toBeTruthy();
await expect(
ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
ErrorsTestEntity.creator(vc)
.setField('id', 2)
.setField('fieldNonNull', 'hello')
.setField('fieldExclusion', 'what')
@@ -199,10 +161,7 @@ describe('postgres errors', () => {
it('throws EntityDatabaseAdapterUnknownError otherwise', async () => {
const vc = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await expect(
ErrorsTestEntity.creator(
vc,
vc.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
ErrorsTestEntity.creator(vc)
.setField('id', 1)
.setField('fieldNonNull', 'hello')
.setField('nonExistentColumn', 'what')
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
* Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@expo/entity-example",
"private": true,
"version": "0.34.0",
"version": "0.35.0",
"description": "An example integration of the @expo/entity framework",
"scripts": {
"tsc": "tsc",
@@ -25,7 +25,7 @@
"dependencies": {
"@apollo/server": "^4.7.3",
"@as-integrations/koa": "^1.1.1",
"@expo/entity": "^0.34.0",
"@expo/entity": "^0.35.0",
"@koa/cors": "^4.0.0",
"@koa/router": "^12.0.0",
"graphql": "^16.6.0",
@@ -10,20 +10,14 @@ describe(NoteEntity, () => {
const userId = uuidv4();
const viewerContext = new UserViewerContext(companionProvider, userId);
const createdEntityResult = await NoteEntity.creator(
viewerContext,
viewerContext.getQueryContext()
)
const createdEntityResult = await NoteEntity.creator(viewerContext)
.setField('userID', userId)
.setField('body', 'image')
.setField('title', 'page')
.createAsync();
expect(createdEntityResult.ok).toBe(true);
const createdEntityResultImpersonate = await NoteEntity.creator(
viewerContext,
viewerContext.getQueryContext()
)
const createdEntityResultImpersonate = await NoteEntity.creator(viewerContext)
.setField('userID', uuidv4()) // a different user
.setField('body', 'image')
.setField('title', 'page')
@@ -1,9 +1,10 @@
import {
PrivacyPolicyRule,
ReadonlyEntity,
EntityQueryContext,
RuleEvaluationResult,
EntityPrivacyPolicyEvaluationContext,
RuleComplexity,
AllowOrSkipPrivacyPolicyRule,
} from '@expo/entity';
import { ExampleViewerContext } from '../viewerContexts';
@@ -27,17 +28,19 @@ export default class AllowIfUserOwnerPrivacyRule<
TID extends NonNullable<TFields[TSelectedFields]>,
TEntity extends ReadonlyEntity<TFields, TID, ExampleViewerContext>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, ExampleViewerContext, TEntity> {
> extends AllowOrSkipPrivacyPolicyRule<TFields, TID, ExampleViewerContext, TEntity> {
constructor(private readonly entityOwnerField: keyof TFields) {
super();
}
override complexity = RuleComplexity.LOW;
async evaluateAsync(
viewerContext: ExampleViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: TEntity
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.ALLOW | RuleEvaluationResult.SKIP> {
if (viewerContext.isUserViewerContext()) {
if (String(entity.getField(this.entityOwnerField)) === viewerContext.userID) {
return RuleEvaluationResult.ALLOW;
@@ -19,7 +19,7 @@ router.get('/', async (ctx) => {
const viewerContext = ctx.state.viewerContext;
let notes: readonly NoteEntity[] = [];
if (viewerContext.isUserViewerContext()) {
notes = await NoteEntity.loader(viewerContext, viewerContext.getQueryContext())
notes = await NoteEntity.loader(viewerContext)
.enforcing()
.loadManyByFieldEqualingAsync('userID', viewerContext.userID);
}
@@ -30,10 +30,7 @@ router.get('/', async (ctx) => {
router.get('/:id', async (ctx) => {
const viewerContext = ctx.state.viewerContext;
const noteResult = await NoteEntity.loader(
viewerContext,
viewerContext.getQueryContext()
).loadByIDAsync(ctx.params['id']!);
const noteResult = await NoteEntity.loader(viewerContext).loadByIDAsync(ctx.params['id']!);
if (!noteResult.ok) {
ctx.throw(403, noteResult.reason);
return;
@@ -56,7 +53,7 @@ router.post('/', async (ctx) => {
const { title, body } = ctx.request.body as any;
const createResult = await NoteEntity.creator(viewerContext, viewerContext.getQueryContext())
const createResult = await NoteEntity.creator(viewerContext)
.setField('userID', viewerContext.userID)
.setField('title', title)
.setField('body', body)
@@ -75,41 +72,44 @@ router.put('/:id', async (ctx) => {
const viewerContext = ctx.state.viewerContext;
const { title, body } = ctx.request.body as any;
try {
const updatedNote = await viewerContext.runInTransactionAsync(async (queryContext) => {
const note = await NoteEntity.loader(viewerContext, queryContext)
.enforcing()
.loadByIDAsync(ctx.params['id']!);
return await NoteEntity.updater(note, queryContext)
.setField('title', title)
.setField('body', body)
.enforceUpdateAsync();
});
ctx.body = {
note: updatedNote.getAllFields(),
};
} catch (e: any) {
ctx.throw(403, e.message);
const noteLoadResult = await NoteEntity.loader(viewerContext).loadByIDAsync(ctx.params['id']!);
if (!noteLoadResult.ok) {
ctx.throw(403, noteLoadResult.reason);
return;
}
const noteUpdateResult = await NoteEntity.updater(noteLoadResult.value)
.setField('title', title)
.setField('body', body)
.updateAsync();
if (!noteUpdateResult.ok) {
ctx.throw(403, noteUpdateResult.reason);
return;
}
ctx.body = {
note: noteUpdateResult.value.getAllFields(),
};
});
router.delete('/:id', async (ctx) => {
const viewerContext = ctx.state.viewerContext;
try {
await viewerContext.runInTransactionAsync(async (queryContext) => {
const note = await NoteEntity.loader(viewerContext, queryContext)
.enforcing()
.loadByIDAsync(ctx.params['id']!);
await NoteEntity.enforceDeleteAsync(note, queryContext);
});
ctx.body = {
status: 'ok',
};
} catch (e: any) {
ctx.throw(403, e.message);
const noteLoadResult = await NoteEntity.loader(viewerContext).loadByIDAsync(ctx.params['id']!);
if (!noteLoadResult.ok) {
ctx.throw(403, noteLoadResult.reason);
return;
}
const noteDeleteResult = await NoteEntity.deleteAsync(noteLoadResult.value);
if (!noteDeleteResult.ok) {
ctx.throw(403, noteDeleteResult.reason);
return;
}
ctx.body = {
status: 'ok',
};
});
export default router;
+15 -21
View File
@@ -46,16 +46,14 @@ export const resolvers: IResolvers<any, GraphqlContext> = {
return viewerContext.userID;
},
async noteByID(_root, args, { viewerContext }) {
return await NoteEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDAsync(args.id);
return await NoteEntity.loader(viewerContext).enforcing().loadByIDAsync(args.id);
},
} as IObjectTypeResolver<any, GraphqlContext>,
User: {
id: (root) => root,
async notes(root, _args, { viewerContext }) {
return await NoteEntity.loader(viewerContext, viewerContext.getQueryContext())
return await NoteEntity.loader(viewerContext)
.enforcing()
.loadManyByFieldEqualingAsync('userID', root);
},
@@ -74,31 +72,27 @@ export const resolvers: IResolvers<any, GraphqlContext> = {
throw new Error('not logged in');
}
return await NoteEntity.creator(viewerContext, viewerContext.getQueryContext())
return await NoteEntity.creator(viewerContext)
.setField('userID', viewerContext.userID)
.setField('title', args.note.title)
.setField('body', args.note.body)
.enforceCreateAsync();
},
async updateNote(_root, args, { viewerContext }) {
return await viewerContext.runInTransactionAsync(async (queryContext) => {
const existingNote = await NoteEntity.loader(viewerContext, queryContext)
.enforcing()
.loadByIDAsync(args.id);
return await NoteEntity.updater(existingNote, queryContext)
.setField('title', args.note.title)
.setField('body', args.note.body)
.enforceUpdateAsync();
});
const existingNote = await NoteEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(args.id);
return await NoteEntity.updater(existingNote)
.setField('title', args.note.title)
.setField('body', args.note.body)
.enforceUpdateAsync();
},
async deleteNote(_root, args, { viewerContext }) {
return await viewerContext.runInTransactionAsync(async (queryContext) => {
const existingNote = await NoteEntity.loader(viewerContext, queryContext)
.enforcing()
.loadByIDAsync(args.id);
await NoteEntity.enforceDeleteAsync(existingNote, queryContext);
return existingNote;
});
const existingNote = await NoteEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(args.id);
await NoteEntity.enforceDeleteAsync(existingNote);
return existingNote;
},
} as IObjectTypeResolver<any, GraphqlContext>,
};
+1 -16
View File
@@ -1,9 +1,4 @@
import {
ViewerContext,
EntityCompanionProvider,
EntityQueryContext,
EntityTransactionalQueryContext,
} from '@expo/entity';
import { ViewerContext, EntityCompanionProvider } from '@expo/entity';
/**
* A base class for better typing Entities and Privacy Policies specific to this application.
@@ -16,16 +11,6 @@ export abstract class ExampleViewerContext extends ViewerContext {
isAnonymousViewerContext(): this is AnonymousViewerContext {
return this instanceof AnonymousViewerContext;
}
public getQueryContext(): EntityQueryContext {
return super.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres');
}
public async runInTransactionAsync<TResult>(
transactionScope: (queryContext: EntityTransactionalQueryContext) => Promise<TResult>
): Promise<TResult> {
return await super.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', transactionScope);
}
}
/**
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
* Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
@@ -1,7 +1,7 @@
{
"name": "@expo/entity-full-integration-tests",
"private": true,
"version": "0.34.0",
"version": "0.35.0",
"description": "Full redis and knex integration tests for the entity framework",
"scripts": {
"tsc": "tsc",
@@ -27,8 +27,8 @@
"@expo/entity-database-adapter-knex": "*"
},
"devDependencies": {
"@expo/entity": "^0.34.0",
"@expo/entity-cache-adapter-redis": "^0.34.0",
"@expo/entity-database-adapter-knex": "^0.34.0"
"@expo/entity": "^0.35.0",
"@expo/entity-cache-adapter-redis": "^0.35.0",
"@expo/entity-database-adapter-knex": "^0.35.0"
}
}
@@ -1,5 +1,6 @@
import {
EntityPrivacyPolicy,
ViewerContext,
AlwaysAllowPrivacyPolicyRule,
Entity,
EntityCompanionDefinition,
@@ -13,7 +14,6 @@ import { knex, Knex } from 'knex';
import nullthrows from 'nullthrows';
import { URL } from 'url';
import TestViewerContext from './entities/TestViewerContext';
import { createFullIntegrationTestEntityCompanionProvider } from '../testfixtures/createFullIntegrationTestEntityCompanionProvider';
interface TestFields {
@@ -25,28 +25,28 @@ interface TestFields {
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity
> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, string, TestViewerContext, TestEntity>(),
new AlwaysAllowPrivacyPolicyRule<TestFields, string, ViewerContext, TestEntity>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, string, TestViewerContext, TestEntity>(),
new AlwaysAllowPrivacyPolicyRule<TestFields, string, ViewerContext, TestEntity>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, string, TestViewerContext, TestEntity>(),
new AlwaysAllowPrivacyPolicyRule<TestFields, string, ViewerContext, TestEntity>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, string, TestViewerContext, TestEntity>(),
new AlwaysAllowPrivacyPolicyRule<TestFields, string, ViewerContext, TestEntity>(),
];
}
class TestEntity extends Entity<TestFields, string, TestViewerContext> {
class TestEntity extends Entity<TestFields, string, ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
TestEntityPrivacyPolicy
> {
@@ -136,20 +136,18 @@ describe('Entity cache inconsistency', () => {
});
test('lots of updates in long-ish running transactions', async () => {
const viewerContext = new TestViewerContext(
const viewerContext = new ViewerContext(
createFullIntegrationTestEntityCompanionProvider(knexInstance, genericRedisCacheContext)
);
const entity1 = await TestEntity.creator(viewerContext, viewerContext.getQueryContext())
const entity1 = await TestEntity.creator(viewerContext)
.setField('other_string', 'hello')
.setField('third_string', 'initial')
.enforceCreateAsync();
// put entities in cache and dataloader
await TestEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDAsync(entity1.getID());
await TestEntity.loader(viewerContext, viewerContext.getQueryContext())
await TestEntity.loader(viewerContext).enforcing().loadByIDAsync(entity1.getID());
await TestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('other_string', 'hello');
@@ -166,17 +164,15 @@ describe('Entity cache inconsistency', () => {
await Promise.all([
// do a load after the transaction below updates the entity but before transaction commits to ensure
// that the cache is cleared after the transaction commits rather than in the middle where the changes
// may not be visible by other requests (TestViewerContexts) which would cache the incorrect
// may not be visible by other requests (ViewerContexts) which would cache the incorrect
// value during the read-through cache.
(async () => {
await barrier1;
const viewerContextInternal = new TestViewerContext(
const viewerContextInternal = new ViewerContext(
createFullIntegrationTestEntityCompanionProvider(knexInstance, genericRedisCacheContext)
);
await TestEntity.loader(viewerContextInternal, viewerContextInternal.getQueryContext())
.enforcing()
.loadByIDAsync(entity1.getID());
await TestEntity.loader(viewerContextInternal).enforcing().loadByIDAsync(entity1.getID());
openBarrier2!();
})(),
@@ -196,20 +192,14 @@ describe('Entity cache inconsistency', () => {
]);
// ensure cache consistency
const viewerContextLast = new TestViewerContext(
const viewerContextLast = new ViewerContext(
createFullIntegrationTestEntityCompanionProvider(knexInstance, genericRedisCacheContext)
);
const loadedById = await TestEntity.loader(
viewerContextLast,
viewerContextLast.getQueryContext()
)
const loadedById = await TestEntity.loader(viewerContextLast)
.enforcing()
.loadByIDAsync(entity1.getID());
const loadedByField = await TestEntity.loader(
viewerContextLast,
viewerContextLast.getQueryContext()
)
const loadedByField = await TestEntity.loader(viewerContextLast)
.enforcing()
.loadByFieldEqualingAsync('other_string', 'hello');
@@ -1,3 +1,4 @@
import { ViewerContext } from '@expo/entity';
import { GenericRedisCacheContext } from '@expo/entity-cache-adapter-redis';
import Redis from 'ioredis';
import { knex, Knex } from 'knex';
@@ -6,7 +7,6 @@ import { URL } from 'url';
import ChildEntity from './entities/ChildEntity';
import ParentEntity from './entities/ParentEntity';
import TestViewerContext from './entities/TestViewerContext';
import { createFullIntegrationTestEntityCompanionProvider } from '../testfixtures/createFullIntegrationTestEntityCompanionProvider';
async function createOrTruncatePostgresTables(knex: Knex): Promise<void> {
@@ -75,41 +75,32 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
describe('EntityEdgeDeletionBehavior.INVALIDATE_CACHE', () => {
it('invalidates the cache', async () => {
const viewerContext = new TestViewerContext(
const viewerContext = new ViewerContext(
createFullIntegrationTestEntityCompanionProvider(knexInstance, genericRedisCacheContext)
);
const parent = await ParentEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext, viewerContext.getQueryContext())
const parent = await ParentEntity.creator(viewerContext).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext)
.setField('parent_id', parent.getID())
.enforceCreateAsync();
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
ChildEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_id', parent.getID())
).resolves.not.toBeNull();
await ParentEntity.enforceDeleteAsync(parent, viewerContext.getQueryContext());
await ParentEntity.enforceDeleteAsync(parent);
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();
await expect(
ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
).resolves.toBeNull();
});
});
@@ -15,7 +15,6 @@ import nullthrows from 'nullthrows';
import { URL } from 'url';
import { v4 as uuidv4 } from 'uuid';
import TestViewerContext from './entities/TestViewerContext';
import { createFullIntegrationTestEntityCompanionProvider } from '../testfixtures/createFullIntegrationTestEntityCompanionProvider';
interface CategoryFields {
@@ -218,53 +217,42 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
edgeDeletionBehavior
);
const viewerContext = new TestViewerContext(
const viewerContext = new ViewerContext(
createFullIntegrationTestEntityCompanionProvider(knexInstance, genericRedisCacheContext)
);
const category1 = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const other1 = await OtherEntity.creator(viewerContext, viewerContext.getQueryContext())
const category1 = await CategoryEntity.creator(viewerContext).enforceCreateAsync();
const other1 = await OtherEntity.creator(viewerContext)
.setField('parent_category_id', category1.getID())
.enforceCreateAsync();
await CategoryEntity.updater(category1, viewerContext.getQueryContext())
await CategoryEntity.updater(category1)
.setField('parent_other_id', other1.getID())
.enforceUpdateAsync();
await CategoryEntity.enforceDeleteAsync(category1, viewerContext.getQueryContext());
await CategoryEntity.enforceDeleteAsync(category1);
if (edgeDeletionBehavior === EntityEdgeDeletionBehavior.SET_NULL) {
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(category1.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(category1.getID())
).resolves.toBeNull();
const otherLoaded = await OtherEntity.loader(viewerContext, viewerContext.getQueryContext())
const otherLoaded = await OtherEntity.loader(viewerContext)
.enforcing()
.loadByIDNullableAsync(other1.getID());
expect(otherLoaded?.getField('parent_category_id')).toBeNull();
} else if (edgeDeletionBehavior === EntityEdgeDeletionBehavior.SET_NULL_INVALIDATE_CACHE_ONLY) {
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(category1.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(category1.getID())
).resolves.toBeNull();
const otherLoaded = await OtherEntity.loader(viewerContext, viewerContext.getQueryContext())
const otherLoaded = await OtherEntity.loader(viewerContext)
.enforcing()
.loadByIDNullableAsync(other1.getID());
expect(otherLoaded?.getField('parent_category_id')).toBeNull();
} else {
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(category1.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(category1.getID())
).resolves.toBeNull();
await expect(
OtherEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(other1.getID())
OtherEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(other1.getID())
).resolves.toBeNull();
}
});
@@ -6,42 +6,36 @@ import {
EntityEdgeDeletionBehavior,
EntityPrivacyPolicy,
UUIDField,
ViewerContext,
} from '@expo/entity';
import ParentEntity from './ParentEntity';
import TestViewerContext from './TestViewerContext';
interface ChildFields {
id: string;
parent_id: string;
}
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
any,
string,
TestViewerContext,
any,
any
> {
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<any, string, ViewerContext, any, any> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
}
export default class ChildEntity extends Entity<ChildFields, string, TestViewerContext> {
export default class ChildEntity extends Entity<ChildFields, string, ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
ChildFields,
string,
TestViewerContext,
ViewerContext,
ChildEntity,
TestEntityPrivacyPolicy
> {
@@ -5,41 +5,35 @@ import {
EntityConfiguration,
EntityPrivacyPolicy,
UUIDField,
ViewerContext,
} from '@expo/entity';
import ChildEntity from './ChildEntity';
import TestViewerContext from './TestViewerContext';
interface ParentFields {
id: string;
}
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
any,
string,
TestViewerContext,
any,
any
> {
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<any, string, ViewerContext, any, any> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
}
export default class ParentEntity extends Entity<ParentFields, string, TestViewerContext> {
export default class ParentEntity extends Entity<ParentFields, string, ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
ParentFields,
string,
TestViewerContext,
ViewerContext,
ParentEntity,
TestEntityPrivacyPolicy
> {
@@ -1,13 +0,0 @@
import { EntityQueryContext, EntityTransactionalQueryContext, ViewerContext } from '@expo/entity';
export default class TestViewerContext extends ViewerContext {
public getQueryContext(): EntityQueryContext {
return super.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres');
}
public async runInTransactionAsync<TResult>(
transactionScope: (queryContext: EntityTransactionalQueryContext) => Promise<TResult>
): Promise<TResult> {
return await super.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', transactionScope);
}
}
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
**Note:** Version bump only for package @expo/entity-ip-address-field
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
**Note:** Version bump only for package @expo/entity-ip-address-field
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-ip-address-field",
"version": "0.34.0",
"version": "0.35.0",
"description": "IP address EntityField definitions for @expo/entity",
"files": [
"build",
@@ -28,7 +28,7 @@
"ip-address": "^8.1.0"
},
"devDependencies": {
"@expo/entity": "^0.34.0",
"@expo/entity": "^0.35.0",
"@types/jsbn": "^1.2.29"
},
"peerDependencies": {
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
* Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-secondary-cache-local-memory",
"version": "0.34.0",
"version": "0.35.0",
"description": "Local memory secondary cache for @expo/entity",
"files": [
"build",
@@ -31,8 +31,8 @@
"@expo/entity-cache-adapter-local-memory": "*"
},
"devDependencies": {
"@expo/entity": "^0.34.0",
"@expo/entity-cache-adapter-local-memory": "^0.34.0",
"@expo/entity": "^0.35.0",
"@expo/entity-cache-adapter-local-memory": "^0.35.0",
"nullthrows": "^1.1.1"
}
}
@@ -172,10 +172,7 @@ describe(LocalMemorySecondaryEntityCache, () => {
it('Loads through secondary loader, caches, and invalidates', async () => {
const viewerContext = new TestViewerContext(createTestEntityCompanionProvider());
const createdEntity = await LocalMemoryTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const createdEntity = await LocalMemoryTestEntity.creator(viewerContext)
.setField('name', 'wat')
.enforceCreateAsync();
@@ -184,10 +181,7 @@ describe(LocalMemorySecondaryEntityCache, () => {
localMemoryTestEntityConfiguration,
GenericLocalMemoryCacher.createLRUCache<LocalMemoryTestEntityFields>({})
),
LocalMemoryTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
LocalMemoryTestEntity.loader(viewerContext)
);
const loadParams = { id: createdEntity.getID() };
@@ -223,10 +217,7 @@ describe(LocalMemorySecondaryEntityCache, () => {
localMemoryTestEntityConfiguration,
GenericLocalMemoryCacher.createLRUCache<LocalMemoryTestEntityFields>({})
),
LocalMemoryTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
LocalMemoryTestEntity.loader(viewerContext)
);
const loadParams = { id: FAKE_ID };
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
* Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
@@ -1,6 +1,6 @@
{
"name": "@expo/entity-secondary-cache-redis",
"version": "0.34.0",
"version": "0.35.0",
"description": "Redis secondary cache for @expo/entity",
"files": [
"build",
@@ -31,8 +31,8 @@
"@expo/entity-cache-adapter-redis": "*"
},
"devDependencies": {
"@expo/entity": "^0.34.0",
"@expo/entity-cache-adapter-redis": "^0.34.0",
"@expo/entity": "^0.35.0",
"@expo/entity-cache-adapter-redis": "^0.35.0",
"ioredis": "^5.2.5",
"nullthrows": "^1.1.1"
}
@@ -79,10 +79,7 @@ describe(RedisSecondaryEntityCache, () => {
createRedisIntegrationTestEntityCompanionProvider(genericRedisCacheContext)
);
const createdEntity = await RedisTestEntity.creator(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
const createdEntity = await RedisTestEntity.creator(viewerContext)
.setField('name', 'wat')
.enforceCreateAsync();
@@ -92,10 +89,7 @@ describe(RedisSecondaryEntityCache, () => {
genericRedisCacheContext,
(loadParams) => `test-key-${loadParams.id}`
),
RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
RedisTestEntity.loader(viewerContext)
);
const loadParams = { id: createdEntity.getID() };
@@ -134,10 +128,7 @@ describe(RedisSecondaryEntityCache, () => {
genericRedisCacheContext,
(loadParams) => `test-key-${loadParams.id}`
),
RedisTestEntity.loader(
viewerContext,
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres')
)
RedisTestEntity.loader(viewerContext)
);
const loadParams = { id: FAKE_ID };
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.35.0](https://github.com/expo/entity/compare/v0.34.0...v0.35.0) (2024-04-11)
### Bug Fixes
* Revert: require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([1bdfbd6](https://github.com/expo/entity/commit/1bdfbd6b562d1200e4029df0533b5adb2d917831))
# [0.34.0](https://github.com/expo/entity/compare/v0.33.0...v0.34.0) (2024-04-11)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@expo/entity",
"version": "0.34.0",
"version": "0.35.0",
"description": "A privacy-first data model",
"files": [
"build",
+29 -6
View File
@@ -61,7 +61,10 @@ export default abstract class Entity<
TMSelectedFields
>,
viewerContext: TMViewerContext2,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = viewerContext
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getQueryContext()
): CreateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy, TMSelectedFields> {
return viewerContext
.getViewerScopedEntityCompanionForClass(this)
@@ -98,7 +101,11 @@ export default abstract class Entity<
TMSelectedFields
>,
existingEntity: TMEntity,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getQueryContext()
): UpdateMutator<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy, TMSelectedFields> {
return existingEntity
.getViewerContext()
@@ -135,7 +142,11 @@ export default abstract class Entity<
TMSelectedFields
>,
existingEntity: TMEntity,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getQueryContext()
): Promise<Result<void>> {
return existingEntity
.getViewerContext()
@@ -173,7 +184,11 @@ export default abstract class Entity<
TMSelectedFields
>,
existingEntity: TMEntity,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getQueryContext()
): Promise<void> {
return existingEntity
.getViewerContext()
@@ -222,7 +237,11 @@ export default abstract class Entity<
TMSelectedFields
>,
existingEntity: TMEntity,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getQueryContext()
): Promise<boolean> {
const companion = existingEntity
.getViewerContext()
@@ -272,7 +291,11 @@ export default abstract class Entity<
TMSelectedFields
>,
existingEntity: TMEntity,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = existingEntity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getQueryContext()
): Promise<boolean> {
const companion = existingEntity
.getViewerContext()
+25 -9
View File
@@ -55,7 +55,11 @@ export default class EntityAssociationLoader<
TAssociatedPrivacyPolicy,
TAssociatedSelectedFields
>,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getQueryContextProvider()
.getQueryContext()
): Promise<
Result<null extends TFields[TIdentifyingField] ? TAssociatedEntity | null : TAssociatedEntity>
> {
@@ -113,7 +117,11 @@ export default class EntityAssociationLoader<
TAssociatedSelectedFields
>,
associatedEntityFieldContainingThisID: keyof Pick<TAssociatedFields, TAssociatedSelectedFields>,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getQueryContextProvider()
.getQueryContext()
): Promise<readonly Result<TAssociatedEntity>[]> {
const thisID = this.entity.getID();
const loader = this.entity
@@ -163,7 +171,11 @@ export default class EntityAssociationLoader<
TAssociatedSelectedFields
>,
associatedEntityLookupByField: keyof Pick<TAssociatedFields, TAssociatedSelectedFields>,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getQueryContextProvider()
.getQueryContext()
): Promise<Result<TAssociatedEntity> | null> {
const associatedFieldValue = this.entity.getField(fieldIdentifyingAssociatedEntity);
if (!associatedFieldValue) {
@@ -216,7 +228,11 @@ export default class EntityAssociationLoader<
TAssociatedSelectedFields
>,
associatedEntityLookupByField: keyof Pick<TAssociatedFields, TAssociatedSelectedFields>,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = this.entity
.getViewerContext()
.getViewerScopedEntityCompanionForClass(associatedEntityClass)
.getQueryContextProvider()
.getQueryContext()
): Promise<readonly Result<TAssociatedEntity>[]> {
const associatedFieldValue = this.entity.getField(fieldIdentifyingAssociatedEntity);
if (!associatedFieldValue) {
@@ -265,7 +281,7 @@ export default class EntityAssociationLoader<
TSelectedFields2
>
],
queryContext: EntityQueryContext
queryContext?: EntityQueryContext
): Promise<Result<TEntity2> | null>;
/**
@@ -320,7 +336,7 @@ export default class EntityAssociationLoader<
TSelectedFields3
>
],
queryContext: EntityQueryContext
queryContext?: EntityQueryContext
): Promise<Result<TEntity3> | null>;
/**
@@ -396,7 +412,7 @@ export default class EntityAssociationLoader<
TSelectedFields4
>
],
queryContext: EntityQueryContext
queryContext?: EntityQueryContext
): Promise<Result<TEntity4> | null>;
/**
@@ -407,12 +423,12 @@ export default class EntityAssociationLoader<
*/
async loadAssociatedEntityThroughAsync(
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any, any, any>[],
queryContext: EntityQueryContext
queryContext?: EntityQueryContext
): Promise<Result<ReadonlyEntity<any, any, any, any>> | null>;
async loadAssociatedEntityThroughAsync(
loadDirectives: EntityLoadThroughDirective<TViewerContext, any, any, any, any, any, any, any>[],
queryContext: EntityQueryContext
queryContext?: EntityQueryContext
): Promise<Result<ReadonlyEntity<any, any, any, any>> | null> {
let currentEntity: ReadonlyEntity<any, any, any, any> = this.entity;
for (const loadDirective of loadDirectives) {
+15
View File
@@ -88,3 +88,18 @@ export class EnumField extends EntityFieldDefinition<string | number> {
return typeof value === 'number' || typeof value === 'string';
}
}
/**
* EntityFieldDefinition for a enum column with a strict typescript enum type.
*/
export class StrictEnumField<T extends object> extends EnumField {
private readonly enum: T;
constructor(options: ConstructorParameters<typeof EnumField>[0] & { enum: T }) {
super(options);
this.enum = options.enum;
}
protected override validateInputValueInternal(value: string | number): boolean {
return super.validateInputValueInternal(value) && Object.values(this.enum).includes(value);
}
}
+28 -2
View File
@@ -6,7 +6,9 @@ import EntityNotAuthorizedError from './errors/EntityNotAuthorizedError';
import IEntityMetricsAdapter, {
EntityMetricsAuthorizationResult,
} from './metrics/IEntityMetricsAdapter';
import PrivacyPolicyRule, { RuleEvaluationResult } from './rules/PrivacyPolicyRule';
import { RuleEvaluationResult } from './rules/PrivacyPolicyRuleEnums';
import { PrivacyPolicyRule } from './rules/PrivacyPolicyRuleTypes';
import { reorderRulesByRuleComplexityGroups } from './rules/internal/PrivacyPolicyRuleOrdering';
/**
* Information about the reason this privacy policy is being evaluated.
@@ -143,6 +145,15 @@ export default abstract class EntityPrivacyPolicy<
};
}
protected async shouldAutoReorderRulesAccordingToComplexityAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_action: EntityAuthorizationAction
): Promise<boolean> {
return false;
}
/**
* Authorize an entity against creation policy.
* @param viewerContext - viewer context of user creating the entity
@@ -248,7 +259,13 @@ export default abstract class EntityPrivacyPolicy<
}
private async authorizeForRulesetAsync(
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
rulesetOriginalOrder: readonly PrivacyPolicyRule<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>[],
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
@@ -257,6 +274,15 @@ export default abstract class EntityPrivacyPolicy<
metricsAdapter: IEntityMetricsAdapter
): Promise<TEntity> {
const privacyPolicyEvaluator = this.getPrivacyPolicyEvaluator(viewerContext);
const shouldAutoReorder = await this.shouldAutoReorderRulesAccordingToComplexityAsync(
viewerContext,
queryContext,
evaluationContext,
action
);
const ruleset = shouldAutoReorder
? reorderRulesByRuleComplexityGroups(rulesetOriginalOrder)
: rulesetOriginalOrder;
switch (privacyPolicyEvaluator.mode) {
case EntityPrivacyPolicyEvaluationMode.ENFORCE:
try {
@@ -12,7 +12,7 @@ export default abstract class EntityQueryContextProvider {
/**
* Vend a regular (non-transactional) entity query context.
*/
public getNonTransactionalQueryContext(): EntityNonTransactionalQueryContext {
public getQueryContext(): EntityNonTransactionalQueryContext {
return new EntityNonTransactionalQueryContext(this.getQueryInterface(), this);
}
+4 -1
View File
@@ -148,7 +148,10 @@ export default abstract class ReadonlyEntity<
TMSelectedFields
>,
viewerContext: TMViewerContext2,
queryContext: EntityQueryContext
queryContext: EntityQueryContext = viewerContext
.getViewerScopedEntityCompanionForClass(this)
.getQueryContextProvider()
.getQueryContext()
): EntityLoader<TMFields, TMID, TMViewerContext, TMEntity, TMPrivacyPolicy, TMSelectedFields> {
return viewerContext
.getViewerScopedEntityCompanionForClass(this)
+5 -5
View File
@@ -2,7 +2,7 @@ import { IEntityClass } from './Entity';
import EntityCompanionProvider, { DatabaseAdapterFlavor } from './EntityCompanionProvider';
import EntityPrivacyPolicy from './EntityPrivacyPolicy';
import {
EntityNonTransactionalQueryContext,
EntityQueryContext,
EntityTransactionalQueryContext,
TransactionConfig,
} from './EntityQueryContext';
@@ -67,12 +67,12 @@ export default class ViewerContext {
* Get the regular (non-transactional) query context for a database adaptor flavor.
* @param databaseAdaptorFlavor - database adaptor flavor
*/
getNonTransactionalQueryContextForDatabaseAdaptorFlavor(
getQueryContextForDatabaseAdaptorFlavor(
databaseAdaptorFlavor: DatabaseAdapterFlavor
): EntityNonTransactionalQueryContext {
): EntityQueryContext {
return this.entityCompanionProvider
.getQueryContextProviderForDatabaseAdaptorFlavor(databaseAdaptorFlavor)
.getNonTransactionalQueryContext();
.getQueryContext();
}
/**
@@ -88,7 +88,7 @@ export default class ViewerContext {
): Promise<TResult> {
return await this.entityCompanionProvider
.getQueryContextProviderForDatabaseAdaptorFlavor(databaseAdaptorFlavor)
.getNonTransactionalQueryContext()
.getQueryContext()
.runInTransactionIfNotInTransactionAsync(transactionScope, transactionConfig);
}
}
+27 -43
View File
@@ -4,27 +4,25 @@ import EntityConfiguration from '../EntityConfiguration';
import { UUIDField } from '../EntityFields';
import { CreateMutator, UpdateMutator } from '../EntityMutator';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
import AlwaysDenyPrivacyPolicyRule from '../rules/AlwaysDenyPrivacyPolicyRule';
import SimpleTestEntity from '../testfixtures/SimpleTestEntity';
import TestViewerContext from '../testfixtures/TestViewerContext';
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
describe(Entity, () => {
describe('creator', () => {
it('creates a new CreateMutator', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
expect(
SimpleTestEntity.creator(viewerContext, viewerContext.getQueryContext())
).toBeInstanceOf(CreateMutator);
const viewerContext = new ViewerContext(companionProvider);
expect(SimpleTestEntity.creator(viewerContext)).toBeInstanceOf(CreateMutator);
});
});
describe('updater', () => {
it('creates a new UpdateMutator', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
const data = {
id: 'what',
};
@@ -34,16 +32,14 @@ describe(Entity, () => {
databaseFields: data,
selectedFields: data,
});
expect(SimpleTestEntity.updater(testEntity, viewerContext.getQueryContext())).toBeInstanceOf(
UpdateMutator
);
expect(SimpleTestEntity.updater(testEntity)).toBeInstanceOf(UpdateMutator);
});
});
describe('canViewerUpdateAsync', () => {
it('appropriately executes update privacy policy', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
const data = {
id: 'what',
};
@@ -53,16 +49,13 @@ describe(Entity, () => {
databaseFields: data,
selectedFields: data,
});
const canViewerUpdate = await SimpleTestDenyDeleteEntity.canViewerUpdateAsync(
testEntity,
viewerContext.getQueryContext()
);
const canViewerUpdate = await SimpleTestDenyDeleteEntity.canViewerUpdateAsync(testEntity);
expect(canViewerUpdate).toBe(true);
});
it('denies when policy denies', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
const data = {
id: 'what',
};
@@ -72,10 +65,7 @@ describe(Entity, () => {
databaseFields: data,
selectedFields: data,
});
const canViewerUpdate = await SimpleTestDenyUpdateEntity.canViewerUpdateAsync(
testEntity,
viewerContext.getQueryContext()
);
const canViewerUpdate = await SimpleTestDenyUpdateEntity.canViewerUpdateAsync(testEntity);
expect(canViewerUpdate).toBe(false);
});
});
@@ -83,7 +73,7 @@ describe(Entity, () => {
describe('canViewerDeleteAsync', () => {
it('appropriately executes update privacy policy', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
const data = {
id: 'what',
};
@@ -93,16 +83,13 @@ describe(Entity, () => {
databaseFields: data,
selectedFields: data,
});
const canViewerDelete = await SimpleTestDenyUpdateEntity.canViewerDeleteAsync(
testEntity,
viewerContext.getQueryContext()
);
const canViewerDelete = await SimpleTestDenyUpdateEntity.canViewerDeleteAsync(testEntity);
expect(canViewerDelete).toBe(true);
});
it('denies when policy denies', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
const data = {
id: 'what',
};
@@ -112,10 +99,7 @@ describe(Entity, () => {
databaseFields: data,
selectedFields: data,
});
const canViewerDelete = await SimpleTestDenyDeleteEntity.canViewerDeleteAsync(
testEntity,
viewerContext.getQueryContext()
);
const canViewerDelete = await SimpleTestDenyDeleteEntity.canViewerDeleteAsync(testEntity);
expect(canViewerDelete).toBe(false);
});
});
@@ -140,14 +124,14 @@ const testEntityConfiguration = new EntityConfiguration<TestEntityFields>({
class SimpleTestDenyUpdateEntityPrivacyPolicy extends EntityPrivacyPolicy<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyUpdateEntity
> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyUpdateEntity
>(),
];
@@ -155,7 +139,7 @@ class SimpleTestDenyUpdateEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysAllowPrivacyPolicyRule<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyUpdateEntity
>(),
];
@@ -163,7 +147,7 @@ class SimpleTestDenyUpdateEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysDenyPrivacyPolicyRule<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyUpdateEntity
>(),
];
@@ -171,7 +155,7 @@ class SimpleTestDenyUpdateEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysAllowPrivacyPolicyRule<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyUpdateEntity
>(),
];
@@ -180,14 +164,14 @@ class SimpleTestDenyUpdateEntityPrivacyPolicy extends EntityPrivacyPolicy<
class SimpleTestDenyDeleteEntityPrivacyPolicy extends EntityPrivacyPolicy<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyDeleteEntity
> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyDeleteEntity
>(),
];
@@ -195,7 +179,7 @@ class SimpleTestDenyDeleteEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysAllowPrivacyPolicyRule<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyDeleteEntity
>(),
];
@@ -203,7 +187,7 @@ class SimpleTestDenyDeleteEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysAllowPrivacyPolicyRule<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyDeleteEntity
>(),
];
@@ -211,17 +195,17 @@ class SimpleTestDenyDeleteEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysDenyPrivacyPolicyRule<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyDeleteEntity
>(),
];
}
class SimpleTestDenyUpdateEntity extends Entity<TestEntityFields, string, TestViewerContext> {
class SimpleTestDenyUpdateEntity extends Entity<TestEntityFields, string, ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyUpdateEntity,
SimpleTestDenyUpdateEntityPrivacyPolicy
> {
@@ -233,11 +217,11 @@ class SimpleTestDenyUpdateEntity extends Entity<TestEntityFields, string, TestVi
}
}
class SimpleTestDenyDeleteEntity extends Entity<TestEntityFields, string, TestViewerContext> {
class SimpleTestDenyDeleteEntity extends Entity<TestEntityFields, string, ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
TestEntityFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestDenyDeleteEntity,
SimpleTestDenyDeleteEntityPrivacyPolicy
> {
@@ -14,24 +14,20 @@ describe(EntityAssociationLoader, () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testOtherEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext()).createAsync()
TestEntity.creator(viewerContext).createAsync()
);
const testEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
TestEntity.creator(viewerContext)
.setField('stringField', testOtherEntity.getID())
.createAsync()
);
const loadedOther = await enforceAsyncResult(
testEntity
.associationLoader()
.loadAssociatedEntityAsync('stringField', TestEntity, viewerContext.getQueryContext())
testEntity.associationLoader().loadAssociatedEntityAsync('stringField', TestEntity)
);
expect(loadedOther.getID()).toEqual(testOtherEntity.getID());
const loadedOther2 = await enforceAsyncResult(
testEntity
.associationLoader()
.loadAssociatedEntityAsync('nullableField', TestEntity, viewerContext.getQueryContext())
testEntity.associationLoader().loadAssociatedEntityAsync('nullableField', TestEntity)
);
expect(loadedOther2).toBeNull();
});
@@ -41,27 +37,15 @@ describe(EntityAssociationLoader, () => {
it('loads many associated entities referencing this entity', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext()).createAsync()
);
const testEntity = await enforceAsyncResult(TestEntity.creator(viewerContext).createAsync());
const testOtherEntity1 = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
.setField('stringField', testEntity.getID())
.createAsync()
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const testOtherEntity2 = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
.setField('stringField', testEntity.getID())
.createAsync()
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const loaded = await enforceResultsAsync(
testEntity
.associationLoader()
.loadManyAssociatedEntitiesAsync(
TestEntity,
'stringField',
viewerContext.getQueryContext()
)
testEntity.associationLoader().loadManyAssociatedEntitiesAsync(TestEntity, 'stringField')
);
expect(loaded).toHaveLength(2);
expect(loaded.find((e) => e.getID() === testOtherEntity1.getID())).not.toBeUndefined();
@@ -74,21 +58,16 @@ describe(EntityAssociationLoader, () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testOtherEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext()).createAsync()
TestEntity.creator(viewerContext).createAsync()
);
const testEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
TestEntity.creator(viewerContext)
.setField('stringField', testOtherEntity.getID())
.createAsync()
);
const loadedOtherResult = await testEntity
.associationLoader()
.loadAssociatedEntityByFieldEqualingAsync(
'stringField',
TestEntity,
'customIdField',
viewerContext.getQueryContext()
);
.loadAssociatedEntityByFieldEqualingAsync('stringField', TestEntity, 'customIdField');
expect(loadedOtherResult?.enforceValue().getID()).toEqual(testOtherEntity.getID());
});
@@ -96,18 +75,11 @@ describe(EntityAssociationLoader, () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
.setField('stringField', uuidv4())
.createAsync()
TestEntity.creator(viewerContext).setField('stringField', uuidv4()).createAsync()
);
const loadedOtherResult = await testEntity
.associationLoader()
.loadAssociatedEntityByFieldEqualingAsync(
'stringField',
TestEntity,
'customIdField',
viewerContext.getQueryContext()
);
.loadAssociatedEntityByFieldEqualingAsync('stringField', TestEntity, 'customIdField');
expect(loadedOtherResult).toBeNull();
});
@@ -115,18 +87,11 @@ describe(EntityAssociationLoader, () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
.setField('stringField', 'blah')
.createAsync()
TestEntity.creator(viewerContext).setField('stringField', 'blah').createAsync()
);
const loadedOtherResult = await testEntity
.associationLoader()
.loadAssociatedEntityByFieldEqualingAsync(
'nullableField',
TestEntity,
'customIdField',
viewerContext.getQueryContext()
);
.loadAssociatedEntityByFieldEqualingAsync('nullableField', TestEntity, 'customIdField');
expect(loadedOtherResult).toBeNull();
});
});
@@ -135,18 +100,12 @@ describe(EntityAssociationLoader, () => {
it('loads many associated entities by field equaling', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext()).createAsync()
);
const testEntity = await enforceAsyncResult(TestEntity.creator(viewerContext).createAsync());
const testOtherEntity1 = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
.setField('stringField', testEntity.getID())
.createAsync()
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const testOtherEntity2 = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
.setField('stringField', testEntity.getID())
.createAsync()
TestEntity.creator(viewerContext).setField('stringField', testEntity.getID()).createAsync()
);
const loaded = await enforceResultsAsync(
testEntity
@@ -154,8 +113,7 @@ describe(EntityAssociationLoader, () => {
.loadManyAssociatedEntitiesByFieldEqualingAsync(
'customIdField',
TestEntity,
'stringField',
viewerContext.getQueryContext()
'stringField'
)
);
expect(loaded).toHaveLength(2);
@@ -166,17 +124,14 @@ describe(EntityAssociationLoader, () => {
it('returns empty results when field being queried by is null', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext()).createAsync()
);
const testEntity = await enforceAsyncResult(TestEntity.creator(viewerContext).createAsync());
const loaded = await enforceResultsAsync(
testEntity
.associationLoader()
.loadManyAssociatedEntitiesByFieldEqualingAsync(
'nullableField',
TestEntity,
'stringField',
viewerContext.getQueryContext()
'stringField'
)
);
expect(loaded).toHaveLength(0);
@@ -187,68 +142,53 @@ describe(EntityAssociationLoader, () => {
it('chain loads associated entities', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const testEntity4 = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext()).createAsync()
);
const testEntity4 = await enforceAsyncResult(TestEntity.creator(viewerContext).createAsync());
const testEntity3 = await enforceAsyncResult(
TestEntity2.creator(viewerContext, viewerContext.getQueryContext())
.setField('foreignKey', testEntity4.getID())
.createAsync()
TestEntity2.creator(viewerContext).setField('foreignKey', testEntity4.getID()).createAsync()
);
const testEntity2 = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
TestEntity.creator(viewerContext)
.setField('testIndexedField', testEntity3.getID())
.createAsync()
);
const testEntity = await enforceAsyncResult(
TestEntity2.creator(viewerContext, viewerContext.getQueryContext())
.setField('foreignKey', testEntity2.getID())
.createAsync()
TestEntity2.creator(viewerContext).setField('foreignKey', testEntity2.getID()).createAsync()
);
const loaded2Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync(
[
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
],
viewerContext.getQueryContext()
);
const loaded2Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
]);
expect(loaded2Result?.enforceValue().getID()).toEqual(testEntity2.getID());
const loaded3Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync(
[
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
{
associatedEntityClass: TestEntity2,
fieldIdentifyingAssociatedEntity: 'testIndexedField',
},
],
viewerContext.getQueryContext()
);
const loaded3Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
{
associatedEntityClass: TestEntity2,
fieldIdentifyingAssociatedEntity: 'testIndexedField',
},
]);
expect(loaded3Result?.enforceValue().getID()).toEqual(testEntity3.getID());
const loaded4Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync(
[
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
{
associatedEntityClass: TestEntity2,
fieldIdentifyingAssociatedEntity: 'testIndexedField',
},
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
],
viewerContext.getQueryContext()
);
const loaded4Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
{
associatedEntityClass: TestEntity2,
fieldIdentifyingAssociatedEntity: 'testIndexedField',
},
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
]);
expect(loaded4Result?.enforceValue().getID()).toEqual(testEntity4.getID());
});
@@ -257,20 +197,15 @@ describe(EntityAssociationLoader, () => {
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(
TestEntity2.creator(viewerContext, viewerContext.getQueryContext())
.setField('foreignKey', uuidv4())
.createAsync()
TestEntity2.creator(viewerContext).setField('foreignKey', uuidv4()).createAsync()
);
const loadResult = await testEntity.associationLoader().loadAssociatedEntityThroughAsync(
[
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
],
viewerContext.getQueryContext()
);
const loadResult = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
},
]);
expect(loadResult?.ok).toBe(false);
});
@@ -280,26 +215,19 @@ describe(EntityAssociationLoader, () => {
const fieldValue = uuidv4();
const testEntity2 = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
.setField('stringField', fieldValue)
.createAsync()
TestEntity.creator(viewerContext).setField('stringField', fieldValue).createAsync()
);
const testEntity = await enforceAsyncResult(
TestEntity2.creator(viewerContext, viewerContext.getQueryContext())
.setField('foreignKey', fieldValue)
.createAsync()
TestEntity2.creator(viewerContext).setField('foreignKey', fieldValue).createAsync()
);
const loaded2Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync(
[
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
associatedEntityLookupByField: 'stringField',
},
],
viewerContext.getQueryContext()
);
const loaded2Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
associatedEntityLookupByField: 'stringField',
},
]);
expect(loaded2Result?.enforceValue().getID()).toEqual(testEntity2.getID());
});
@@ -308,21 +236,16 @@ describe(EntityAssociationLoader, () => {
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(
TestEntity2.creator(viewerContext, viewerContext.getQueryContext())
.setField('foreignKey', uuidv4())
.createAsync()
TestEntity2.creator(viewerContext).setField('foreignKey', uuidv4()).createAsync()
);
const loaded2Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync(
[
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
associatedEntityLookupByField: 'stringField',
},
],
viewerContext.getQueryContext()
);
const loaded2Result = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'foreignKey',
associatedEntityLookupByField: 'stringField',
},
]);
expect(loaded2Result).toBeNull();
});
@@ -331,20 +254,15 @@ describe(EntityAssociationLoader, () => {
const viewerContext = new TestViewerContext(companionProvider);
const testEntity = await enforceAsyncResult(
TestEntity.creator(viewerContext, viewerContext.getQueryContext())
.setField('nullableField', null)
.createAsync()
TestEntity.creator(viewerContext).setField('nullableField', null).createAsync()
);
const loadedResult = await testEntity.associationLoader().loadAssociatedEntityThroughAsync(
[
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'nullableField',
},
],
viewerContext.getQueryContext()
);
const loadedResult = await testEntity.associationLoader().loadAssociatedEntityThroughAsync([
{
associatedEntityClass: TestEntity,
fieldIdentifyingAssociatedEntity: 'nullableField',
},
]);
expect(loadedResult).toBeNull();
});
});
@@ -7,15 +7,16 @@ import EntityConfiguration from '../EntityConfiguration';
import { UUIDField } from '../EntityFields';
import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ViewerContext from '../ViewerContext';
import { enforceResultsAsync } from '../entityUtils';
import EntityNotAuthorizedError from '../errors/EntityNotAuthorizedError';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
import AlwaysDenyPrivacyPolicyRule from '../rules/AlwaysDenyPrivacyPolicyRule';
import PrivacyPolicyRule, { RuleEvaluationResult } from '../rules/PrivacyPolicyRule';
import TestViewerContext from '../testfixtures/TestViewerContext';
import { RuleComplexity, RuleEvaluationResult } from '../rules/PrivacyPolicyRuleEnums';
import { DenyOrSkipPrivacyPolicyRule } from '../rules/PrivacyPolicyRuleTypes';
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
class TestUserViewerContext extends TestViewerContext {
class TestUserViewerContext extends ViewerContext {
constructor(entityCompanionProvider: EntityCompanionProvider, private readonly userID: string) {
super(entityCompanionProvider);
}
@@ -60,18 +61,20 @@ class BlahEntity extends Entity<BlahFields, string, TestUserViewerContext> {
}
}
class DenyIfNotOwnerPrivacyPolicyRule extends PrivacyPolicyRule<
class DenyIfNotOwnerPrivacyPolicyRule extends DenyOrSkipPrivacyPolicyRule<
BlahFields,
string,
TestUserViewerContext,
BlahEntity
> {
override complexity = RuleComplexity.LOW;
async evaluateAsync(
viewerContext: TestUserViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: BlahEntity
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.DENY | RuleEvaluationResult.SKIP> {
if (viewerContext.getUserID() === entity.getField('ownerID')) {
return RuleEvaluationResult.SKIP;
}
@@ -82,23 +85,23 @@ class DenyIfNotOwnerPrivacyPolicyRule extends PrivacyPolicyRule<
class BlahEntityPrivacyPolicy extends EntityPrivacyPolicy<
BlahFields,
string,
TestUserViewerContext,
ViewerContext,
BlahEntity
> {
protected override readonly createRules = [
new DenyIfNotOwnerPrivacyPolicyRule(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, TestUserViewerContext, BlahEntity>(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
protected override readonly readRules = [
new DenyIfNotOwnerPrivacyPolicyRule(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, TestUserViewerContext, BlahEntity>(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
protected override readonly updateRules = [
new DenyIfNotOwnerPrivacyPolicyRule(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, TestUserViewerContext, BlahEntity>(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
protected override readonly deleteRules = [
new AlwaysDenyPrivacyPolicyRule<BlahFields, string, TestUserViewerContext, BlahEntity>(),
new AlwaysDenyPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
}
@@ -110,21 +113,15 @@ it('runs through a common workflow', async () => {
const vc2 = new TestUserViewerContext(entityCompanionProvider, uuidv4());
const blahOwner1 = await enforceAsyncResult(
BlahEntity.creator(vc1, vc1.getQueryContext())
.setField('ownerID', vc1.getUserID()!)
.createAsync()
BlahEntity.creator(vc1).setField('ownerID', vc1.getUserID()!).createAsync()
);
await enforceAsyncResult(
BlahEntity.creator(vc1, vc1.getQueryContext())
.setField('ownerID', vc1.getUserID()!)
.createAsync()
BlahEntity.creator(vc1).setField('ownerID', vc1.getUserID()!).createAsync()
);
const blahOwner2 = await enforceAsyncResult(
BlahEntity.creator(vc2, vc2.getQueryContext())
.setField('ownerID', vc2.getUserID()!)
.createAsync()
BlahEntity.creator(vc2).setField('ownerID', vc2.getUserID()!).createAsync()
);
// sanity check created objects
@@ -133,45 +130,33 @@ it('runs through a common workflow', async () => {
// check that two people can't read each others data
await expect(
enforceAsyncResult(
BlahEntity.loader(vc1, vc1.getQueryContext()).loadByIDAsync(blahOwner2.getID())
)
enforceAsyncResult(BlahEntity.loader(vc1).loadByIDAsync(blahOwner2.getID()))
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
await expect(
enforceAsyncResult(
BlahEntity.loader(vc2, vc2.getQueryContext()).loadByIDAsync(blahOwner1.getID())
)
enforceAsyncResult(BlahEntity.loader(vc2).loadByIDAsync(blahOwner1.getID()))
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
// check that all of owner 1's objects can be loaded
const results = await enforceResultsAsync(
BlahEntity.loader(vc1, vc1.getQueryContext()).loadManyByFieldEqualingAsync(
'ownerID',
vc1.getUserID()!
)
BlahEntity.loader(vc1).loadManyByFieldEqualingAsync('ownerID', vc1.getUserID()!)
);
expect(results).toHaveLength(2);
// check that two people can't create objects owned by others
await expect(
enforceAsyncResult(
BlahEntity.creator(vc2, vc2.getQueryContext())
.setField('ownerID', blahOwner1.getID())
.createAsync()
BlahEntity.creator(vc2).setField('ownerID', blahOwner1.getID()).createAsync()
)
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
// check that empty load many returns nothing
const results2 = await BlahEntity.loader(
vc1,
vc1.getQueryContext()
).loadManyByFieldEqualingManyAsync('ownerID', []);
const results2 = await BlahEntity.loader(vc1).loadManyByFieldEqualingManyAsync('ownerID', []);
for (const value in results2.values) {
expect(value).toHaveLength(0);
}
// check that the user can't delete their own data (as specified by privacy rules)
await expect(
enforceAsyncResult(BlahEntity.deleteAsync(blahOwner2, vc1.getQueryContext()))
).rejects.toBeInstanceOf(EntityNotAuthorizedError);
await expect(enforceAsyncResult(BlahEntity.deleteAsync(blahOwner2))).rejects.toBeInstanceOf(
EntityNotAuthorizedError
);
});
+46 -105
View File
@@ -13,7 +13,8 @@ import EntityPrivacyPolicy, {
} from '../EntityPrivacyPolicy';
import { EntityTransactionalQueryContext, EntityQueryContext } from '../EntityQueryContext';
import { CacheStatus } from '../internal/ReadThroughEntityCache';
import PrivacyPolicyRule, { RuleEvaluationResult } from '../rules/PrivacyPolicyRule';
import { RuleComplexity, RuleEvaluationResult } from '../rules/PrivacyPolicyRuleEnums';
import { AllowOrSkipPrivacyPolicyRule } from '../rules/PrivacyPolicyRuleTypes';
import TestViewerContext from '../testfixtures/TestViewerContext';
import { InMemoryFullCacheStubCacheAdapter } from '../utils/testing/StubCacheAdapter';
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
@@ -66,13 +67,15 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
},
};
class AlwaysAllowPrivacyPolicyRuleThatRecords extends PrivacyPolicyRule<
class AlwaysAllowPrivacyPolicyRuleThatRecords extends AllowOrSkipPrivacyPolicyRule<
any,
string,
TestViewerContext,
any,
any
> {
override complexity = RuleComplexity.LOW;
constructor(private readonly action: EntityAuthorizationAction) {
super();
}
@@ -82,7 +85,7 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
_queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: any
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.ALLOW> {
if (privacyPolicyEvaluationRecords.shouldRecord) {
(privacyPolicyEvaluationRecords as any)[entity.constructor.name][this.action].push(
evaluationContext
@@ -469,54 +472,36 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const parent = await ParentEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext, viewerContext.getQueryContext())
const parent = await ParentEntity.creator(viewerContext).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext)
.setField('parent_id', parent.getID())
.enforceCreateAsync();
const grandchild = await GrandChildEntity.creator(
viewerContext,
viewerContext.getQueryContext()
)
const grandchild = await GrandChildEntity.creator(viewerContext)
.setField('parent_id', child.getID())
.enforceCreateAsync();
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
).resolves.not.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(grandchild.getID())
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
).resolves.not.toBeNull();
privacyPolicyEvaluationRecords.shouldRecord = true;
await ParentEntity.enforceDeleteAsync(parent, viewerContext.getQueryContext());
await ParentEntity.enforceDeleteAsync(parent);
privacyPolicyEvaluationRecords.shouldRecord = false;
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();
await expect(
ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
).resolves.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(grandchild.getID())
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
).resolves.toBeNull();
// two calls for each trigger, one beforeDelete, one afterDelete
@@ -605,55 +590,38 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const parent = await ParentEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext, viewerContext.getQueryContext())
const parent = await ParentEntity.creator(viewerContext).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext)
.setField('parent_id', parent.getID())
.enforceCreateAsync();
const grandchild = await GrandChildEntity.creator(
viewerContext,
viewerContext.getQueryContext()
)
const grandchild = await GrandChildEntity.creator(viewerContext)
.setField('parent_id', child.getID())
.enforceCreateAsync();
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
).resolves.not.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(grandchild.getID())
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
).resolves.not.toBeNull();
privacyPolicyEvaluationRecords.shouldRecord = true;
await ParentEntity.enforceDeleteAsync(parent, viewerContext.getQueryContext());
await ParentEntity.enforceDeleteAsync(parent);
privacyPolicyEvaluationRecords.shouldRecord = false;
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();
const loadedChild = await ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
const loadedChild = await ChildEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(child.getID());
expect(loadedChild.getField('parent_id')).toBeNull();
const loadedGrandchild = await GrandChildEntity.loader(
viewerContext,
viewerContext.getQueryContext()
)
const loadedGrandchild = await GrandChildEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(grandchild.getID());
expect(loadedGrandchild.getField('parent_id')).toEqual(loadedChild.getID());
@@ -733,32 +701,24 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const parent = await ParentEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext, viewerContext.getQueryContext())
const parent = await ParentEntity.creator(viewerContext).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext)
.setField('parent_id', parent.getID())
.enforceCreateAsync();
const grandchild = await GrandChildEntity.creator(
viewerContext,
viewerContext.getQueryContext()
)
const grandchild = await GrandChildEntity.creator(viewerContext)
.setField('parent_id', child.getID())
.enforceCreateAsync();
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
ChildEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_id', parent.getID())
).resolves.not.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext, viewerContext.getQueryContext())
GrandChildEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_id', child.getID())
).resolves.not.toBeNull();
@@ -782,7 +742,7 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
expect(grandChildCachedBefore.get(child.getID())?.status).toEqual(CacheStatus.HIT);
privacyPolicyEvaluationRecords.shouldRecord = true;
await ParentEntity.enforceDeleteAsync(parent, viewerContext.getQueryContext());
await ParentEntity.enforceDeleteAsync(parent);
privacyPolicyEvaluationRecords.shouldRecord = false;
const childCachedAfter = await childCacheAdapter.loadManyAsync('parent_id', [parent.getID()]);
@@ -794,20 +754,15 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
expect(grandChildCachedAfter.get(child.getID())?.status).toEqual(CacheStatus.HIT);
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();
const loadedChild = await ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
const loadedChild = await ChildEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(child.getID());
expect(loadedChild).not.toBeNull();
const loadedGrandchild = await GrandChildEntity.loader(
viewerContext,
viewerContext.getQueryContext()
)
const loadedGrandchild = await GrandChildEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(grandchild.getID());
expect(loadedGrandchild.getField('parent_id')).toEqual(loadedChild.getID());
@@ -879,32 +834,24 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const parent = await ParentEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext, viewerContext.getQueryContext())
const parent = await ParentEntity.creator(viewerContext).enforceCreateAsync();
const child = await ChildEntity.creator(viewerContext)
.setField('parent_id', parent.getID())
.enforceCreateAsync();
const grandchild = await GrandChildEntity.creator(
viewerContext,
viewerContext.getQueryContext()
)
const grandchild = await GrandChildEntity.creator(viewerContext)
.setField('parent_id', child.getID())
.enforceCreateAsync();
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.not.toBeNull();
await expect(
ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
ChildEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_id', parent.getID())
).resolves.not.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext, viewerContext.getQueryContext())
GrandChildEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_id', child.getID())
).resolves.not.toBeNull();
@@ -928,7 +875,7 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
expect(grandChildCachedBefore.get(child.getID())?.status).toEqual(CacheStatus.HIT);
privacyPolicyEvaluationRecords.shouldRecord = true;
await ParentEntity.enforceDeleteAsync(parent, viewerContext.getQueryContext());
await ParentEntity.enforceDeleteAsync(parent);
privacyPolicyEvaluationRecords.shouldRecord = false;
const childCachedAfter = await childCacheAdapter.loadManyAsync('parent_id', [parent.getID()]);
@@ -940,19 +887,13 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
expect(grandChildCachedAfter.get(child.getID())?.status).toEqual(CacheStatus.MISS);
await expect(
ParentEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parent.getID())
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
).resolves.toBeNull();
await expect(
ChildEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(child.getID())
ChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(child.getID())
).resolves.not.toBeNull();
await expect(
GrandChildEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(grandchild.getID())
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
).resolves.not.toBeNull();
// two calls for each trigger, one beforeDelete, one afterDelete
@@ -11,6 +11,7 @@ import {
StringArrayField,
JSONObjectField,
EnumField,
StrictEnumField,
} from '../EntityFields';
import describeFieldTestCase from '../utils/testing/describeFieldTestCase';
@@ -75,3 +76,14 @@ describeFieldTestCase(
);
describeFieldTestCase(new JSONObjectField({ columnName: 'wat' }), [{}], [true, 'hello']);
describeFieldTestCase(new EnumField({ columnName: 'wat' }), ['hello', 1], [true]);
enum TestEnum {
HELLO = 'world',
WHO = 'wat',
}
describeFieldTestCase(
new StrictEnumField({ columnName: 'wat', enum: TestEnum }),
[TestEnum.HELLO, TestEnum.WHO, 'world'],
['what', 1, true]
);
@@ -123,7 +123,7 @@ describe(EntityLoader, () => {
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
@@ -5,6 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
import { OrderByOrdering } from '../EntityDatabaseAdapter';
import EntityLoader from '../EntityLoader';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import { enforceResultsAsync } from '../entityUtils';
import EntityDataManager from '../internal/EntityDataManager';
import ReadThroughEntityCache from '../internal/ReadThroughEntityCache';
@@ -14,7 +15,6 @@ import TestEntity, {
TestEntityPrivacyPolicy,
testEntityConfiguration,
} from '../testfixtures/TestEntity';
import TestViewerContext from '../testfixtures/TestViewerContext';
import { NoCacheStubCacheAdapterProvider } from '../utils/testing/StubCacheAdapter';
import StubDatabaseAdapter from '../utils/testing/StubDatabaseAdapter';
import StubQueryContextProvider from '../utils/testing/StubQueryContextProvider';
@@ -22,10 +22,10 @@ import StubQueryContextProvider from '../utils/testing/StubQueryContextProvider'
describe(EntityLoader, () => {
it('loads entities', async () => {
const dateToInsert = new Date();
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -117,10 +117,10 @@ describe(EntityLoader, () => {
it('loads entities with loadManyByFieldEqualityConjunction', async () => {
const privacyPolicy = new TestEntityPrivacyPolicy();
const spiedPrivacyPolicy = spy(privacyPolicy);
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -216,10 +216,10 @@ describe(EntityLoader, () => {
it('loads entities with loadFirstByFieldEqualityConjunction', async () => {
const privacyPolicy = new TestEntityPrivacyPolicy();
const spiedPrivacyPolicy = spy(privacyPolicy);
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -313,10 +313,10 @@ describe(EntityLoader, () => {
const privacyPolicy = new TestEntityPrivacyPolicy();
const spiedPrivacyPolicy = spy(privacyPolicy);
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
@@ -374,10 +374,10 @@ describe(EntityLoader, () => {
});
it('invalidates upon invalidate one', async () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const privacyPolicy = instance(mock(TestEntityPrivacyPolicy));
const dataManagerMock = mock<EntityDataManager<TestFields>>();
const dataManagerInstance = instance(dataManagerMock);
@@ -402,10 +402,10 @@ describe(EntityLoader, () => {
});
it('invalidates upon invalidate by field', async () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const privacyPolicy = instance(mock(TestEntityPrivacyPolicy));
const dataManagerMock = mock<EntityDataManager<TestFields>>();
const dataManagerInstance = instance(dataManagerMock);
@@ -429,10 +429,10 @@ describe(EntityLoader, () => {
});
it('invalidates upon invalidate by entity', async () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const privacyPolicy = instance(mock(TestEntityPrivacyPolicy));
const dataManagerMock = mock<EntityDataManager<TestFields>>();
const dataManagerInstance = instance(dataManagerMock);
@@ -460,10 +460,10 @@ describe(EntityLoader, () => {
});
it('returns error result when not allowed', async () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const privacyPolicyMock = mock(TestEntityPrivacyPolicy);
const dataManagerMock = mock<EntityDataManager<TestFields>>();
@@ -506,10 +506,10 @@ describe(EntityLoader, () => {
});
it('throws upon database adapter error', async () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const privacyPolicy = instance(mock(TestEntityPrivacyPolicy));
const dataManagerMock = mock<EntityDataManager<TestFields>>();
@@ -5,8 +5,8 @@ import { UUIDField } from '../EntityFields';
import { EntityMutationType, EntityTriggerMutationInfo } from '../EntityMutationInfo';
import { EntityNonTransactionalMutationTrigger } from '../EntityMutationTriggerConfiguration';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
import TestViewerContext from '../testfixtures/TestViewerContext';
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
type BlahFields = {
@@ -16,28 +16,28 @@ type BlahFields = {
class BlahEntityPrivacyPolicy extends EntityPrivacyPolicy<
BlahFields,
string,
TestViewerContext,
ViewerContext,
BlahEntity
> {
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, TestViewerContext, BlahEntity>(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, TestViewerContext, BlahEntity>(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, TestViewerContext, BlahEntity>(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, TestViewerContext, BlahEntity>(),
new AlwaysAllowPrivacyPolicyRule<BlahFields, string, ViewerContext, BlahEntity>(),
];
}
class BlahEntity extends Entity<BlahFields, string, TestViewerContext> {
class BlahEntity extends Entity<BlahFields, string, ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
BlahFields,
string,
TestViewerContext,
ViewerContext,
BlahEntity,
BlahEntityPrivacyPolicy
> {
@@ -66,16 +66,16 @@ class BlahEntity extends Entity<BlahFields, string, TestViewerContext> {
class TestNonTransactionalMutationTrigger extends EntityNonTransactionalMutationTrigger<
BlahFields,
string,
TestViewerContext,
ViewerContext,
BlahEntity
> {
async executeAsync(
viewerContext: TestViewerContext,
viewerContext: ViewerContext,
entity: BlahEntity,
mutationInfo: EntityTriggerMutationInfo<BlahFields, string, TestViewerContext, BlahEntity>
mutationInfo: EntityTriggerMutationInfo<BlahFields, string, ViewerContext, BlahEntity>
): Promise<void> {
if (mutationInfo.type === EntityMutationType.DELETE) {
const entityLoaded = await BlahEntity.loader(viewerContext, viewerContext.getQueryContext())
const entityLoaded = await BlahEntity.loader(viewerContext)
.enforcing()
.loadByIDNullableAsync(entity.getID());
if (entityLoaded) {
@@ -90,17 +90,14 @@ class TestNonTransactionalMutationTrigger extends EntityNonTransactionalMutation
describe('EntityMutator', () => {
test('cache consistency with post-commit callbacks', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
// put it in cache
const entity = await BlahEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const entityLoaded = await BlahEntity.loader(viewerContext, viewerContext.getQueryContext())
const entity = await BlahEntity.creator(viewerContext).enforceCreateAsync();
const entityLoaded = await BlahEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity.getID());
await BlahEntity.enforceDeleteAsync(entityLoaded, viewerContext.getQueryContext());
await BlahEntity.enforceDeleteAsync(entityLoaded);
});
});
@@ -31,6 +31,7 @@ import EntityMutatorFactory from '../EntityMutatorFactory';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityTransactionalQueryContext, EntityQueryContext } from '../EntityQueryContext';
import IEntityDatabaseAdapterProvider from '../IEntityDatabaseAdapterProvider';
import ViewerContext from '../ViewerContext';
import { enforceResultsAsync } from '../entityUtils';
import EntityDataManager from '../internal/EntityDataManager';
import ReadThroughEntityCache from '../internal/ReadThroughEntityCache';
@@ -46,7 +47,6 @@ import TestEntity, {
TestEntityPrivacyPolicy,
testEntityConfiguration,
} from '../testfixtures/TestEntity';
import TestViewerContext from '../testfixtures/TestViewerContext';
import { NoCacheStubCacheAdapterProvider } from '../utils/testing/StubCacheAdapter';
import StubDatabaseAdapter from '../utils/testing/StubDatabaseAdapter';
import StubQueryContextProvider from '../utils/testing/StubQueryContextProvider';
@@ -54,18 +54,18 @@ import StubQueryContextProvider from '../utils/testing/StubQueryContextProvider'
class TestMutationTrigger extends EntityMutationTrigger<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
> {
async executeAsync(
_viewerContext: TestViewerContext,
_viewerContext: ViewerContext,
_queryContext: EntityQueryContext,
_entity: TestEntity,
_mutationInfo: EntityTriggerMutationInfo<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>
@@ -75,37 +75,31 @@ class TestMutationTrigger extends EntityMutationTrigger<
class TestNonTransactionalMutationTrigger extends EntityNonTransactionalMutationTrigger<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
> {
async executeAsync(_viewerContext: TestViewerContext, _entity: TestEntity): Promise<void> {}
async executeAsync(_viewerContext: ViewerContext, _entity: TestEntity): Promise<void> {}
}
const setUpMutationValidatorSpies = (
mutationValidators: EntityMutationValidator<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>[]
): EntityMutationValidator<
TestFields,
string,
TestViewerContext,
TestEntity,
keyof TestFields
>[] => {
): EntityMutationValidator<TestFields, string, ViewerContext, TestEntity, keyof TestFields>[] => {
return mutationValidators.map((validator) => spy(validator));
};
const verifyValidatorCounts = (
viewerContext: TestViewerContext,
viewerContext: ViewerContext,
mutationValidatorSpies: EntityMutationValidator<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>[],
@@ -113,7 +107,7 @@ const verifyValidatorCounts = (
mutationInfo: EntityValidatorMutationInfo<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>
@@ -134,14 +128,14 @@ const setUpMutationTriggerSpies = (
mutationTriggers: EntityMutationTriggerConfiguration<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>
): EntityMutationTriggerConfiguration<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
> => {
@@ -159,11 +153,11 @@ const setUpMutationTriggerSpies = (
};
const verifyTriggerCounts = (
viewerContext: TestViewerContext,
viewerContext: ViewerContext,
mutationTriggerSpies: EntityMutationTriggerConfiguration<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>,
@@ -172,7 +166,7 @@ const verifyTriggerCounts = (
EntityMutationTriggerConfiguration<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>,
@@ -188,7 +182,7 @@ const verifyTriggerCounts = (
mutationInfo: EntityTriggerMutationInfo<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>
@@ -249,7 +243,7 @@ const createEntityMutatorFactory = (
entityLoaderFactory: EntityLoaderFactory<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
TestEntityPrivacyPolicy,
keyof TestFields
@@ -257,7 +251,7 @@ const createEntityMutatorFactory = (
entityMutatorFactory: EntityMutatorFactory<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
TestEntityPrivacyPolicy
>;
@@ -265,14 +259,14 @@ const createEntityMutatorFactory = (
mutationValidators: EntityMutationValidator<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>[];
mutationTriggers: EntityMutationTriggerConfiguration<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>;
@@ -280,14 +274,14 @@ const createEntityMutatorFactory = (
const mutationValidators: EntityMutationValidator<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
>[] = [new TestMutationTrigger()];
const mutationTriggers: EntityMutationTriggerConfiguration<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
keyof TestFields
> = {
@@ -378,8 +372,8 @@ const createEntityMutatorFactory = (
describe(EntityMutatorFactory, () => {
describe('forCreate', () => {
it('creates entities', async () => {
const viewerContext = mock<TestViewerContext>();
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const viewerContext = mock<ViewerContext>();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -409,8 +403,8 @@ describe(EntityMutatorFactory, () => {
});
it('checks privacy', async () => {
const viewerContext = mock<TestViewerContext>();
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const viewerContext = mock<ViewerContext>();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -452,8 +446,8 @@ describe(EntityMutatorFactory, () => {
});
it('executes triggers', async () => {
const viewerContext = mock<TestViewerContext>();
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const viewerContext = mock<ViewerContext>();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -499,8 +493,8 @@ describe(EntityMutatorFactory, () => {
});
it('executes validators', async () => {
const viewerContext = mock<TestViewerContext>();
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const viewerContext = mock<ViewerContext>();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -536,9 +530,9 @@ describe(EntityMutatorFactory, () => {
describe('forUpdate', () => {
it('updates entities', async () => {
const viewerContext = mock<TestViewerContext>();
const viewerContext = mock<ViewerContext>();
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -585,9 +579,9 @@ describe(EntityMutatorFactory, () => {
});
it('checks privacy', async () => {
const viewerContext = mock<TestViewerContext>();
const viewerContext = mock<ViewerContext>();
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -636,9 +630,9 @@ describe(EntityMutatorFactory, () => {
});
it('executes triggers', async () => {
const viewerContext = mock<TestViewerContext>();
const viewerContext = mock<ViewerContext>();
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -694,9 +688,9 @@ describe(EntityMutatorFactory, () => {
);
});
it('executes validators', async () => {
const viewerContext = mock<TestViewerContext>();
const viewerContext = mock<ViewerContext>();
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const id2 = uuidv4();
@@ -743,9 +737,9 @@ describe(EntityMutatorFactory, () => {
describe('forDelete', () => {
it('deletes entities', async () => {
const viewerContext = mock<TestViewerContext>();
const viewerContext = mock<ViewerContext>();
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const { entityMutatorFactory, entityLoaderFactory } = createEntityMutatorFactory([
@@ -778,9 +772,9 @@ describe(EntityMutatorFactory, () => {
});
it('checks privacy', async () => {
const viewerContext = mock<TestViewerContext>();
const viewerContext = mock<ViewerContext>();
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const { privacyPolicy, entityMutatorFactory, entityLoaderFactory } =
@@ -817,9 +811,9 @@ describe(EntityMutatorFactory, () => {
});
it('executes triggers', async () => {
const viewerContext = mock<TestViewerContext>();
const viewerContext = mock<ViewerContext>();
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const { mutationTriggers, entityMutatorFactory, entityLoaderFactory } =
@@ -860,9 +854,9 @@ describe(EntityMutatorFactory, () => {
});
it('does not execute validators', async () => {
const viewerContext = mock<TestViewerContext>();
const viewerContext = mock<ViewerContext>();
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const { mutationValidators, entityMutatorFactory, entityLoaderFactory } =
@@ -894,9 +888,9 @@ describe(EntityMutatorFactory, () => {
});
it('invalidates cache for fields upon create', async () => {
const viewerContext = mock<TestViewerContext>();
const viewerContext = mock<ViewerContext>();
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const { entityMutatorFactory, entityLoaderFactory } = createEntityMutatorFactory([
@@ -933,8 +927,8 @@ describe(EntityMutatorFactory, () => {
});
it('throws error when field not valid', async () => {
const viewerContext = mock<TestViewerContext>();
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const viewerContext = mock<ViewerContext>();
const queryContext = StubQueryContextProvider.getQueryContext();
const id1 = uuidv4();
const { entityMutatorFactory } = createEntityMutatorFactory([
{
@@ -969,8 +963,8 @@ describe(EntityMutatorFactory, () => {
it('returns error result when not authorized to create', async () => {
const entityCompanionProvider = instance(mock(EntityCompanionProvider));
const viewerContext = instance(mock(TestViewerContext));
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const viewerContext = instance(mock(ViewerContext));
const queryContext = StubQueryContextProvider.getQueryContext();
const privacyPolicyMock = mock(SimpleTestEntityPrivacyPolicy);
const databaseAdapter = instance(mock<EntityDatabaseAdapter<SimpleTestFields>>());
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
@@ -992,7 +986,7 @@ describe(EntityMutatorFactory, () => {
EntityLoader<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestEntityPrivacyPolicy,
keyof SimpleTestFields
@@ -1006,7 +1000,7 @@ describe(EntityMutatorFactory, () => {
EntityLoaderFactory<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestEntityPrivacyPolicy,
keyof SimpleTestFields
@@ -1093,8 +1087,8 @@ describe(EntityMutatorFactory, () => {
const entityCompanionProvider = instance(entityCompanionProviderMock);
const viewerContext = instance(mock(TestViewerContext));
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const viewerContext = instance(mock(ViewerContext));
const queryContext = StubQueryContextProvider.getQueryContext();
const privacyPolicy = instance(mock(SimpleTestEntityPrivacyPolicy));
const databaseAdapterMock = mock<EntityDatabaseAdapter<SimpleTestFields>>();
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
@@ -1116,7 +1110,7 @@ describe(EntityMutatorFactory, () => {
EntityLoader<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestEntityPrivacyPolicy,
keyof SimpleTestFields
@@ -1130,7 +1124,7 @@ describe(EntityMutatorFactory, () => {
EntityLoaderFactory<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestEntityPrivacyPolicy,
keyof SimpleTestFields
@@ -1190,8 +1184,8 @@ describe(EntityMutatorFactory, () => {
});
it('records metrics appropriately', async () => {
const viewerContext = mock<TestViewerContext>();
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const viewerContext = mock<ViewerContext>();
const queryContext = StubQueryContextProvider.getQueryContext();
const { entityMutatorFactory, metricsAdapter } = createEntityMutatorFactory([]);
const spiedMetricsAdapter = spy(metricsAdapter);
@@ -19,7 +19,8 @@ import IEntityMetricsAdapter, {
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
import AlwaysDenyPrivacyPolicyRule from '../rules/AlwaysDenyPrivacyPolicyRule';
import AlwaysSkipPrivacyPolicyRule from '../rules/AlwaysSkipPrivacyPolicyRule';
import PrivacyPolicyRule, { RuleEvaluationResult } from '../rules/PrivacyPolicyRule';
import { RuleComplexity, RuleEvaluationResult } from '../rules/PrivacyPolicyRuleEnums';
import { AllowOrSkipPrivacyPolicyRule } from '../rules/PrivacyPolicyRuleTypes';
type BlahFields = {
id: string;
@@ -172,18 +173,20 @@ class SkipAllPolicy extends EntityPrivacyPolicy<BlahFields, string, ViewerContex
];
}
class AlwaysThrowPrivacyPolicyRule extends PrivacyPolicyRule<
class AlwaysThrowPrivacyPolicyRule extends AllowOrSkipPrivacyPolicyRule<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
override complexity = RuleComplexity.LOW;
evaluateAsync(
_viewerContext: ViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_entity: BlahEntity
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.ALLOW> {
throw new Error('WooHoo!');
}
}
@@ -0,0 +1,271 @@
import { mock, instance } from 'ts-mockito';
import Entity from '../Entity';
import { EntityCompanionDefinition } from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField } from '../EntityFields';
import EntityPrivacyPolicy, {
EntityAuthorizationAction,
EntityPrivacyPolicyEvaluationContext,
} from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ViewerContext from '../ViewerContext';
import IEntityMetricsAdapter from '../metrics/IEntityMetricsAdapter';
import { RuleComplexity, RuleEvaluationResult } from '../rules/PrivacyPolicyRuleEnums';
import {
AllowOrSkipPrivacyPolicyRule,
DenyOrSkipPrivacyPolicyRule,
SkipPrivacyPolicyRule,
} from '../rules/PrivacyPolicyRuleTypes';
type BlahFields = {
id: string;
};
class BlahEntity extends Entity<BlahFields, string, ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
BlahFields,
string,
ViewerContext,
BlahEntity,
any
> {
return {
entityClass: BlahEntity,
entityConfiguration: new EntityConfiguration<BlahFields>({
idField: 'id',
tableName: 'blah_table',
schema: {
id: new UUIDField({
columnName: 'id',
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
}),
privacyPolicyClass: ReorderablePolicy,
};
}
}
class LargeAsyncComplexityAllowRule extends AllowOrSkipPrivacyPolicyRule<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP | RuleEvaluationResult.ALLOW> {
return RuleEvaluationResult.SKIP;
}
override complexity = RuleComplexity.HIGH;
}
class ConstantTimeComplexityAllowRule extends AllowOrSkipPrivacyPolicyRule<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP | RuleEvaluationResult.ALLOW> {
return RuleEvaluationResult.SKIP;
}
override complexity = RuleComplexity.LOW;
}
class LargeAsyncComplexitySkipRule extends SkipPrivacyPolicyRule<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP> {
return RuleEvaluationResult.SKIP;
}
override complexity = RuleComplexity.HIGH;
}
class ConstantTimeComplexitySkipRule extends SkipPrivacyPolicyRule<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP> {
return RuleEvaluationResult.SKIP;
}
override complexity = RuleComplexity.LOW;
}
class LargeAsyncComplexityDenyRule extends DenyOrSkipPrivacyPolicyRule<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP | RuleEvaluationResult.DENY> {
return RuleEvaluationResult.SKIP;
}
override complexity = RuleComplexity.HIGH;
}
class ConstantTimeComplexityDenyRule extends DenyOrSkipPrivacyPolicyRule<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
override async evaluateAsync(): Promise<RuleEvaluationResult.SKIP | RuleEvaluationResult.DENY> {
return RuleEvaluationResult.SKIP;
}
override complexity = RuleComplexity.LOW;
}
class ReorderablePolicy extends EntityPrivacyPolicy<BlahFields, string, ViewerContext, BlahEntity> {
protected override async shouldAutoReorderRulesAccordingToComplexityAsync(
_viewerContext: ViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_action: EntityAuthorizationAction
): Promise<boolean> {
return true;
}
protected override readonly createRules = [
new LargeAsyncComplexityDenyRule(),
new ConstantTimeComplexityDenyRule(),
new LargeAsyncComplexityAllowRule(),
new ConstantTimeComplexitySkipRule(),
new ConstantTimeComplexityAllowRule(),
new LargeAsyncComplexityDenyRule(),
new LargeAsyncComplexitySkipRule(),
];
protected override readonly readRules = [];
protected override readonly updateRules = [];
protected override readonly deleteRules = [];
}
class NonReorderablePolicy extends EntityPrivacyPolicy<
BlahFields,
string,
ViewerContext,
BlahEntity
> {
protected override readonly createRules = [
new LargeAsyncComplexityDenyRule(),
new ConstantTimeComplexityDenyRule(),
new LargeAsyncComplexityAllowRule(),
new ConstantTimeComplexitySkipRule(),
new ConstantTimeComplexityAllowRule(),
new LargeAsyncComplexityDenyRule(),
new LargeAsyncComplexitySkipRule(),
];
protected override readonly readRules = [];
protected override readonly updateRules = [];
protected override readonly deleteRules = [];
}
describe(EntityPrivacyPolicy, () => {
test('reorderable policy', async () => {
const viewerContext = instance(mock(ViewerContext));
const queryContext = instance(mock(EntityQueryContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
const metricsAdapter = instance(metricsAdapterMock);
const entity = new BlahEntity({
viewerContext,
id: '1',
databaseFields: { id: '1' },
selectedFields: { id: '1' },
});
const policy = new ReorderablePolicy();
const spy0 = jest.spyOn(policy['createRules'][0]!, 'evaluateAsync');
const spy1 = jest.spyOn(policy['createRules'][1]!, 'evaluateAsync');
const spy2 = jest.spyOn(policy['createRules'][2]!, 'evaluateAsync');
const spy3 = jest.spyOn(policy['createRules'][3]!, 'evaluateAsync');
const spy4 = jest.spyOn(policy['createRules'][4]!, 'evaluateAsync');
const spy5 = jest.spyOn(policy['createRules'][5]!, 'evaluateAsync');
const spy6 = jest.spyOn(policy['createRules'][6]!, 'evaluateAsync');
// this will deny since all rules skip, but we only care about rule evaluation order
try {
await policy.authorizeCreateAsync(
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
entity,
metricsAdapter
);
} catch {}
const spy0CallOrder = spy0.mock.invocationCallOrder[0]!;
const spy1CallOrder = spy1.mock.invocationCallOrder[0]!;
const spy2CallOrder = spy2.mock.invocationCallOrder[0]!;
const spy3CallOrder = spy3.mock.invocationCallOrder[0]!;
const spy4CallOrder = spy4.mock.invocationCallOrder[0]!;
const spy5CallOrder = spy5.mock.invocationCallOrder[0]!;
const spy6CallOrder = spy6.mock.invocationCallOrder[0]!;
// expected reordering: 1, 0, 3, 4, 2, 5, 6
expect(spy1CallOrder).toBeLessThan(spy0CallOrder);
expect(spy0CallOrder).toBeLessThan(spy3CallOrder);
expect(spy3CallOrder).toBeLessThan(spy4CallOrder);
expect(spy4CallOrder).toBeLessThan(spy2CallOrder);
expect(spy2CallOrder).toBeLessThan(spy5CallOrder);
expect(spy5CallOrder).toBeLessThan(spy6CallOrder);
});
test('non-reorderable policy', async () => {
const viewerContext = instance(mock(ViewerContext));
const queryContext = instance(mock(EntityQueryContext));
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
const metricsAdapterMock = mock<IEntityMetricsAdapter>();
const metricsAdapter = instance(metricsAdapterMock);
const entity = new BlahEntity({
viewerContext,
id: '1',
databaseFields: { id: '1' },
selectedFields: { id: '1' },
});
const policy = new NonReorderablePolicy();
const spy0 = jest.spyOn(policy['createRules'][0]!, 'evaluateAsync');
const spy1 = jest.spyOn(policy['createRules'][1]!, 'evaluateAsync');
const spy2 = jest.spyOn(policy['createRules'][2]!, 'evaluateAsync');
const spy3 = jest.spyOn(policy['createRules'][3]!, 'evaluateAsync');
const spy4 = jest.spyOn(policy['createRules'][4]!, 'evaluateAsync');
const spy5 = jest.spyOn(policy['createRules'][5]!, 'evaluateAsync');
const spy6 = jest.spyOn(policy['createRules'][6]!, 'evaluateAsync');
// this will deny since all rules skip, but we only care about rule evaluation order
try {
await policy.authorizeCreateAsync(
viewerContext,
queryContext,
privacyPolicyEvaluationContext,
entity,
metricsAdapter
);
} catch {}
const spy0CallOrder = spy0.mock.invocationCallOrder[0]!;
const spy1CallOrder = spy1.mock.invocationCallOrder[0]!;
const spy2CallOrder = spy2.mock.invocationCallOrder[0]!;
const spy3CallOrder = spy3.mock.invocationCallOrder[0]!;
const spy4CallOrder = spy4.mock.invocationCallOrder[0]!;
const spy5CallOrder = spy5.mock.invocationCallOrder[0]!;
const spy6CallOrder = spy6.mock.invocationCallOrder[0]!;
// expected reordering: 0, 1, 2, 3, 4, 5, 6
expect(spy0CallOrder).toBeLessThan(spy1CallOrder);
expect(spy1CallOrder).toBeLessThan(spy2CallOrder);
expect(spy2CallOrder).toBeLessThan(spy3CallOrder);
expect(spy3CallOrder).toBeLessThan(spy4CallOrder);
expect(spy4CallOrder).toBeLessThan(spy5CallOrder);
expect(spy5CallOrder).toBeLessThan(spy6CallOrder);
});
});
@@ -2,11 +2,11 @@ import { anyOfClass, anything, deepEqual, instance, mock, spy, verify, when } fr
import { EntityNonTransactionalQueryContext } from '../EntityQueryContext';
import EntitySecondaryCacheLoader, { ISecondaryEntityCache } from '../EntitySecondaryCacheLoader';
import ViewerContext from '../ViewerContext';
import SimpleTestEntity, {
SimpleTestEntityPrivacyPolicy,
SimpleTestFields,
} from '../testfixtures/SimpleTestEntity';
import TestViewerContext from '../testfixtures/TestViewerContext';
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
type TestLoadParams = { id: string };
@@ -15,7 +15,7 @@ class TestSecondaryRedisCacheLoader extends EntitySecondaryCacheLoader<
TestLoadParams,
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestEntityPrivacyPolicy
> {
@@ -30,12 +30,9 @@ class TestSecondaryRedisCacheLoader extends EntitySecondaryCacheLoader<
describe(EntitySecondaryCacheLoader, () => {
describe('loadManyAsync', () => {
it('calls into secondary cache with correct params', async () => {
const vc1 = new TestViewerContext(createUnitTestEntityCompanionProvider());
const vc1 = new ViewerContext(createUnitTestEntityCompanionProvider());
const createdEntity = await SimpleTestEntity.creator(
vc1,
vc1.getQueryContext()
).enforceCreateAsync();
const createdEntity = await SimpleTestEntity.creator(vc1).enforceCreateAsync();
const loadParams = { id: createdEntity.getID() };
const secondaryEntityCacheMock =
@@ -47,7 +44,7 @@ describe(EntitySecondaryCacheLoader, () => {
const secondaryCacheLoader = new TestSecondaryRedisCacheLoader(
secondaryEntityCache,
SimpleTestEntity.loader(vc1, vc1.getQueryContext())
SimpleTestEntity.loader(vc1)
);
await secondaryCacheLoader.loadManyAsync([loadParams]);
@@ -58,12 +55,9 @@ describe(EntitySecondaryCacheLoader, () => {
});
it('constructs and authorizes entities', async () => {
const vc1 = new TestViewerContext(createUnitTestEntityCompanionProvider());
const vc1 = new ViewerContext(createUnitTestEntityCompanionProvider());
const createdEntity = await SimpleTestEntity.creator(
vc1,
vc1.getQueryContext()
).enforceCreateAsync();
const createdEntity = await SimpleTestEntity.creator(vc1).enforceCreateAsync();
const loadParams = { id: createdEntity.getID() };
const secondaryEntityCacheMock =
@@ -73,7 +67,7 @@ describe(EntitySecondaryCacheLoader, () => {
).thenResolve(new Map([[loadParams, createdEntity.getAllFields()]]));
const secondaryEntityCache = instance(secondaryEntityCacheMock);
const loader = SimpleTestEntity.loader(vc1, vc1.getQueryContext());
const loader = SimpleTestEntity.loader(vc1);
const spiedPrivacyPolicy = spy(loader['privacyPolicy']);
const secondaryCacheLoader = new TestSecondaryRedisCacheLoader(secondaryEntityCache, loader);
@@ -94,18 +88,15 @@ describe(EntitySecondaryCacheLoader, () => {
describe('invalidateManyAsync', () => {
it('calls invalidate on the secondary cache', async () => {
const vc1 = new TestViewerContext(createUnitTestEntityCompanionProvider());
const vc1 = new ViewerContext(createUnitTestEntityCompanionProvider());
const createdEntity = await SimpleTestEntity.creator(
vc1,
vc1.getQueryContext()
).enforceCreateAsync();
const createdEntity = await SimpleTestEntity.creator(vc1).enforceCreateAsync();
const loadParams = { id: createdEntity.getID() };
const secondaryEntityCacheMock =
mock<ISecondaryEntityCache<SimpleTestFields, TestLoadParams>>();
const secondaryEntityCache = instance(secondaryEntityCacheMock);
const loader = SimpleTestEntity.loader(vc1, vc1.getQueryContext());
const loader = SimpleTestEntity.loader(vc1);
const secondaryCacheLoader = new TestSecondaryRedisCacheLoader(secondaryEntityCache, loader);
await secondaryCacheLoader.invalidateManyAsync([loadParams]);
@@ -87,52 +87,34 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const parentCategory = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const subCategory = await CategoryEntity.creator(viewerContext, viewerContext.getQueryContext())
const parentCategory = await CategoryEntity.creator(viewerContext).enforceCreateAsync();
const subCategory = await CategoryEntity.creator(viewerContext)
.setField('parent_category_id', parentCategory.getID())
.enforceCreateAsync();
const subSubCategory = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
)
const subSubCategory = await CategoryEntity.creator(viewerContext)
.setField('parent_category_id', subCategory.getID())
.enforceCreateAsync();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(subCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(subSubCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
).resolves.not.toBeNull();
await CategoryEntity.enforceDeleteAsync(parentCategory, viewerContext.getQueryContext());
await CategoryEntity.enforceDeleteAsync(parentCategory);
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
).resolves.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(subCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
).resolves.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(subSubCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
).resolves.toBeNull();
});
@@ -142,28 +124,21 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const categoryA = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const categoryB = await CategoryEntity.creator(viewerContext, viewerContext.getQueryContext())
const categoryA = await CategoryEntity.creator(viewerContext).enforceCreateAsync();
const categoryB = await CategoryEntity.creator(viewerContext)
.setField('parent_category_id', categoryA.getID())
.enforceCreateAsync();
await CategoryEntity.updater(categoryA, viewerContext.getQueryContext())
await CategoryEntity.updater(categoryA)
.setField('parent_category_id', categoryB.getID())
.enforceUpdateAsync();
await CategoryEntity.enforceDeleteAsync(categoryA, viewerContext.getQueryContext());
await CategoryEntity.enforceDeleteAsync(categoryA);
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(categoryA.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(categoryA.getID())
).resolves.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(categoryB.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(categoryB.getID())
).resolves.toBeNull();
});
});
@@ -175,56 +150,36 @@ describe('EntityEdgeDeletionBehavior.SET_NULL', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const parentCategory = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const subCategory = await CategoryEntity.creator(viewerContext, viewerContext.getQueryContext())
const parentCategory = await CategoryEntity.creator(viewerContext).enforceCreateAsync();
const subCategory = await CategoryEntity.creator(viewerContext)
.setField('parent_category_id', parentCategory.getID())
.enforceCreateAsync();
const subSubCategory = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
)
const subSubCategory = await CategoryEntity.creator(viewerContext)
.setField('parent_category_id', subCategory.getID())
.enforceCreateAsync();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(subCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(subSubCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
).resolves.not.toBeNull();
await CategoryEntity.enforceDeleteAsync(parentCategory, viewerContext.getQueryContext());
await CategoryEntity.enforceDeleteAsync(parentCategory);
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
).resolves.toBeNull();
const loadedSubCategory = await CategoryEntity.loader(
viewerContext,
viewerContext.getQueryContext()
)
const loadedSubCategory = await CategoryEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(subCategory.getID());
expect(loadedSubCategory.getField('parent_category_id')).toBeNull();
const loadedSubSubCategory = await CategoryEntity.loader(
viewerContext,
viewerContext.getQueryContext()
)
const loadedSubSubCategory = await CategoryEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(subSubCategory.getID());
expect(loadedSubSubCategory.getField('parent_category_id')).not.toBeNull();
@@ -236,23 +191,17 @@ describe('EntityEdgeDeletionBehavior.SET_NULL', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const categoryA = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const categoryB = await CategoryEntity.creator(viewerContext, viewerContext.getQueryContext())
const categoryA = await CategoryEntity.creator(viewerContext).enforceCreateAsync();
const categoryB = await CategoryEntity.creator(viewerContext)
.setField('parent_category_id', categoryA.getID())
.enforceCreateAsync();
await CategoryEntity.updater(categoryA, viewerContext.getQueryContext())
await CategoryEntity.updater(categoryA)
.setField('parent_category_id', categoryB.getID())
.enforceUpdateAsync();
await CategoryEntity.enforceDeleteAsync(categoryA, viewerContext.getQueryContext());
await CategoryEntity.enforceDeleteAsync(categoryA);
const loadedCategoryB = await CategoryEntity.loader(
viewerContext,
viewerContext.getQueryContext()
)
const loadedCategoryB = await CategoryEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(categoryB.getID());
expect(loadedCategoryB.getField('parent_category_id')).toBeNull();
@@ -268,32 +217,24 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const parentCategory = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const subCategory = await CategoryEntity.creator(viewerContext, viewerContext.getQueryContext())
const parentCategory = await CategoryEntity.creator(viewerContext).enforceCreateAsync();
const subCategory = await CategoryEntity.creator(viewerContext)
.setField('parent_category_id', parentCategory.getID())
.enforceCreateAsync();
const subSubCategory = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
)
const subSubCategory = await CategoryEntity.creator(viewerContext)
.setField('parent_category_id', subCategory.getID())
.enforceCreateAsync();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
CategoryEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_category_id', parentCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
CategoryEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_category_id', subCategory.getID())
).resolves.not.toBeNull();
@@ -314,7 +255,7 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
);
expect(subSubCategoryCachedBefore.get(subCategory.getID())?.status).toEqual(CacheStatus.HIT);
await CategoryEntity.enforceDeleteAsync(parentCategory, viewerContext.getQueryContext());
await CategoryEntity.enforceDeleteAsync(parentCategory);
const subCategoryCachedAfter = await categoryCacheAdapter.loadManyAsync('parent_category_id', [
parentCategory.getID(),
@@ -328,19 +269,13 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
expect(subSubCategoryCachedAfter.get(subCategory.getID())?.status).toEqual(CacheStatus.MISS);
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(parentCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parentCategory.getID())
).resolves.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(subCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subCategory.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDNullableAsync(subSubCategory.getID())
CategoryEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(subSubCategory.getID())
).resolves.not.toBeNull();
});
@@ -352,24 +287,21 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const categoryA = await CategoryEntity.creator(
viewerContext,
viewerContext.getQueryContext()
).enforceCreateAsync();
const categoryB = await CategoryEntity.creator(viewerContext, viewerContext.getQueryContext())
const categoryA = await CategoryEntity.creator(viewerContext).enforceCreateAsync();
const categoryB = await CategoryEntity.creator(viewerContext)
.setField('parent_category_id', categoryA.getID())
.enforceCreateAsync();
await CategoryEntity.updater(categoryA, viewerContext.getQueryContext())
await CategoryEntity.updater(categoryA)
.setField('parent_category_id', categoryB.getID())
.enforceUpdateAsync();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
CategoryEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_category_id', categoryA.getID())
).resolves.not.toBeNull();
await expect(
CategoryEntity.loader(viewerContext, viewerContext.getQueryContext())
CategoryEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('parent_category_id', categoryB.getID())
).resolves.not.toBeNull();
@@ -386,7 +318,7 @@ describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
expect(categoriesCachedBefore.get(categoryA.getID())?.status).toEqual(CacheStatus.HIT);
expect(categoriesCachedBefore.get(categoryB.getID())?.status).toEqual(CacheStatus.HIT);
await CategoryEntity.enforceDeleteAsync(categoryA, viewerContext.getQueryContext());
await CategoryEntity.enforceDeleteAsync(categoryA);
const categoriesCachedAfter = await categoryCacheAdapter.loadManyAsync('parent_category_id', [
categoryA.getID(),
@@ -3,15 +3,15 @@ import { instance, mock } from 'ts-mockito';
import EntityAssociationLoader from '../EntityAssociationLoader';
import EntityLoader from '../EntityLoader';
import ReadonlyEntity from '../ReadonlyEntity';
import ViewerContext from '../ViewerContext';
import SimpleTestEntity from '../testfixtures/SimpleTestEntity';
import TestEntity from '../testfixtures/TestEntity';
import TestViewerContext from '../testfixtures/TestViewerContext';
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
describe(ReadonlyEntity, () => {
describe('getID', () => {
it('returns correct value', () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const data = {
id: 'what',
};
@@ -27,7 +27,7 @@ describe(ReadonlyEntity, () => {
describe('toString', () => {
it('returns correct value', () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const data = {
id: 'what',
};
@@ -43,7 +43,7 @@ describe(ReadonlyEntity, () => {
describe('getUniqueIdentifier', () => {
it('returns a different value for two different entities of the same type', () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const testEntity = new SimpleTestEntity({
viewerContext,
id: '1',
@@ -68,8 +68,8 @@ describe(ReadonlyEntity, () => {
});
it('returns the same value even if different viewer context', () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext2 = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const viewerContext2 = instance(mock(ViewerContext));
const data = { id: '1' };
const testEntity = new SimpleTestEntity({
viewerContext,
@@ -87,7 +87,7 @@ describe(ReadonlyEntity, () => {
});
it('returns a different value for different entities even if same ID', () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const data = { id: '1' };
const testEntity = new SimpleTestEntity({
viewerContext,
@@ -114,7 +114,7 @@ describe(ReadonlyEntity, () => {
});
it('cannot be created without an ID', () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const dataWithoutID = {};
expect(() => {
// eslint-disable-next-line no-new
@@ -128,7 +128,7 @@ describe(ReadonlyEntity, () => {
});
it('returns correct viewerCo}ntext from instantiation', () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const data = {
id: 'what',
};
@@ -142,7 +142,7 @@ describe(ReadonlyEntity, () => {
});
it('returns correct data for field getters', () => {
const viewerContext = instance(mock(TestViewerContext));
const viewerContext = instance(mock(ViewerContext));
const data = {
id: 'what',
};
@@ -159,7 +159,7 @@ describe(ReadonlyEntity, () => {
describe('associationLoader', () => {
it('returns a new association loader', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
const data = {
id: 'what',
};
@@ -176,10 +176,8 @@ describe(ReadonlyEntity, () => {
describe('loader', () => {
it('creates a new EntityLoader', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
expect(
SimpleTestEntity.loader(viewerContext, viewerContext.getQueryContext())
).toBeInstanceOf(EntityLoader);
const viewerContext = new ViewerContext(companionProvider);
expect(SimpleTestEntity.loader(viewerContext)).toBeInstanceOf(EntityLoader);
});
});
});
@@ -3,12 +3,11 @@ import ViewerContext from '../ViewerContext';
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
describe(ViewerContext, () => {
describe('getNonTransactionalQueryContextForDatabaseAdaptorFlavor', () => {
describe('getQueryContextForDatabaseAdaptorFlavor', () => {
it('creates a new regular query context', () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new ViewerContext(companionProvider);
const queryContext =
viewerContext.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres');
const queryContext = viewerContext.getQueryContextForDatabaseAdaptorFlavor('postgres');
expect(queryContext).toBeInstanceOf(EntityQueryContext);
expect(queryContext.isInTransaction()).toBe(false);
});
@@ -1,24 +1,24 @@
import Entity from '../../Entity';
import { EntityCompanionDefinition } from '../../EntityCompanionProvider';
import EntityConfiguration from '../../EntityConfiguration';
import { UUIDField, EnumField, StringField } from '../../EntityFields';
import { UUIDField, StringField, StrictEnumField } from '../../EntityFields';
import EntityPrivacyPolicy from '../../EntityPrivacyPolicy';
import ViewerContext from '../../ViewerContext';
import { successfulResults, failedResults } from '../../entityUtils';
import AlwaysAllowPrivacyPolicyRule from '../../rules/AlwaysAllowPrivacyPolicyRule';
import TestViewerContext from '../../testfixtures/TestViewerContext';
import { createUnitTestEntityCompanionProvider } from '../../utils/testing/createUnitTestEntityCompanionProvider';
describe('Two entities backed by the same table', () => {
test('load by different types', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
const one = await OneTestEntity.creator(viewerContext, viewerContext.getQueryContext())
const one = await OneTestEntity.creator(viewerContext)
.setField('entity_type', EntityType.ONE)
.setField('common_other_field', 'wat')
.enforceCreateAsync();
const two = await TwoTestEntity.creator(viewerContext, viewerContext.getQueryContext())
const two = await TwoTestEntity.creator(viewerContext)
.setField('entity_type', EntityType.TWO)
.setField('other_field', 'blah')
.setField('common_other_field', 'wat')
@@ -28,21 +28,17 @@ describe('Two entities backed by the same table', () => {
expect(two).toBeInstanceOf(TwoTestEntity);
await expect(
TwoTestEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDAsync(one.getID())
TwoTestEntity.loader(viewerContext).enforcing().loadByIDAsync(one.getID())
).rejects.toThrowError('TwoTestEntity must be instantiated with two data');
await expect(
OneTestEntity.loader(viewerContext, viewerContext.getQueryContext())
.enforcing()
.loadByIDAsync(two.getID())
OneTestEntity.loader(viewerContext).enforcing().loadByIDAsync(two.getID())
).rejects.toThrowError('OneTestEntity must be instantiated with one data');
const manyResults = await OneTestEntity.loader(
viewerContext,
viewerContext.getQueryContext()
).loadManyByFieldEqualingAsync('common_other_field', 'wat');
const manyResults = await OneTestEntity.loader(viewerContext).loadManyByFieldEqualingAsync(
'common_other_field',
'wat'
);
const successfulManyResults = successfulResults(manyResults);
const failedManyResults = failedResults(manyResults);
@@ -55,8 +51,7 @@ describe('Two entities backed by the same table', () => {
);
const fieldEqualityConjunctionResults = await OneTestEntity.loader(
viewerContext,
viewerContext.getQueryContext()
viewerContext
).loadManyByFieldEqualityConjunctionAsync([
{
fieldName: 'common_other_field',
@@ -103,38 +98,33 @@ const testEntityConfiguration = new EntityConfiguration<TestFields>({
common_other_field: new StringField({
columnName: 'common_other_field',
}),
entity_type: new EnumField({
entity_type: new StrictEnumField({
columnName: 'entity_type',
enum: EntityType,
}),
},
databaseAdapterFlavor: 'postgres',
cacheAdapterFlavor: 'redis',
});
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
any,
string,
TestViewerContext,
any,
any
> {
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<any, string, ViewerContext, any, any> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
}
class OneTestEntity extends Entity<TestFields, string, TestViewerContext, OneTestFields> {
class OneTestEntity extends Entity<TestFields, string, ViewerContext, OneTestFields> {
constructor(constructorParams: {
viewerContext: TestViewerContext;
viewerContext: ViewerContext;
id: string;
databaseFields: Readonly<TestFields>;
selectedFields: Readonly<Pick<TestFields, OneTestFields>>;
@@ -148,7 +138,7 @@ class OneTestEntity extends Entity<TestFields, string, TestViewerContext, OneTes
static defineCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
TestViewerContext,
ViewerContext,
OneTestEntity,
TestEntityPrivacyPolicy,
OneTestFields
@@ -162,9 +152,9 @@ class OneTestEntity extends Entity<TestFields, string, TestViewerContext, OneTes
}
}
class TwoTestEntity extends Entity<TestFields, string, TestViewerContext, TwoTestFields> {
class TwoTestEntity extends Entity<TestFields, string, ViewerContext, TwoTestFields> {
constructor(constructorParams: {
viewerContext: TestViewerContext;
viewerContext: ViewerContext;
id: string;
databaseFields: Readonly<TestFields>;
selectedFields: Readonly<Pick<TestFields, TwoTestFields>>;
@@ -178,7 +168,7 @@ class TwoTestEntity extends Entity<TestFields, string, TestViewerContext, TwoTes
static defineCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
TestViewerContext,
ViewerContext,
TwoTestEntity,
TestEntityPrivacyPolicy,
TwoTestFields
@@ -3,26 +3,26 @@ import { EntityCompanionDefinition } from '../../EntityCompanionProvider';
import EntityConfiguration from '../../EntityConfiguration';
import { UUIDField, StringField } from '../../EntityFields';
import EntityPrivacyPolicy from '../../EntityPrivacyPolicy';
import ViewerContext from '../../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../../rules/AlwaysAllowPrivacyPolicyRule';
import TestViewerContext from '../../testfixtures/TestViewerContext';
import { createUnitTestEntityCompanionProvider } from '../../utils/testing/createUnitTestEntityCompanionProvider';
describe('Two entities backed by the same table', () => {
test('mutate through different types and keep consistent cache and dataloader', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
const entity1 = await OneTestEntity.creator(viewerContext, viewerContext.getQueryContext())
const entity1 = await OneTestEntity.creator(viewerContext)
.setField('fake_field', 'hello')
.enforceCreateAsync();
expect(entity1).toBeInstanceOf(OneTestEntity);
const entity2 = await TwoTestEntity.loader(viewerContext, viewerContext.getQueryContext())
const entity2 = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1.getID());
expect(entity2).toBeInstanceOf(TwoTestEntity);
const updated2 = await TwoTestEntity.updater(entity2, viewerContext.getQueryContext())
const updated2 = await TwoTestEntity.updater(entity2)
.setField('fake_field', 'world')
.setField('other_field', 'wat')
.enforceUpdateAsync();
@@ -32,7 +32,7 @@ describe('Two entities backed by the same table', () => {
fake_field: 'world',
});
const loaded1 = await OneTestEntity.loader(viewerContext, viewerContext.getQueryContext())
const loaded1 = await OneTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity1.getID());
expect(loaded1.getAllFields()).toMatchObject({
@@ -43,14 +43,14 @@ describe('Two entities backed by the same table', () => {
test('cached field that differs between the two to test invalidation', async () => {
const companionProvider = createUnitTestEntityCompanionProvider();
const viewerContext = new TestViewerContext(companionProvider);
const viewerContext = new ViewerContext(companionProvider);
const entity = await TwoTestEntity.creator(viewerContext, viewerContext.getQueryContext())
const entity = await TwoTestEntity.creator(viewerContext)
.setField('fake_field', 'hello')
.setField('other_field', 'huh')
.enforceCreateAsync();
const loadedEntity = await TwoTestEntity.loader(viewerContext, viewerContext.getQueryContext())
const loadedEntity = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('other_field', 'huh');
expect(loadedEntity?.getAllFields()).toMatchObject({
@@ -59,14 +59,12 @@ describe('Two entities backed by the same table', () => {
other_field: 'huh',
});
const loaded1 = await OneTestEntity.loader(viewerContext, viewerContext.getQueryContext())
const loaded1 = await OneTestEntity.loader(viewerContext)
.enforcing()
.loadByIDAsync(entity.getID());
await OneTestEntity.updater(loaded1, viewerContext.getQueryContext())
.setField('fake_field', 'world')
.enforceUpdateAsync();
await OneTestEntity.updater(loaded1).setField('fake_field', 'world').enforceUpdateAsync();
const loaded2 = await TwoTestEntity.loader(viewerContext, viewerContext.getQueryContext())
const loaded2 = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('other_field', 'huh');
expect(loaded2?.getAllFields()).toMatchObject({
@@ -75,7 +73,7 @@ describe('Two entities backed by the same table', () => {
other_field: 'huh',
});
const loaded22 = await TwoTestEntity.loader(viewerContext, viewerContext.getQueryContext())
const loaded22 = await TwoTestEntity.loader(viewerContext)
.enforcing()
.loadByFieldEqualingAsync('fake_field', 'world');
expect(loaded22?.getAllFields()).toMatchObject({
@@ -116,32 +114,26 @@ const testEntityConfiguration = new EntityConfiguration<TestFields>({
cacheAdapterFlavor: 'redis',
});
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
any,
string,
TestViewerContext,
any,
any
> {
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<any, string, ViewerContext, any, any> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
new AlwaysAllowPrivacyPolicyRule<any, string, ViewerContext, any, any>(),
];
}
class OneTestEntity extends Entity<TestFields, string, TestViewerContext, OneTestFields> {
class OneTestEntity extends Entity<TestFields, string, ViewerContext, OneTestFields> {
static defineCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
TestViewerContext,
ViewerContext,
OneTestEntity,
TestEntityPrivacyPolicy,
OneTestFields
@@ -155,11 +147,11 @@ class OneTestEntity extends Entity<TestFields, string, TestViewerContext, OneTes
}
}
class TwoTestEntity extends Entity<TestFields, string, TestViewerContext, TwoTestFields> {
class TwoTestEntity extends Entity<TestFields, string, ViewerContext, TwoTestFields> {
static defineCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
TestViewerContext,
ViewerContext,
TwoTestEntity,
TestEntityPrivacyPolicy,
TwoTestFields
+2 -2
View File
@@ -66,8 +66,8 @@ export { default as NoOpEntityMetricsAdapter } from './metrics/NoOpEntityMetrics
export { default as AlwaysAllowPrivacyPolicyRule } from './rules/AlwaysAllowPrivacyPolicyRule';
export { default as AlwaysDenyPrivacyPolicyRule } from './rules/AlwaysDenyPrivacyPolicyRule';
export { default as AlwaysSkipPrivacyPolicyRule } from './rules/AlwaysSkipPrivacyPolicyRule';
export { default as PrivacyPolicyRule } from './rules/PrivacyPolicyRule';
export * from './rules/PrivacyPolicyRule';
export * from './rules/PrivacyPolicyRuleEnums';
export * from './rules/PrivacyPolicyRuleTypes';
export * from './utils/testing/PrivacyPolicyRuleTestUtils';
export * from './utils/testing/StubCacheAdapter';
export { default as describeFieldTestCase } from './utils/testing/describeFieldTestCase';
@@ -76,7 +76,7 @@ export default class EntityDataManager<TFields> {
entityClassName: this.entityClassName,
});
return await this.databaseAdapter.fetchManyWhereAsync(
this.queryContextProvider.getNonTransactionalQueryContext(),
this.queryContextProvider.getQueryContext(),
fieldName,
fetcherValues
);
@@ -78,7 +78,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
@@ -126,7 +126,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
@@ -174,7 +174,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
// use second data manager to ensure that cache is hit instead of data loader
const entityDataManager2 = new EntityDataManager(
databaseAdapter,
@@ -218,7 +218,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
@@ -254,7 +254,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyWhereAsync');
const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
@@ -298,7 +298,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const objectInQuestion = objects.get(testEntityConfiguration.tableName)![1]!;
@@ -337,7 +337,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const objectInQuestion = objects.get(testEntityConfiguration.tableName)![1]!;
@@ -415,7 +415,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
const dbSpy = jest.spyOn(databaseAdapter, 'fetchManyByFieldEqualityConjunctionAsync');
const cacheSpy = jest.spyOn(entityCache, 'readManyThroughAsync');
@@ -461,7 +461,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
await expect(
entityDataManager.loadManyByFieldEqualingAsync(queryContext, 'customIdField', ['2'])
@@ -488,7 +488,7 @@ describe(EntityDataManager, () => {
metricsAdapter,
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
await entityDataManager.loadManyByFieldEqualingAsync(queryContext, 'customIdField', ['1']);
verify(
@@ -591,7 +591,7 @@ describe(EntityDataManager, () => {
new NoOpEntityMetricsAdapter(),
TestEntity.name
);
const queryContext = StubQueryContextProvider.getNonTransactionalQueryContext();
const queryContext = StubQueryContextProvider.getQueryContext();
await expect(
entityDataManager.loadManyByFieldEqualingAsync(queryContext, 'nullableField', [null as any])
@@ -1,4 +1,5 @@
import PrivacyPolicyRule, { RuleEvaluationResult } from './PrivacyPolicyRule';
import { RuleComplexity, RuleEvaluationResult } from './PrivacyPolicyRuleEnums';
import { AllowOrSkipPrivacyPolicyRule } from './PrivacyPolicyRuleTypes';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
@@ -13,13 +14,15 @@ export default class AlwaysAllowPrivacyPolicyRule<
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
> extends AllowOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
override complexity = RuleComplexity.LOW;
async evaluateAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_entity: TEntity
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.ALLOW | RuleEvaluationResult.SKIP> {
return RuleEvaluationResult.ALLOW;
}
}
@@ -1,4 +1,5 @@
import PrivacyPolicyRule, { RuleEvaluationResult } from './PrivacyPolicyRule';
import { RuleComplexity, RuleEvaluationResult } from './PrivacyPolicyRuleEnums';
import { DenyOrSkipPrivacyPolicyRule } from './PrivacyPolicyRuleTypes';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
@@ -13,13 +14,15 @@ export default class AlwaysDenyPrivacyPolicyRule<
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
> extends DenyOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
override complexity = RuleComplexity.LOW;
async evaluateAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_entity: TEntity
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.DENY | RuleEvaluationResult.SKIP> {
return RuleEvaluationResult.DENY;
}
}
@@ -1,4 +1,5 @@
import PrivacyPolicyRule, { RuleEvaluationResult } from './PrivacyPolicyRule';
import { RuleComplexity, RuleEvaluationResult } from './PrivacyPolicyRuleEnums';
import { SkipPrivacyPolicyRule } from './PrivacyPolicyRuleTypes';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
@@ -13,13 +14,15 @@ export default class AlwaysSkipPrivacyPolicyRule<
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
> extends SkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
override complexity = RuleComplexity.LOW;
async evaluateAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_entity: TEntity
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.SKIP> {
return RuleEvaluationResult.SKIP;
}
}
@@ -0,0 +1,61 @@
/**
* Enum return type of privacy policy rule evaluation.
*/
export enum RuleEvaluationResult {
/**
* Deny viewer access to the entity.
*/
DENY = -1,
/**
* Defer entity viewer access to subsequent rule in the privacy policy.
*/
SKIP = 0,
/**
* Allow viewer access to the entity.
*/
ALLOW = 1,
}
/**
* Estimated complexity of a privacy policy rule. Definition of what rules fall into what complexity category
* is application-specific, but the AlwaysAllow/AlwaysDeny/AlwaysSkip rules provided are all defined as LOW
* complexity.
*/
export enum RuleComplexity {
/**
* Rule complexity is low. Definition dependent on application.
*/
LOW = 0,
/**
* Rule complexity is medium. Definition dependent on application.
*/
MEDIUM = 1,
/**
* Rule complexity is high. Definition dependent on application.
*/
HIGH = 2,
}
/**
* Used for disambiguation between classes of privacy policy rules at runtime.
* Multiple rules of the same type in a row can be reordered safely to improve policy evaluation time
* based on complexities.
*/
export enum RuleEvaluationResultType {
/**
* Rule is a DenyOrSkipPrivacyPolicyRule. Can be safely reordered with other sequential DenyOrSkipPrivacyPolicyRule or SkipPrivacyPolicyRule in a policy.
*/
DENY_OR_SKIP,
/**
* Rule is a SkipPrivacyPolicyRule. Can be safely reordered with any other sequential privacy policy rules in a policy.
*/
SKIP,
/**
* Rule is an AllowOrSkipPrivacyPolicyRule. Can be safely reordered with other sequential AllowOrSkipPrivacyPolicyRule or SkipPrivacyPolicyRule in a policy.
*/
ALLOW_OR_SKIP,
}
@@ -0,0 +1,68 @@
import { RuleEvaluationResult, RuleEvaluationResultType } from './PrivacyPolicyRuleEnums';
import { PrivacyPolicyRuleInternal } from './internal/PrivacyPolicyRule';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
import ViewerContext from '../ViewerContext';
export type PrivacyPolicyRule<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> =
| AllowOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>
| DenyOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>
| SkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>;
export abstract class AllowOrSkipPrivacyPolicyRule<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRuleInternal<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
abstract override evaluateAsync(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: TEntity
): Promise<RuleEvaluationResult.ALLOW | RuleEvaluationResult.SKIP>;
override resultType = RuleEvaluationResultType.ALLOW_OR_SKIP;
}
export abstract class DenyOrSkipPrivacyPolicyRule<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRuleInternal<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
abstract override evaluateAsync(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: TEntity
): Promise<RuleEvaluationResult.DENY | RuleEvaluationResult.SKIP>;
override resultType = RuleEvaluationResultType.DENY_OR_SKIP;
}
export abstract class SkipPrivacyPolicyRule<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRuleInternal<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
abstract override evaluateAsync(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: TEntity
): Promise<RuleEvaluationResult.SKIP>;
override resultType = RuleEvaluationResultType.SKIP;
}
@@ -1,24 +1,12 @@
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
import ViewerContext from '../ViewerContext';
export enum RuleEvaluationResult {
/**
* Deny viewer access to the entity.
*/
DENY = -1,
/**
* Defer entity viewer access to subsequent rule in the privacy policy.
*/
SKIP = 0,
/**
* Allow viewer access to the entity.
*/
ALLOW = 1,
}
import { EntityPrivacyPolicyEvaluationContext } from '../../EntityPrivacyPolicy';
import { EntityQueryContext } from '../../EntityQueryContext';
import ReadonlyEntity from '../../ReadonlyEntity';
import ViewerContext from '../../ViewerContext';
import {
RuleComplexity,
RuleEvaluationResult,
RuleEvaluationResultType,
} from '../PrivacyPolicyRuleEnums';
/**
* A single unit of which declarative privacy policies are composed, allowing for simple
@@ -36,17 +24,25 @@ export enum RuleEvaluationResult {
* - Blocking. For example, a user blocks another user from seeing their posts, and the rule
* would be named something like `DenyIfViewerHasBeenBlockedPrivacyPolicyRule`.
*/
export default abstract class PrivacyPolicyRule<
export abstract class PrivacyPolicyRuleInternal<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> {
/**
* Estimated complexity of a privacy policy rule. Used for automatic (safe) rule reordering.
* Typed as a number so that applications can define their own complexity scale.
*/
abstract readonly complexity: RuleComplexity;
abstract evaluateAsync(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: TEntity
): Promise<RuleEvaluationResult>;
abstract readonly resultType: RuleEvaluationResultType;
}
@@ -0,0 +1,70 @@
import invariant from 'invariant';
import { RuleEvaluationResultType } from '../PrivacyPolicyRuleEnums';
/**
* Privacy policy list of PrivacyPolicyRule can be reordered safely according to
* the following constraints on RuleEvaluationResultType:
* 1. Two adjacent ALLOW_OR_SKIP rules can be swapped.
* 2. Two adjacent DENY_OR_SKIP rules can be swapped.
* 3. SKIP rules can be swapped with any other adjacent rule.
*
* @param rulesetOriginalOrder - ruleset with original ordering defined in code
* @returns reordered ruleset depending on constraints above
*/
export function reorderRulesByRuleComplexityGroups<
TRule extends { resultType: RuleEvaluationResultType; complexity: number }
>(rulesetOriginalOrder: readonly TRule[]): readonly TRule[] {
// first, group the rules according to constraints
const groups = groupRulesByResultType(rulesetOriginalOrder);
// sort the groups by declared complexity
const internallySortedGroups = groups.map((group) => sortGroupByComplexity(group));
// flatten the groups
return internallySortedGroups.flat();
}
export function sortGroupByComplexity<TRule extends { complexity: number }>(
ruleGroup: readonly TRule[]
): readonly TRule[] {
return [...ruleGroup].sort((a, b) => a.complexity - b.complexity);
}
export function groupRulesByResultType<TRule extends { resultType: RuleEvaluationResultType }>(
ruleset: readonly TRule[]
): readonly (readonly TRule[])[] {
const groups: TRule[][] = [[]];
let currentGroupIdx = 0;
let currentGroupType:
| RuleEvaluationResultType.ALLOW_OR_SKIP
| RuleEvaluationResultType.DENY_OR_SKIP
| null = null;
for (const rule of ruleset) {
// if group type of this rule is not compatible with the current group type, start a new group
if (
currentGroupType !== null &&
rule.resultType !== currentGroupType &&
rule.resultType !== RuleEvaluationResultType.SKIP
) {
currentGroupIdx += 1;
groups.push([]);
currentGroupType = null;
}
// add the current rule to the current group
const currentGroup = groups[currentGroupIdx];
invariant(currentGroup, `invalid groups index ${currentGroupIdx}`);
currentGroup.push(rule);
// if there isn't a known group type yet for the current group, set it to the group type of the rule.
// note that if there isn't a known group type yet and the current rule is SKIP, we'll
// continue to the next rule to determine group type
if (currentGroupType === null && rule.resultType !== RuleEvaluationResultType.SKIP) {
currentGroupType = rule.resultType;
}
}
return groups;
}
@@ -0,0 +1,152 @@
import { RuleComplexity, RuleEvaluationResultType } from '../../PrivacyPolicyRuleEnums';
import {
groupRulesByResultType,
reorderRulesByRuleComplexityGroups,
sortGroupByComplexity,
} from '../PrivacyPolicyRuleOrdering';
type TestRuleJustResultType = { id: number; resultType: RuleEvaluationResultType };
describe(groupRulesByResultType, () => {
const rule1 = { id: 1, resultType: RuleEvaluationResultType.ALLOW_OR_SKIP };
const rule2 = { id: 2, resultType: RuleEvaluationResultType.ALLOW_OR_SKIP };
const rule3 = { id: 3, resultType: RuleEvaluationResultType.SKIP };
const rule4 = { id: 4, resultType: RuleEvaluationResultType.SKIP };
const rule5 = { id: 5, resultType: RuleEvaluationResultType.DENY_OR_SKIP };
const rule6 = { id: 6, resultType: RuleEvaluationResultType.DENY_OR_SKIP };
test.each([
// two adjacent of the same type should be grouped
{ ruleset: [rule1, rule2], expectedResultGroups: [[rule1, rule2]] },
{ ruleset: [rule3, rule4], expectedResultGroups: [[rule3, rule4]] },
{ ruleset: [rule5, rule6], expectedResultGroups: [[rule5, rule6]] },
// two adjacent of ALLOW_OR_SKIP or DENY_OR_SKIP should not be grouped
{ ruleset: [rule1, rule5], expectedResultGroups: [[rule1], [rule5]] },
{ ruleset: [rule5, rule1], expectedResultGroups: [[rule5], [rule1]] },
// adjacent (ALLOW_OR_SKIP and SKIP) or (DENY_OR_SKIP and SKIP) should be grouped
{ ruleset: [rule1, rule2, rule3], expectedResultGroups: [[rule1, rule2, rule3]] },
{ ruleset: [rule1, rule2, rule3, rule4], expectedResultGroups: [[rule1, rule2, rule3, rule4]] },
{ ruleset: [rule5, rule6, rule3], expectedResultGroups: [[rule5, rule6, rule3]] },
{ ruleset: [rule5, rule6, rule3, rule4], expectedResultGroups: [[rule5, rule6, rule3, rule4]] },
// skip cases group with first group
{
ruleset: [rule1, rule2, rule3, rule4, rule5, rule6],
expectedResultGroups: [
[rule1, rule2, rule3, rule4],
[rule5, rule6],
],
},
// skip in first N slots should take the first group type
{
ruleset: [rule3, rule4, rule1, rule2, rule5, rule6],
expectedResultGroups: [
[rule3, rule4, rule1, rule2],
[rule5, rule6],
],
},
// lots of groups
{
ruleset: [rule1, rule5, rule3, rule2, rule6, rule4],
expectedResultGroups: [[rule1], [rule5, rule3], [rule2], [rule6, rule4]],
},
// skip in first N slots
{
ruleset: [rule3, rule4, rule5, rule1],
expectedResultGroups: [[rule3, rule4, rule5], [rule1]],
},
])(
'case %#',
({
ruleset,
expectedResultGroups,
}: {
ruleset: TestRuleJustResultType[];
expectedResultGroups: TestRuleJustResultType[][];
}) => {
const resultGroups = groupRulesByResultType(ruleset);
expect(resultGroups).toStrictEqual(expectedResultGroups);
}
);
});
type TestRuleJustComplexity = { id: number; complexity: number };
describe(sortGroupByComplexity, () => {
const rule1 = { id: 1, complexity: RuleComplexity.LOW };
const rule2 = { id: 2, complexity: RuleComplexity.LOW };
const rule3 = { id: 3, complexity: RuleComplexity.MEDIUM };
const rule5 = { id: 3, complexity: RuleComplexity.HIGH };
test.each([
{ ruleGroup: [rule1, rule2], expectedRuleGroup: [rule1, rule2] },
{ ruleGroup: [rule1, rule3], expectedRuleGroup: [rule1, rule3] },
{ ruleGroup: [rule1, rule3, rule5], expectedRuleGroup: [rule1, rule3, rule5] },
{ ruleGroup: [rule5, rule3, rule1], expectedRuleGroup: [rule1, rule3, rule5] },
])(
'case %#',
({
ruleGroup,
expectedRuleGroup,
}: {
ruleGroup: TestRuleJustComplexity[];
expectedRuleGroup: TestRuleJustComplexity[];
}) => {
const resultGroup = sortGroupByComplexity(ruleGroup);
expect(resultGroup).toStrictEqual(expectedRuleGroup);
}
);
});
type TestRule = { id: number; resultType: RuleEvaluationResultType; complexity: number };
describe(reorderRulesByRuleComplexityGroups, () => {
const rule1 = {
id: 1,
resultType: RuleEvaluationResultType.ALLOW_OR_SKIP,
complexity: RuleComplexity.HIGH,
};
const rule2 = {
id: 2,
resultType: RuleEvaluationResultType.ALLOW_OR_SKIP,
complexity: RuleComplexity.LOW,
};
const rule3 = {
id: 3,
resultType: RuleEvaluationResultType.SKIP,
complexity: RuleComplexity.LOW,
};
const rule4 = {
id: 4,
resultType: RuleEvaluationResultType.SKIP,
complexity: RuleComplexity.HIGH,
};
const rule5 = {
id: 5,
resultType: RuleEvaluationResultType.DENY_OR_SKIP,
complexity: RuleComplexity.MEDIUM,
};
const rule6 = {
id: 6,
resultType: RuleEvaluationResultType.DENY_OR_SKIP,
complexity: RuleComplexity.LOW,
};
test.each([
{
ruleset: [rule1, rule2, rule3, rule4, rule5, rule6],
expectedRuleset: [rule2, rule3, rule1, rule4, rule6, rule5],
},
])(
'case %#',
({ ruleset, expectedRuleset }: { ruleset: TestRule[]; expectedRuleset: TestRule[] }) => {
const resultRuleset = reorderRulesByRuleComplexityGroups(ruleset);
expect(resultRuleset).toStrictEqual(expectedRuleset);
}
);
});
@@ -1,9 +1,9 @@
import TestViewerContext from './TestViewerContext';
import Entity from '../Entity';
import { EntityCompanionDefinition } from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField } from '../EntityFields';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
export type SimpleTestFields = {
@@ -27,7 +27,7 @@ export const simpleTestEntityConfiguration = new EntityConfiguration<SimpleTestF
export class SimpleTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
> {
@@ -35,7 +35,7 @@ export class SimpleTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
@@ -44,7 +44,7 @@ export class SimpleTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
@@ -53,7 +53,7 @@ export class SimpleTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
@@ -62,7 +62,7 @@ export class SimpleTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
new AlwaysAllowPrivacyPolicyRule<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestFieldSelection
>(),
@@ -72,13 +72,13 @@ export class SimpleTestEntityPrivacyPolicy extends EntityPrivacyPolicy<
export default class SimpleTestEntity extends Entity<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestFieldSelection
> {
static defineCompanionDefinition(): EntityCompanionDefinition<
SimpleTestFields,
string,
TestViewerContext,
ViewerContext,
SimpleTestEntity,
SimpleTestEntityPrivacyPolicy,
SimpleTestFieldSelection
+12 -15
View File
@@ -1,11 +1,11 @@
import { result, Result } from '@expo/results';
import TestViewerContext from './TestViewerContext';
import Entity from '../Entity';
import { EntityCompanionDefinition } from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField, StringField, DateField, IntField } from '../EntityFields';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
export type TestFields = {
@@ -48,28 +48,28 @@ export const testEntityConfiguration = new EntityConfiguration<TestFields>({
export class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity
> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, string, TestViewerContext, TestEntity>(),
new AlwaysAllowPrivacyPolicyRule<TestFields, string, ViewerContext, TestEntity>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, string, TestViewerContext, TestEntity>(),
new AlwaysAllowPrivacyPolicyRule<TestFields, string, ViewerContext, TestEntity>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, string, TestViewerContext, TestEntity>(),
new AlwaysAllowPrivacyPolicyRule<TestFields, string, ViewerContext, TestEntity>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<TestFields, string, TestViewerContext, TestEntity>(),
new AlwaysAllowPrivacyPolicyRule<TestFields, string, ViewerContext, TestEntity>(),
];
}
export default class TestEntity extends Entity<TestFields, string, TestViewerContext> {
export default class TestEntity extends Entity<TestFields, string, ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
TestFields,
string,
TestViewerContext,
ViewerContext,
TestEntity,
TestEntityPrivacyPolicy
> {
@@ -84,10 +84,7 @@ export default class TestEntity extends Entity<TestFields, string, TestViewerCon
return 'Hello World!';
}
static async hello(
viewerContext: TestViewerContext,
testValue: string
): Promise<Result<TestEntity>> {
static async hello(viewerContext: ViewerContext, testValue: string): Promise<Result<TestEntity>> {
const fields = {
customIdField: testValue,
testIndexedField: 'hello',
@@ -106,15 +103,15 @@ export default class TestEntity extends Entity<TestFields, string, TestViewerCon
);
}
static async returnError(_viewerContext: TestViewerContext): Promise<Result<TestEntity>> {
static async returnError(_viewerContext: ViewerContext): Promise<Result<TestEntity>> {
return result(new Error('return entity'));
}
static async throwError(_viewerContext: TestViewerContext): Promise<Result<TestEntity>> {
static async throwError(_viewerContext: ViewerContext): Promise<Result<TestEntity>> {
throw new Error('threw entity');
}
static async nonResult(_viewerContext: TestViewerContext, testValue: string): Promise<string> {
static async nonResult(_viewerContext: ViewerContext, testValue: string): Promise<string> {
return testValue;
}
}
@@ -1,9 +1,9 @@
import TestViewerContext from './TestViewerContext';
import Entity from '../Entity';
import { EntityCompanionDefinition } from '../EntityCompanionProvider';
import EntityConfiguration from '../EntityConfiguration';
import { UUIDField } from '../EntityFields';
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
import ViewerContext from '../ViewerContext';
import AlwaysAllowPrivacyPolicyRule from '../rules/AlwaysAllowPrivacyPolicyRule';
export type Test2Fields = {
@@ -29,28 +29,28 @@ export const testEntity2Configuration = new EntityConfiguration<Test2Fields>({
export class TestEntity2PrivacyPolicy extends EntityPrivacyPolicy<
Test2Fields,
string,
TestViewerContext,
ViewerContext,
TestEntity2
> {
protected override readonly readRules = [
new AlwaysAllowPrivacyPolicyRule<Test2Fields, string, TestViewerContext, TestEntity2>(),
new AlwaysAllowPrivacyPolicyRule<Test2Fields, string, ViewerContext, TestEntity2>(),
];
protected override readonly createRules = [
new AlwaysAllowPrivacyPolicyRule<Test2Fields, string, TestViewerContext, TestEntity2>(),
new AlwaysAllowPrivacyPolicyRule<Test2Fields, string, ViewerContext, TestEntity2>(),
];
protected override readonly updateRules = [
new AlwaysAllowPrivacyPolicyRule<Test2Fields, string, TestViewerContext, TestEntity2>(),
new AlwaysAllowPrivacyPolicyRule<Test2Fields, string, ViewerContext, TestEntity2>(),
];
protected override readonly deleteRules = [
new AlwaysAllowPrivacyPolicyRule<Test2Fields, string, TestViewerContext, TestEntity2>(),
new AlwaysAllowPrivacyPolicyRule<Test2Fields, string, ViewerContext, TestEntity2>(),
];
}
export default class TestEntity2 extends Entity<Test2Fields, string, TestViewerContext> {
export default class TestEntity2 extends Entity<Test2Fields, string, ViewerContext> {
static defineCompanionDefinition(): EntityCompanionDefinition<
Test2Fields,
string,
TestViewerContext,
ViewerContext,
TestEntity2,
TestEntity2PrivacyPolicy
> {
@@ -1,14 +1,3 @@
import { EntityQueryContext, EntityTransactionalQueryContext } from '../EntityQueryContext';
import ViewerContext from '../ViewerContext';
export default class TestViewerContext extends ViewerContext {
public getQueryContext(): EntityQueryContext {
return super.getNonTransactionalQueryContextForDatabaseAdaptorFlavor('postgres');
}
public async runInTransactionAsync<TResult>(
transactionScope: (queryContext: EntityTransactionalQueryContext) => Promise<TResult>
): Promise<TResult> {
return await super.runInTransactionForDatabaseAdaptorFlavorAsync('postgres', transactionScope);
}
}
export default class TestViewerContext extends ViewerContext {}
@@ -2,7 +2,8 @@ import { EntityPrivacyPolicyEvaluationContext } from '../../EntityPrivacyPolicy'
import { EntityQueryContext } from '../../EntityQueryContext';
import ReadonlyEntity from '../../ReadonlyEntity';
import ViewerContext from '../../ViewerContext';
import PrivacyPolicyRule, { RuleEvaluationResult } from '../../rules/PrivacyPolicyRule';
import { RuleEvaluationResult } from '../../rules/PrivacyPolicyRuleEnums';
import { PrivacyPolicyRule } from '../../rules/PrivacyPolicyRuleTypes';
export interface Case<
TFields extends object,