mirror of
https://github.com/expo/entity.git
synced 2026-11-15 03:48:08 -10:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58797a50b6 | ||
|
|
d09a735264 | ||
|
|
dc3aafc6ab | ||
|
|
87532525d0 | ||
|
|
33771295c5 | ||
|
|
1bdfbd6b56 |
No files matched your search
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
+12
-30
@@ -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"
|
||||
}
|
||||
}
|
||||
+12
-36
@@ -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();
|
||||
});
|
||||
|
||||
+15
-50
@@ -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());
|
||||
|
||||
+2
-8
@@ -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"
|
||||
}
|
||||
}
|
||||
+80
-364
@@ -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();
|
||||
|
||||
+5
-30
@@ -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');
|
||||
});
|
||||
});
|
||||
+11
-57
@@ -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')
|
||||
|
||||
@@ -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-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;
|
||||
@@ -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,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"
|
||||
}
|
||||
}
|
||||
+18
-28
@@ -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');
|
||||
|
||||
|
||||
+9
-18
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+11
-23
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
+8
-14
@@ -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
|
||||
> {
|
||||
|
||||
+8
-14
@@ -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
|
||||
> {
|
||||
|
||||
-13
@@ -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"
|
||||
}
|
||||
}
|
||||
+3
-12
@@ -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"
|
||||
}
|
||||
|
||||
+3
-12
@@ -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 };
|
||||
|
||||
@@ -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",
|
||||
"version": "0.34.0",
|
||||
"version": "0.35.0",
|
||||
"description": "A privacy-first data model",
|
||||
"files": [
|
||||
"build",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+18
-22
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user