mirror of
https://github.com/expo/entity.git
synced 2026-11-15 03:48:08 -10:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58797a50b6 | ||
|
|
d09a735264 | ||
|
|
dc3aafc6ab | ||
|
|
87532525d0 | ||
|
|
33771295c5 | ||
|
|
1bdfbd6b56 | ||
|
|
0fa6f554a2 | ||
|
|
8b0b31fdde | ||
|
|
cc6f3dc1e6 | ||
|
|
9c43a56a39 | ||
|
|
ef7b452c9f | ||
|
|
659e08386f | ||
|
|
aad94aca47 |
No files matched your search
@@ -3,6 +3,40 @@
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Allow UUIDv6/7/8 in UUIDField validation ([#221](https://github.com/expo/entity/issues/221)) ([cc6f3dc](https://github.com/expo/entity/commit/cc6f3dc1e677382eec220723c37b5b883d980372))
|
||||
* require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add loadManyByIDsNullableAsync loader method ([#214](https://github.com/expo/entity/issues/214)) ([aad94ac](https://github.com/expo/entity/commit/aad94aca479ac07cc7f76b5e1a122ff85a2d1542))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.32.0"
|
||||
"version": "0.35.0"
|
||||
}
|
||||
@@ -3,6 +3,36 @@
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-local-memory
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-cache-adapter-local-memory",
|
||||
"version": "0.32.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.32.0"
|
||||
"@expo/entity": "^0.35.0"
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,24 @@ import {
|
||||
|
||||
import GenericLocalMemoryCacher, { LocalMemoryCache } from './GenericLocalMemoryCacher';
|
||||
|
||||
/**
|
||||
* Vends local memory cache adapters. An instance of this class may be shared across requests to
|
||||
* share the local memory cache.
|
||||
*/
|
||||
export default class LocalMemoryCacheAdapterProvider implements IEntityCacheAdapterProvider {
|
||||
// local memory cache adapters should be shared/reused across requests
|
||||
private static localMemoryCacheAdapterMap = new Map<string, GenericEntityCacheAdapter<any>>();
|
||||
|
||||
static getNoOpProvider(): IEntityCacheAdapterProvider {
|
||||
/**
|
||||
* @returns a no-op local memory cache adapter provider, or one that doesn't cache locally.
|
||||
*/
|
||||
static createNoOpProvider(): IEntityCacheAdapterProvider {
|
||||
return new LocalMemoryCacheAdapterProvider(<TFields>() =>
|
||||
GenericLocalMemoryCacher.createNoOpCache<TFields>()
|
||||
);
|
||||
}
|
||||
|
||||
static getProvider(
|
||||
/**
|
||||
* @returns a local memory cache adapter provider configured with the supplied options.
|
||||
*/
|
||||
static createProviderWithOptions(
|
||||
options: { maxSize?: number; ttlSeconds?: number } = {}
|
||||
): IEntityCacheAdapterProvider {
|
||||
return new LocalMemoryCacheAdapterProvider(<TFields>() =>
|
||||
@@ -26,6 +33,8 @@ export default class LocalMemoryCacheAdapterProvider implements IEntityCacheAdap
|
||||
);
|
||||
}
|
||||
|
||||
private localMemoryCacheAdapterMap = new Map<string, GenericEntityCacheAdapter<any>>();
|
||||
|
||||
private constructor(
|
||||
private readonly localMemoryCacheCreator: <TFields>() => LocalMemoryCache<TFields>
|
||||
) {}
|
||||
@@ -33,15 +42,11 @@ export default class LocalMemoryCacheAdapterProvider implements IEntityCacheAdap
|
||||
public getCacheAdapter<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
): IEntityCacheAdapter<TFields> {
|
||||
return computeIfAbsent(
|
||||
LocalMemoryCacheAdapterProvider.localMemoryCacheAdapterMap,
|
||||
entityConfiguration.tableName,
|
||||
() => {
|
||||
const localMemoryCache = this.localMemoryCacheCreator<TFields>();
|
||||
return new GenericEntityCacheAdapter(
|
||||
new GenericLocalMemoryCacher(entityConfiguration, localMemoryCache)
|
||||
);
|
||||
}
|
||||
);
|
||||
return computeIfAbsent(this.localMemoryCacheAdapterMap, entityConfiguration.tableName, () => {
|
||||
const localMemoryCache = this.localMemoryCacheCreator<TFields>();
|
||||
return new GenericEntityCacheAdapter(
|
||||
new GenericLocalMemoryCacher(entityConfiguration, localMemoryCache)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+25
-52
@@ -1,4 +1,9 @@
|
||||
import { CacheStatus, ViewerContext } from '@expo/entity';
|
||||
import {
|
||||
CacheAdapterFlavor,
|
||||
CacheAdapterFlavorDefinition,
|
||||
CacheStatus,
|
||||
ViewerContext,
|
||||
} from '@expo/entity';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import GenericLocalMemoryCacher from '../GenericLocalMemoryCacher';
|
||||
@@ -6,16 +11,13 @@ import LocalMemoryCacheAdapterProvider from '../LocalMemoryCacheAdapterProvider'
|
||||
import LocalMemoryTestEntity from '../testfixtures/LocalMemoryTestEntity';
|
||||
import {
|
||||
createLocalMemoryTestEntityCompanionProvider,
|
||||
createNoopLocalMemoryIntegrationTestEntityCompanionProvider,
|
||||
createNoOpLocalMemoryIntegrationTestEntityCompanionProvider,
|
||||
} from '../testfixtures/createLocalMemoryTestEntityCompanionProvider';
|
||||
|
||||
describe(GenericLocalMemoryCacher, () => {
|
||||
beforeEach(async () => {
|
||||
LocalMemoryCacheAdapterProvider['localMemoryCacheAdapterMap'].clear();
|
||||
});
|
||||
|
||||
it('has correct caching behavior', async () => {
|
||||
const viewerContext = new ViewerContext(createLocalMemoryTestEntityCompanionProvider());
|
||||
const entityCompanionProvider = createLocalMemoryTestEntityCompanionProvider();
|
||||
const viewerContext = new ViewerContext(entityCompanionProvider);
|
||||
const genericCacher =
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)[
|
||||
'tableDataCoordinator'
|
||||
@@ -33,7 +35,13 @@ describe(GenericLocalMemoryCacher, () => {
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
const entitySpecificGenericCacher = LocalMemoryCacheAdapterProvider[
|
||||
const localMemoryCacheAdapterProvider = (
|
||||
entityCompanionProvider['cacheAdapterFlavors'] as ReadonlyMap<
|
||||
CacheAdapterFlavor,
|
||||
CacheAdapterFlavorDefinition
|
||||
>
|
||||
).get('local-memory')!.cacheAdapterProvider as LocalMemoryCacheAdapterProvider;
|
||||
const entitySpecificGenericCacher = localMemoryCacheAdapterProvider[
|
||||
'localMemoryCacheAdapterMap'
|
||||
].get(
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)
|
||||
@@ -82,50 +90,9 @@ describe(GenericLocalMemoryCacher, () => {
|
||||
expect(cachedValueMiss).toMatchObject({ status: CacheStatus.MISS });
|
||||
});
|
||||
|
||||
it('shares the cache between different requests', async () => {
|
||||
const genericLocalMemoryCacherLoadManySpy = jest.spyOn(
|
||||
GenericLocalMemoryCacher.prototype as unknown as any,
|
||||
'loadManyAsync'
|
||||
);
|
||||
const viewerContext = new ViewerContext(createLocalMemoryTestEntityCompanionProvider());
|
||||
|
||||
const date = new Date();
|
||||
const entity1Created = await LocalMemoryTestEntity.creator(viewerContext)
|
||||
.setField('name', 'blah')
|
||||
.setField('dateField', date)
|
||||
.enforceCreateAsync();
|
||||
|
||||
// loading an entity should put it in cache
|
||||
await LocalMemoryTestEntity.loader(viewerContext)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
// load entity with a different request
|
||||
const viewerContext2 = new ViewerContext(createLocalMemoryTestEntityCompanionProvider());
|
||||
const entity1WithVc2 = await LocalMemoryTestEntity.loader(viewerContext2)
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
const genericCacher =
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)[
|
||||
'tableDataCoordinator'
|
||||
]['cacheAdapter']['genericCacher'];
|
||||
const cacheKeyMaker = genericCacher['makeCacheKey'].bind(genericCacher);
|
||||
expect(entity1WithVc2.getAllFields()).toMatchObject({
|
||||
id: entity1WithVc2.getID(),
|
||||
name: 'blah',
|
||||
dateField: date,
|
||||
});
|
||||
expect(genericLocalMemoryCacherLoadManySpy).toBeCalledWith([
|
||||
cacheKeyMaker('id', entity1WithVc2.getID()),
|
||||
]);
|
||||
expect(genericLocalMemoryCacherLoadManySpy).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('respects the parameters of a noop cache', async () => {
|
||||
const viewerContext = new ViewerContext(
|
||||
createNoopLocalMemoryIntegrationTestEntityCompanionProvider()
|
||||
);
|
||||
const entityCompanionProvider = createNoOpLocalMemoryIntegrationTestEntityCompanionProvider();
|
||||
const viewerContext = new ViewerContext(entityCompanionProvider);
|
||||
const genericCacher =
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)[
|
||||
'tableDataCoordinator'
|
||||
@@ -143,7 +110,13 @@ describe(GenericLocalMemoryCacher, () => {
|
||||
.enforcing()
|
||||
.loadByIDAsync(entity1Created.getID());
|
||||
|
||||
const entitySpecificGenericCacher = LocalMemoryCacheAdapterProvider[
|
||||
const localMemoryCacheAdapterProvider = (
|
||||
entityCompanionProvider['cacheAdapterFlavors'] as ReadonlyMap<
|
||||
CacheAdapterFlavor,
|
||||
CacheAdapterFlavorDefinition
|
||||
>
|
||||
).get('local-memory')!.cacheAdapterProvider as LocalMemoryCacheAdapterProvider;
|
||||
const entitySpecificGenericCacher = localMemoryCacheAdapterProvider[
|
||||
'localMemoryCacheAdapterMap'
|
||||
].get(
|
||||
viewerContext.entityCompanionProvider.getCompanionForEntity(LocalMemoryTestEntity)
|
||||
|
||||
+3
-3
@@ -14,8 +14,8 @@ export const createLocalMemoryTestEntityCompanionProvider = (
|
||||
): EntityCompanionProvider => {
|
||||
const localMemoryCacheAdapterProvider =
|
||||
localMemoryOptions.maxSize === 0 && localMemoryOptions.ttlSeconds === 0
|
||||
? LocalMemoryCacheAdapterProvider.getNoOpProvider()
|
||||
: LocalMemoryCacheAdapterProvider.getProvider(localMemoryOptions);
|
||||
? LocalMemoryCacheAdapterProvider.createNoOpProvider()
|
||||
: LocalMemoryCacheAdapterProvider.createProviderWithOptions(localMemoryOptions);
|
||||
return new EntityCompanionProvider(
|
||||
metricsAdapter,
|
||||
new Map([
|
||||
@@ -38,7 +38,7 @@ export const createLocalMemoryTestEntityCompanionProvider = (
|
||||
);
|
||||
};
|
||||
|
||||
export const createNoopLocalMemoryIntegrationTestEntityCompanionProvider = (
|
||||
export const createNoOpLocalMemoryIntegrationTestEntityCompanionProvider = (
|
||||
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter()
|
||||
): EntityCompanionProvider => {
|
||||
return createLocalMemoryTestEntityCompanionProvider(
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-cache-adapter-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-cache-adapter-redis",
|
||||
"version": "0.32.0",
|
||||
"version": "0.35.0",
|
||||
"description": "Redis cache adapter for @expo/entity",
|
||||
"files": [
|
||||
"build",
|
||||
@@ -31,7 +31,7 @@
|
||||
"ioredis": ">=5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/entity": "^0.32.0",
|
||||
"@expo/entity": "^0.35.0",
|
||||
"ioredis": "^5.2.5"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,36 @@
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-database-adapter-knex
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-database-adapter-knex",
|
||||
"version": "0.32.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.32.0",
|
||||
"@expo/entity": "^0.35.0",
|
||||
"pg": "8.10.0"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,36 @@
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-example
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/entity-example",
|
||||
"private": true,
|
||||
"version": "0.32.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.32.0",
|
||||
"@expo/entity": "^0.35.0",
|
||||
"@koa/cors": "^4.0.0",
|
||||
"@koa/router": "^12.0.0",
|
||||
"graphql": "^16.6.0",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-full-integration-tests
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/entity-full-integration-tests",
|
||||
"private": true,
|
||||
"version": "0.32.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.32.0",
|
||||
"@expo/entity-cache-adapter-redis": "^0.32.0",
|
||||
"@expo/entity-database-adapter-knex": "^0.32.0"
|
||||
"@expo/entity": "^0.35.0",
|
||||
"@expo/entity-cache-adapter-redis": "^0.35.0",
|
||||
"@expo/entity-database-adapter-knex": "^0.35.0"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,30 @@
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-ip-address-field
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-ip-address-field
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-ip-address-field",
|
||||
"version": "0.32.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.32.0",
|
||||
"@expo/entity": "^0.35.0",
|
||||
"@types/jsbn": "^1.2.29"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-secondary-cache-local-memory
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-secondary-cache-local-memory",
|
||||
"version": "0.32.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.32.0",
|
||||
"@expo/entity-cache-adapter-local-memory": "^0.32.0",
|
||||
"@expo/entity": "^0.35.0",
|
||||
"@expo/entity-cache-adapter-local-memory": "^0.35.0",
|
||||
"nullthrows": "^1.1.1"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -122,7 +122,7 @@ export const createTestEntityCompanionProvider = (
|
||||
[
|
||||
'local-memory',
|
||||
{
|
||||
cacheAdapterProvider: LocalMemoryCacheAdapterProvider.getProvider(),
|
||||
cacheAdapterProvider: LocalMemoryCacheAdapterProvider.createProviderWithOptions(),
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
**Note:** Version bump only for package @expo/entity-secondary-cache-redis
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity-secondary-cache-redis",
|
||||
"version": "0.32.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.32.0",
|
||||
"@expo/entity-cache-adapter-redis": "^0.32.0",
|
||||
"@expo/entity": "^0.35.0",
|
||||
"@expo/entity-cache-adapter-redis": "^0.35.0",
|
||||
"ioredis": "^5.2.5",
|
||||
"nullthrows": "^1.1.1"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,40 @@
|
||||
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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Allow UUIDv6/7/8 in UUIDField validation ([#221](https://github.com/expo/entity/issues/221)) ([cc6f3dc](https://github.com/expo/entity/commit/cc6f3dc1e677382eec220723c37b5b883d980372))
|
||||
* require explicit query context specification ([#219](https://github.com/expo/entity/issues/219)) ([8b0b31f](https://github.com/expo/entity/commit/8b0b31fdde5bd565aa527719003ef283a45f55cc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.33.0](https://github.com/expo/entity/compare/v0.32.0...v0.33.0) (2023-10-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add loadManyByIDsNullableAsync loader method ([#214](https://github.com/expo/entity/issues/214)) ([aad94ac](https://github.com/expo/entity/commit/aad94aca479ac07cc7f76b5e1a122ff85a2d1542))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.32.0](https://github.com/expo/entity/compare/v0.31.1...v0.32.0) (2023-06-10)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/entity",
|
||||
"version": "0.32.0",
|
||||
"version": "0.35.0",
|
||||
"description": "A privacy-first data model",
|
||||
"files": [
|
||||
"build",
|
||||
|
||||
@@ -114,6 +114,15 @@ export default class EnforcingEntityLoader<
|
||||
return mapMap(entityResults, (result) => result.enforceValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
|
||||
*/
|
||||
async loadManyByIDsNullableAsync(ids: readonly TID[]): Promise<ReadonlyMap<TID, TEntity | null>> {
|
||||
const entityResults = await this.entityLoader.loadManyByIDsNullableAsync(ids);
|
||||
return mapMap(entityResults, (result) => result?.enforceValue() ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforcing version of entity loader method by the same name.
|
||||
* @throws EntityNotAuthorizedError when viewer is not authorized to view one or more of the returned entities
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { validate as validateUUID } from 'uuid';
|
||||
|
||||
import { EntityFieldDefinition } from './EntityFieldDefinition';
|
||||
|
||||
// Use our own regex since the `uuid` package doesn't support validating UUIDv6/7/8 yet
|
||||
const UUID_REGEX =
|
||||
/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
|
||||
|
||||
/**
|
||||
* EntityFieldDefinition for a column with a JS string type.
|
||||
*/
|
||||
@@ -17,7 +19,7 @@ export class StringField extends EntityFieldDefinition<string> {
|
||||
*/
|
||||
export class UUIDField extends StringField {
|
||||
protected override validateInputValueInternal(value: string): boolean {
|
||||
return validateUUID(value);
|
||||
return super.validateInputValueInternal(value) && UUID_REGEX.test(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,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);
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,23 @@ export default class EntityLoader<
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads many entities for a list of IDs, returning null for any IDs that are non-existent.
|
||||
* @param ids - IDs of the entities to load
|
||||
* @returns map from ID to nullable corresponding entity result, where result error can be UnauthorizedError or EntityNotFoundError.
|
||||
*/
|
||||
async loadManyByIDsNullableAsync(
|
||||
ids: readonly TID[]
|
||||
): Promise<ReadonlyMap<TID, Result<TEntity> | null>> {
|
||||
const entityResults = (await this.loadManyByFieldEqualingManyAsync(
|
||||
this.entityConfiguration.idField as TSelectedFields,
|
||||
ids
|
||||
)) as ReadonlyMap<TID, readonly Result<TEntity>[]>;
|
||||
return mapMap(entityResults, (entityResultsForId) => {
|
||||
return entityResultsForId[0] ?? null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the first entity matching the selection constructed from the conjunction of specified
|
||||
* operands, or null if no matching entity exists. Entities loaded using this method are not
|
||||
|
||||
@@ -51,9 +51,8 @@ export default interface EntityMutationTriggerConfiguration<
|
||||
afterAll?: EntityMutationTrigger<TFields, TID, TViewerContext, TEntity, TSelectedFields>[];
|
||||
|
||||
/**
|
||||
* Trigger set that runs after committing the transaction unless one is supplied
|
||||
* after any mutation (create, update, delete). If the call to the mutation is wrapped in a transaction,
|
||||
* this too will be within the transaction.
|
||||
* Trigger set that runs after committing the mutation transaction. If the call to the mutation is wrapped in a transaction, these
|
||||
* will be run after the wrapping transaction is completed.
|
||||
*/
|
||||
afterCommit?: EntityNonTransactionalMutationTrigger<
|
||||
TFields,
|
||||
@@ -65,9 +64,8 @@ export default interface EntityMutationTriggerConfiguration<
|
||||
}
|
||||
|
||||
/**
|
||||
* A trigger is a way to specify entity mutation operation side-effects that run within the
|
||||
* same transaction as the mutation itself. The one exception is afterCommit, which will run within
|
||||
* the transaction if a transaction is supplied.
|
||||
* A transactional trigger is a way to specify entity mutation operation side-effects that run within the
|
||||
* same transaction as the mutation itself.
|
||||
*/
|
||||
export abstract class EntityMutationTrigger<
|
||||
TFields extends object,
|
||||
@@ -86,7 +84,7 @@ export abstract class EntityMutationTrigger<
|
||||
|
||||
/**
|
||||
* A non-transactional trigger is like a EntityMutationTrigger but used for afterCommit triggers
|
||||
* since they explicitly occur outside of the transaction.
|
||||
* since they run after the transaction is committed.
|
||||
*/
|
||||
export abstract class EntityNonTransactionalMutationTrigger<
|
||||
TFields extends object,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -7,7 +7,7 @@ import IEntityCacheAdapter from './IEntityCacheAdapter';
|
||||
*/
|
||||
export default interface IEntityCacheAdapterProvider {
|
||||
/**
|
||||
* Vend a cache adapter.
|
||||
* Vend a cache adapter for an entity configuration.
|
||||
*/
|
||||
getCacheAdapter<TFields>(
|
||||
entityConfiguration: EntityConfiguration<TFields>
|
||||
|
||||
@@ -222,6 +222,49 @@ describe(EnforcingEntityLoader, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadManyByIDsNullableAsync', () => {
|
||||
it('throws when result is unsuccessful even when there is a null result', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const rejection = new Error();
|
||||
when(entityLoaderMock.loadManyByIDsNullableAsync(anything())).thenResolve(
|
||||
new Map(
|
||||
Object.entries({
|
||||
hello: result(rejection),
|
||||
world: null,
|
||||
})
|
||||
)
|
||||
);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(enforcingEntityLoader.loadManyByIDsNullableAsync(anything())).rejects.toThrow(
|
||||
rejection
|
||||
);
|
||||
});
|
||||
|
||||
it('returns value when result is successful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
const resolved = {};
|
||||
when(entityLoaderMock.loadManyByIDsNullableAsync(anything())).thenResolve(
|
||||
new Map(
|
||||
Object.entries({
|
||||
hello: result(resolved),
|
||||
world: null,
|
||||
})
|
||||
)
|
||||
);
|
||||
const entityLoader = instance(entityLoaderMock);
|
||||
const enforcingEntityLoader = new EnforcingEntityLoader(entityLoader);
|
||||
await expect(enforcingEntityLoader.loadManyByIDsNullableAsync(anything())).resolves.toEqual(
|
||||
new Map(
|
||||
Object.entries({
|
||||
hello: resolved,
|
||||
world: null,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadFirstByFieldEqualityConjunction', () => {
|
||||
it('throws when result is unsuccessful', async () => {
|
||||
const entityLoaderMock = mock<EntityLoader<any, any, any, any, any, any>>(EntityLoader);
|
||||
|
||||
@@ -12,7 +12,8 @@ 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 { RuleComplexity, RuleEvaluationResult } from '../rules/PrivacyPolicyRuleEnums';
|
||||
import { DenyOrSkipPrivacyPolicyRule } from '../rules/PrivacyPolicyRuleTypes';
|
||||
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
|
||||
|
||||
class TestUserViewerContext extends ViewerContext {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
StringArrayField,
|
||||
JSONObjectField,
|
||||
EnumField,
|
||||
StrictEnumField,
|
||||
} from '../EntityFields';
|
||||
import describeFieldTestCase from '../utils/testing/describeFieldTestCase';
|
||||
|
||||
@@ -55,7 +56,13 @@ describe(EntityFieldDefinition, () => {
|
||||
describeFieldTestCase(new StringField({ columnName: 'wat' }), ['hello', ''], [1, true, {}, [[]]]);
|
||||
describeFieldTestCase(
|
||||
new UUIDField({ columnName: 'wat' }),
|
||||
[uuidv1(), uuidv3('wat', uuidv3.DNS), uuidv4(), uuidv5('wat', uuidv5.DNS)],
|
||||
[
|
||||
uuidv1(),
|
||||
uuidv3('wat', uuidv3.DNS),
|
||||
uuidv4(),
|
||||
uuidv5('wat', uuidv5.DNS),
|
||||
/* UUIDv7 */ '018ebfda-dc80-782d-a891-22a0aa057d52',
|
||||
],
|
||||
[uuidv4().replace('-', ''), '', 'hello']
|
||||
);
|
||||
describeFieldTestCase(new DateField({ columnName: 'wat' }), [new Date()], [Date.now()]);
|
||||
@@ -69,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]
|
||||
);
|
||||
@@ -538,7 +538,11 @@ describe(EntityLoader, () => {
|
||||
await expect(entityLoader.loadByIDAsync(loadByValue)).rejects.toEqual(error);
|
||||
await expect(entityLoader.enforcing().loadByIDAsync(loadByValue)).rejects.toEqual(error);
|
||||
await expect(entityLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
|
||||
await expect(entityLoader.loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
|
||||
await expect(entityLoader.enforcing().loadManyByIDsAsync([loadByValue])).rejects.toEqual(error);
|
||||
await expect(entityLoader.loadManyByIDsNullableAsync([loadByValue])).rejects.toEqual(error);
|
||||
await expect(
|
||||
entityLoader.enforcing().loadManyByIDsNullableAsync([loadByValue])
|
||||
).rejects.toEqual(error);
|
||||
await expect(
|
||||
entityLoader.loadManyByFieldEqualingAsync('customIdField', loadByValue)
|
||||
).rejects.toEqual(error);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
@@ -98,8 +98,9 @@ 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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -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