Compare commits

...
Author SHA1 Message Date
Will Schurman 58797a50b6 Change boolean decider to be async 2024-05-07 09:25:04 -07:00
Will Schurman d09a735264 Address comment 2024-05-07 09:20:21 -07:00
Will Schurman dc3aafc6ab feat: add privacy policy rule reordering 2024-05-06 13:02:27 -07:00
Will Schurman 87532525d0 feat: Add StrictEnumField with better validation (#222) 2024-05-02 11:30:32 -07:00
Will Schurman 33771295c5 v0.35.0 2024-04-11 09:55:15 -07:00
Will Schurman 1bdfbd6b56 fix: Revert: require explicit query context specification (#219)
This reverts commit 8b0b31fdde.
2024-04-11 09:51:40 -07:00
Will Schurman 0fa6f554a2 v0.34.0 2024-04-11 09:13:48 -07:00
Will Schurman 8b0b31fdde feat: require explicit query context specification (#219) 2024-04-11 09:09:18 -07:00
James Ide cc6f3dc1e6 feat: Allow UUIDv6/7/8 in UUIDField validation (#221)
Why
---
Currently the UUIDField type supports only UUIDv1-5 and to use a newer UUID version we need to use StringField.

How
---
Manually implemented UUID validation, adding support for versions 6, 7, and 8. Also added support for the "max" UUID, which is now part of the spec.

Test Plan
---
Added a UUIDv7 value (from https://uuid7.com/) to test cases.
2024-04-08 18:17:24 -07:00
Will Schurman 9c43a56a39 BREAKING CHANGE: hoist the responsibility of sharing local memory cache adapter into application (#218) 2023-12-06 12:38:30 -08:00
Will Schurman ef7b452c9f docs: fix EntityMutationTrigger docblocks (#216) 2023-12-04 16:18:29 -08:00
Will Schurman 659e08386f v0.33.0 2023-10-06 16:00:57 -07:00
Will Schurman aad94aca47 feat: add loadManyByIDsNullableAsync loader method (#214) 2023-10-04 15:28:28 -07:00
49 changed files with 1209 additions and 156 deletions

No files matched your search

+34
View File
@@ -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
View File
@@ -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)
);
});
}
}
@@ -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)
@@ -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"
}
}
+30
View File
@@ -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)
+2 -2
View File
@@ -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"
}
}
@@ -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"
}
+34
View File
@@ -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
View File
@@ -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
+20 -3
View File
@@ -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);
}
}
+17
View File
@@ -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,
+28 -2
View File
@@ -6,7 +6,9 @@ import EntityNotAuthorizedError from './errors/EntityNotAuthorizedError';
import IEntityMetricsAdapter, {
EntityMetricsAuthorizationResult,
} from './metrics/IEntityMetricsAdapter';
import PrivacyPolicyRule, { RuleEvaluationResult } from './rules/PrivacyPolicyRule';
import { RuleEvaluationResult } from './rules/PrivacyPolicyRuleEnums';
import { PrivacyPolicyRule } from './rules/PrivacyPolicyRuleTypes';
import { reorderRulesByRuleComplexityGroups } from './rules/internal/PrivacyPolicyRuleOrdering';
/**
* Information about the reason this privacy policy is being evaluated.
@@ -143,6 +145,15 @@ export default abstract class EntityPrivacyPolicy<
};
}
protected async shouldAutoReorderRulesAccordingToComplexityAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_action: EntityAuthorizationAction
): Promise<boolean> {
return false;
}
/**
* Authorize an entity against creation policy.
* @param viewerContext - viewer context of user creating the entity
@@ -248,7 +259,13 @@ export default abstract class EntityPrivacyPolicy<
}
private async authorizeForRulesetAsync(
ruleset: readonly PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>[],
rulesetOriginalOrder: readonly PrivacyPolicyRule<
TFields,
TID,
TViewerContext,
TEntity,
TSelectedFields
>[],
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
@@ -257,6 +274,15 @@ export default abstract class EntityPrivacyPolicy<
metricsAdapter: IEntityMetricsAdapter
): Promise<TEntity> {
const privacyPolicyEvaluator = this.getPrivacyPolicyEvaluator(viewerContext);
const shouldAutoReorder = await this.shouldAutoReorderRulesAccordingToComplexityAsync(
viewerContext,
queryContext,
evaluationContext,
action
);
const ruleset = shouldAutoReorder
? reorderRulesByRuleComplexityGroups(rulesetOriginalOrder)
: rulesetOriginalOrder;
switch (privacyPolicyEvaluator.mode) {
case EntityPrivacyPolicyEvaluationMode.ENFORCE:
try {
@@ -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',
+2 -2
View File
@@ -66,8 +66,8 @@ export { default as NoOpEntityMetricsAdapter } from './metrics/NoOpEntityMetrics
export { default as AlwaysAllowPrivacyPolicyRule } from './rules/AlwaysAllowPrivacyPolicyRule';
export { default as AlwaysDenyPrivacyPolicyRule } from './rules/AlwaysDenyPrivacyPolicyRule';
export { default as AlwaysSkipPrivacyPolicyRule } from './rules/AlwaysSkipPrivacyPolicyRule';
export { default as PrivacyPolicyRule } from './rules/PrivacyPolicyRule';
export * from './rules/PrivacyPolicyRule';
export * from './rules/PrivacyPolicyRuleEnums';
export * from './rules/PrivacyPolicyRuleTypes';
export * from './utils/testing/PrivacyPolicyRuleTestUtils';
export * from './utils/testing/StubCacheAdapter';
export { default as describeFieldTestCase } from './utils/testing/describeFieldTestCase';
@@ -1,4 +1,5 @@
import PrivacyPolicyRule, { RuleEvaluationResult } from './PrivacyPolicyRule';
import { RuleComplexity, RuleEvaluationResult } from './PrivacyPolicyRuleEnums';
import { AllowOrSkipPrivacyPolicyRule } from './PrivacyPolicyRuleTypes';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
@@ -13,13 +14,15 @@ export default class AlwaysAllowPrivacyPolicyRule<
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
> extends AllowOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
override complexity = RuleComplexity.LOW;
async evaluateAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_entity: TEntity
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.ALLOW | RuleEvaluationResult.SKIP> {
return RuleEvaluationResult.ALLOW;
}
}
@@ -1,4 +1,5 @@
import PrivacyPolicyRule, { RuleEvaluationResult } from './PrivacyPolicyRule';
import { RuleComplexity, RuleEvaluationResult } from './PrivacyPolicyRuleEnums';
import { DenyOrSkipPrivacyPolicyRule } from './PrivacyPolicyRuleTypes';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
@@ -13,13 +14,15 @@ export default class AlwaysDenyPrivacyPolicyRule<
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
> extends DenyOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
override complexity = RuleComplexity.LOW;
async evaluateAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_entity: TEntity
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.DENY | RuleEvaluationResult.SKIP> {
return RuleEvaluationResult.DENY;
}
}
@@ -1,4 +1,5 @@
import PrivacyPolicyRule, { RuleEvaluationResult } from './PrivacyPolicyRule';
import { RuleComplexity, RuleEvaluationResult } from './PrivacyPolicyRuleEnums';
import { SkipPrivacyPolicyRule } from './PrivacyPolicyRuleTypes';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
@@ -13,13 +14,15 @@ export default class AlwaysSkipPrivacyPolicyRule<
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
> extends SkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
override complexity = RuleComplexity.LOW;
async evaluateAsync(
_viewerContext: TViewerContext,
_queryContext: EntityQueryContext,
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
_entity: TEntity
): Promise<RuleEvaluationResult> {
): Promise<RuleEvaluationResult.SKIP> {
return RuleEvaluationResult.SKIP;
}
}
@@ -0,0 +1,61 @@
/**
* Enum return type of privacy policy rule evaluation.
*/
export enum RuleEvaluationResult {
/**
* Deny viewer access to the entity.
*/
DENY = -1,
/**
* Defer entity viewer access to subsequent rule in the privacy policy.
*/
SKIP = 0,
/**
* Allow viewer access to the entity.
*/
ALLOW = 1,
}
/**
* Estimated complexity of a privacy policy rule. Definition of what rules fall into what complexity category
* is application-specific, but the AlwaysAllow/AlwaysDeny/AlwaysSkip rules provided are all defined as LOW
* complexity.
*/
export enum RuleComplexity {
/**
* Rule complexity is low. Definition dependent on application.
*/
LOW = 0,
/**
* Rule complexity is medium. Definition dependent on application.
*/
MEDIUM = 1,
/**
* Rule complexity is high. Definition dependent on application.
*/
HIGH = 2,
}
/**
* Used for disambiguation between classes of privacy policy rules at runtime.
* Multiple rules of the same type in a row can be reordered safely to improve policy evaluation time
* based on complexities.
*/
export enum RuleEvaluationResultType {
/**
* Rule is a DenyOrSkipPrivacyPolicyRule. Can be safely reordered with other sequential DenyOrSkipPrivacyPolicyRule or SkipPrivacyPolicyRule in a policy.
*/
DENY_OR_SKIP,
/**
* Rule is a SkipPrivacyPolicyRule. Can be safely reordered with any other sequential privacy policy rules in a policy.
*/
SKIP,
/**
* Rule is an AllowOrSkipPrivacyPolicyRule. Can be safely reordered with other sequential AllowOrSkipPrivacyPolicyRule or SkipPrivacyPolicyRule in a policy.
*/
ALLOW_OR_SKIP,
}
@@ -0,0 +1,68 @@
import { RuleEvaluationResult, RuleEvaluationResultType } from './PrivacyPolicyRuleEnums';
import { PrivacyPolicyRuleInternal } from './internal/PrivacyPolicyRule';
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
import ViewerContext from '../ViewerContext';
export type PrivacyPolicyRule<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> =
| AllowOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>
| DenyOrSkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>
| SkipPrivacyPolicyRule<TFields, TID, TViewerContext, TEntity, TSelectedFields>;
export abstract class AllowOrSkipPrivacyPolicyRule<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRuleInternal<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
abstract override evaluateAsync(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: TEntity
): Promise<RuleEvaluationResult.ALLOW | RuleEvaluationResult.SKIP>;
override resultType = RuleEvaluationResultType.ALLOW_OR_SKIP;
}
export abstract class DenyOrSkipPrivacyPolicyRule<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRuleInternal<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
abstract override evaluateAsync(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: TEntity
): Promise<RuleEvaluationResult.DENY | RuleEvaluationResult.SKIP>;
override resultType = RuleEvaluationResultType.DENY_OR_SKIP;
}
export abstract class SkipPrivacyPolicyRule<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> extends PrivacyPolicyRuleInternal<TFields, TID, TViewerContext, TEntity, TSelectedFields> {
abstract override evaluateAsync(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: TEntity
): Promise<RuleEvaluationResult.SKIP>;
override resultType = RuleEvaluationResultType.SKIP;
}
@@ -1,24 +1,12 @@
import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
import { EntityQueryContext } from '../EntityQueryContext';
import ReadonlyEntity from '../ReadonlyEntity';
import ViewerContext from '../ViewerContext';
export enum RuleEvaluationResult {
/**
* Deny viewer access to the entity.
*/
DENY = -1,
/**
* Defer entity viewer access to subsequent rule in the privacy policy.
*/
SKIP = 0,
/**
* Allow viewer access to the entity.
*/
ALLOW = 1,
}
import { EntityPrivacyPolicyEvaluationContext } from '../../EntityPrivacyPolicy';
import { EntityQueryContext } from '../../EntityQueryContext';
import ReadonlyEntity from '../../ReadonlyEntity';
import ViewerContext from '../../ViewerContext';
import {
RuleComplexity,
RuleEvaluationResult,
RuleEvaluationResultType,
} from '../PrivacyPolicyRuleEnums';
/**
* A single unit of which declarative privacy policies are composed, allowing for simple
@@ -36,17 +24,25 @@ export enum RuleEvaluationResult {
* - Blocking. For example, a user blocks another user from seeing their posts, and the rule
* would be named something like `DenyIfViewerHasBeenBlockedPrivacyPolicyRule`.
*/
export default abstract class PrivacyPolicyRule<
export abstract class PrivacyPolicyRuleInternal<
TFields extends object,
TID extends NonNullable<TFields[TSelectedFields]>,
TViewerContext extends ViewerContext,
TEntity extends ReadonlyEntity<TFields, TID, TViewerContext, TSelectedFields>,
TSelectedFields extends keyof TFields = keyof TFields
> {
/**
* Estimated complexity of a privacy policy rule. Used for automatic (safe) rule reordering.
* Typed as a number so that applications can define their own complexity scale.
*/
abstract readonly complexity: RuleComplexity;
abstract evaluateAsync(
viewerContext: TViewerContext,
queryContext: EntityQueryContext,
evaluationContext: EntityPrivacyPolicyEvaluationContext,
entity: TEntity
): Promise<RuleEvaluationResult>;
abstract readonly resultType: RuleEvaluationResultType;
}
@@ -0,0 +1,70 @@
import invariant from 'invariant';
import { RuleEvaluationResultType } from '../PrivacyPolicyRuleEnums';
/**
* Privacy policy list of PrivacyPolicyRule can be reordered safely according to
* the following constraints on RuleEvaluationResultType:
* 1. Two adjacent ALLOW_OR_SKIP rules can be swapped.
* 2. Two adjacent DENY_OR_SKIP rules can be swapped.
* 3. SKIP rules can be swapped with any other adjacent rule.
*
* @param rulesetOriginalOrder - ruleset with original ordering defined in code
* @returns reordered ruleset depending on constraints above
*/
export function reorderRulesByRuleComplexityGroups<
TRule extends { resultType: RuleEvaluationResultType; complexity: number }
>(rulesetOriginalOrder: readonly TRule[]): readonly TRule[] {
// first, group the rules according to constraints
const groups = groupRulesByResultType(rulesetOriginalOrder);
// sort the groups by declared complexity
const internallySortedGroups = groups.map((group) => sortGroupByComplexity(group));
// flatten the groups
return internallySortedGroups.flat();
}
export function sortGroupByComplexity<TRule extends { complexity: number }>(
ruleGroup: readonly TRule[]
): readonly TRule[] {
return [...ruleGroup].sort((a, b) => a.complexity - b.complexity);
}
export function groupRulesByResultType<TRule extends { resultType: RuleEvaluationResultType }>(
ruleset: readonly TRule[]
): readonly (readonly TRule[])[] {
const groups: TRule[][] = [[]];
let currentGroupIdx = 0;
let currentGroupType:
| RuleEvaluationResultType.ALLOW_OR_SKIP
| RuleEvaluationResultType.DENY_OR_SKIP
| null = null;
for (const rule of ruleset) {
// if group type of this rule is not compatible with the current group type, start a new group
if (
currentGroupType !== null &&
rule.resultType !== currentGroupType &&
rule.resultType !== RuleEvaluationResultType.SKIP
) {
currentGroupIdx += 1;
groups.push([]);
currentGroupType = null;
}
// add the current rule to the current group
const currentGroup = groups[currentGroupIdx];
invariant(currentGroup, `invalid groups index ${currentGroupIdx}`);
currentGroup.push(rule);
// if there isn't a known group type yet for the current group, set it to the group type of the rule.
// note that if there isn't a known group type yet and the current rule is SKIP, we'll
// continue to the next rule to determine group type
if (currentGroupType === null && rule.resultType !== RuleEvaluationResultType.SKIP) {
currentGroupType = rule.resultType;
}
}
return groups;
}
@@ -0,0 +1,152 @@
import { RuleComplexity, RuleEvaluationResultType } from '../../PrivacyPolicyRuleEnums';
import {
groupRulesByResultType,
reorderRulesByRuleComplexityGroups,
sortGroupByComplexity,
} from '../PrivacyPolicyRuleOrdering';
type TestRuleJustResultType = { id: number; resultType: RuleEvaluationResultType };
describe(groupRulesByResultType, () => {
const rule1 = { id: 1, resultType: RuleEvaluationResultType.ALLOW_OR_SKIP };
const rule2 = { id: 2, resultType: RuleEvaluationResultType.ALLOW_OR_SKIP };
const rule3 = { id: 3, resultType: RuleEvaluationResultType.SKIP };
const rule4 = { id: 4, resultType: RuleEvaluationResultType.SKIP };
const rule5 = { id: 5, resultType: RuleEvaluationResultType.DENY_OR_SKIP };
const rule6 = { id: 6, resultType: RuleEvaluationResultType.DENY_OR_SKIP };
test.each([
// two adjacent of the same type should be grouped
{ ruleset: [rule1, rule2], expectedResultGroups: [[rule1, rule2]] },
{ ruleset: [rule3, rule4], expectedResultGroups: [[rule3, rule4]] },
{ ruleset: [rule5, rule6], expectedResultGroups: [[rule5, rule6]] },
// two adjacent of ALLOW_OR_SKIP or DENY_OR_SKIP should not be grouped
{ ruleset: [rule1, rule5], expectedResultGroups: [[rule1], [rule5]] },
{ ruleset: [rule5, rule1], expectedResultGroups: [[rule5], [rule1]] },
// adjacent (ALLOW_OR_SKIP and SKIP) or (DENY_OR_SKIP and SKIP) should be grouped
{ ruleset: [rule1, rule2, rule3], expectedResultGroups: [[rule1, rule2, rule3]] },
{ ruleset: [rule1, rule2, rule3, rule4], expectedResultGroups: [[rule1, rule2, rule3, rule4]] },
{ ruleset: [rule5, rule6, rule3], expectedResultGroups: [[rule5, rule6, rule3]] },
{ ruleset: [rule5, rule6, rule3, rule4], expectedResultGroups: [[rule5, rule6, rule3, rule4]] },
// skip cases group with first group
{
ruleset: [rule1, rule2, rule3, rule4, rule5, rule6],
expectedResultGroups: [
[rule1, rule2, rule3, rule4],
[rule5, rule6],
],
},
// skip in first N slots should take the first group type
{
ruleset: [rule3, rule4, rule1, rule2, rule5, rule6],
expectedResultGroups: [
[rule3, rule4, rule1, rule2],
[rule5, rule6],
],
},
// lots of groups
{
ruleset: [rule1, rule5, rule3, rule2, rule6, rule4],
expectedResultGroups: [[rule1], [rule5, rule3], [rule2], [rule6, rule4]],
},
// skip in first N slots
{
ruleset: [rule3, rule4, rule5, rule1],
expectedResultGroups: [[rule3, rule4, rule5], [rule1]],
},
])(
'case %#',
({
ruleset,
expectedResultGroups,
}: {
ruleset: TestRuleJustResultType[];
expectedResultGroups: TestRuleJustResultType[][];
}) => {
const resultGroups = groupRulesByResultType(ruleset);
expect(resultGroups).toStrictEqual(expectedResultGroups);
}
);
});
type TestRuleJustComplexity = { id: number; complexity: number };
describe(sortGroupByComplexity, () => {
const rule1 = { id: 1, complexity: RuleComplexity.LOW };
const rule2 = { id: 2, complexity: RuleComplexity.LOW };
const rule3 = { id: 3, complexity: RuleComplexity.MEDIUM };
const rule5 = { id: 3, complexity: RuleComplexity.HIGH };
test.each([
{ ruleGroup: [rule1, rule2], expectedRuleGroup: [rule1, rule2] },
{ ruleGroup: [rule1, rule3], expectedRuleGroup: [rule1, rule3] },
{ ruleGroup: [rule1, rule3, rule5], expectedRuleGroup: [rule1, rule3, rule5] },
{ ruleGroup: [rule5, rule3, rule1], expectedRuleGroup: [rule1, rule3, rule5] },
])(
'case %#',
({
ruleGroup,
expectedRuleGroup,
}: {
ruleGroup: TestRuleJustComplexity[];
expectedRuleGroup: TestRuleJustComplexity[];
}) => {
const resultGroup = sortGroupByComplexity(ruleGroup);
expect(resultGroup).toStrictEqual(expectedRuleGroup);
}
);
});
type TestRule = { id: number; resultType: RuleEvaluationResultType; complexity: number };
describe(reorderRulesByRuleComplexityGroups, () => {
const rule1 = {
id: 1,
resultType: RuleEvaluationResultType.ALLOW_OR_SKIP,
complexity: RuleComplexity.HIGH,
};
const rule2 = {
id: 2,
resultType: RuleEvaluationResultType.ALLOW_OR_SKIP,
complexity: RuleComplexity.LOW,
};
const rule3 = {
id: 3,
resultType: RuleEvaluationResultType.SKIP,
complexity: RuleComplexity.LOW,
};
const rule4 = {
id: 4,
resultType: RuleEvaluationResultType.SKIP,
complexity: RuleComplexity.HIGH,
};
const rule5 = {
id: 5,
resultType: RuleEvaluationResultType.DENY_OR_SKIP,
complexity: RuleComplexity.MEDIUM,
};
const rule6 = {
id: 6,
resultType: RuleEvaluationResultType.DENY_OR_SKIP,
complexity: RuleComplexity.LOW,
};
test.each([
{
ruleset: [rule1, rule2, rule3, rule4, rule5, rule6],
expectedRuleset: [rule2, rule3, rule1, rule4, rule6, rule5],
},
])(
'case %#',
({ ruleset, expectedRuleset }: { ruleset: TestRule[]; expectedRuleset: TestRule[] }) => {
const resultRuleset = reorderRulesByRuleComplexityGroups(ruleset);
expect(resultRuleset).toStrictEqual(expectedRuleset);
}
);
});
@@ -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,