@expo/entity 0.22.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/ComposedEntityCacheAdapter.d.ts +19 -0
- package/build/ComposedEntityCacheAdapter.js +66 -0
- package/build/ComposedEntityCacheAdapter.js.map +1 -0
- package/build/ComposedSecondaryEntityCache.d.ts +15 -0
- package/build/ComposedSecondaryEntityCache.js +37 -0
- package/build/ComposedSecondaryEntityCache.js.map +1 -0
- package/build/Entity.js +6 -6
- package/build/Entity.js.map +1 -1
- package/build/EntityAssociationLoader.js +4 -4
- package/build/EntityAssociationLoader.js.map +1 -1
- package/build/EntityCacheAdapter.d.ts +2 -9
- package/build/EntityCacheAdapter.js.map +1 -1
- package/build/EntityFieldDefinition.d.ts +8 -0
- package/build/EntityFieldDefinition.js +5 -0
- package/build/EntityFieldDefinition.js.map +1 -1
- package/build/EntityFields.d.ts +38 -0
- package/build/EntityFields.js +38 -0
- package/build/EntityFields.js.map +1 -1
- package/build/EntityLoader.d.ts +3 -2
- package/build/EntityLoader.js +5 -4
- package/build/EntityLoader.js.map +1 -1
- package/build/EntityLoaderFactory.d.ts +2 -2
- package/build/EntityLoaderFactory.js +2 -2
- package/build/EntityLoaderFactory.js.map +1 -1
- package/build/EntityMutationInfo.d.ts +12 -3
- package/build/EntityMutator.d.ts +5 -4
- package/build/EntityMutator.js +31 -24
- package/build/EntityMutator.js.map +1 -1
- package/build/EntityMutatorFactory.d.ts +4 -4
- package/build/EntityMutatorFactory.js +6 -6
- package/build/EntityMutatorFactory.js.map +1 -1
- package/build/EntityPrivacyPolicy.d.ts +15 -4
- package/build/EntityPrivacyPolicy.js +14 -14
- package/build/EntityPrivacyPolicy.js.map +1 -1
- package/build/GenericSecondaryEntityCache.d.ts +19 -0
- package/build/GenericSecondaryEntityCache.js +74 -0
- package/build/GenericSecondaryEntityCache.js.map +1 -0
- package/build/IEntityGenericCacher.d.ts +11 -0
- package/build/IEntityGenericCacher.js +3 -0
- package/build/IEntityGenericCacher.js.map +1 -0
- package/build/ReadonlyEntity.js +1 -1
- package/build/ReadonlyEntity.js.map +1 -1
- package/build/ViewerScopedEntityLoaderFactory.d.ts +2 -2
- package/build/ViewerScopedEntityLoaderFactory.js +2 -2
- package/build/ViewerScopedEntityLoaderFactory.js.map +1 -1
- package/build/ViewerScopedEntityMutatorFactory.d.ts +4 -4
- package/build/ViewerScopedEntityMutatorFactory.js +6 -6
- package/build/ViewerScopedEntityMutatorFactory.js.map +1 -1
- package/build/__tests__/ComposedCacheAdapter-test.d.ts +1 -0
- package/build/__tests__/ComposedCacheAdapter-test.js +198 -0
- package/build/__tests__/ComposedCacheAdapter-test.js.map +1 -0
- package/build/__tests__/ComposedSecondaryEntityCache-test.d.ts +1 -0
- package/build/__tests__/ComposedSecondaryEntityCache-test.js +65 -0
- package/build/__tests__/ComposedSecondaryEntityCache-test.js.map +1 -0
- package/build/__tests__/EntityCommonUseCases-test.js +1 -1
- package/build/__tests__/EntityCommonUseCases-test.js.map +1 -1
- package/build/__tests__/EntityEdges-test.js +260 -37
- package/build/__tests__/EntityEdges-test.js.map +1 -1
- package/build/__tests__/EntityLoader-constructor-test.js +2 -1
- package/build/__tests__/EntityLoader-constructor-test.js.map +1 -1
- package/build/__tests__/EntityLoader-test.js +19 -11
- package/build/__tests__/EntityLoader-test.js.map +1 -1
- package/build/__tests__/EntityMutator-test.js +96 -43
- package/build/__tests__/EntityMutator-test.js.map +1 -1
- package/build/__tests__/EntityPrivacyPolicy-test.js +23 -12
- package/build/__tests__/EntityPrivacyPolicy-test.js.map +1 -1
- package/build/__tests__/EntitySecondaryCacheLoader-test.js +1 -1
- package/build/__tests__/EntitySecondaryCacheLoader-test.js.map +1 -1
- package/build/__tests__/ViewerScopedEntityLoaderFactory-test.js +3 -2
- package/build/__tests__/ViewerScopedEntityLoaderFactory-test.js.map +1 -1
- package/build/__tests__/ViewerScopedEntityMutatorFactory-test.js +3 -2
- package/build/__tests__/ViewerScopedEntityMutatorFactory-test.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.js +3 -1
- package/build/index.js.map +1 -1
- package/build/internal/ReadThroughEntityCache.d.ts +2 -3
- package/build/internal/ReadThroughEntityCache.js +2 -6
- package/build/internal/ReadThroughEntityCache.js.map +1 -1
- package/build/internal/__tests__/ReadThroughEntityCache-test.js +0 -32
- package/build/internal/__tests__/ReadThroughEntityCache-test.js.map +1 -1
- package/build/rules/AlwaysAllowPrivacyPolicyRule.d.ts +2 -1
- package/build/rules/AlwaysAllowPrivacyPolicyRule.js +1 -1
- package/build/rules/AlwaysAllowPrivacyPolicyRule.js.map +1 -1
- package/build/rules/AlwaysDenyPrivacyPolicyRule.d.ts +2 -1
- package/build/rules/AlwaysDenyPrivacyPolicyRule.js +1 -1
- package/build/rules/AlwaysDenyPrivacyPolicyRule.js.map +1 -1
- package/build/rules/AlwaysSkipPrivacyPolicyRule.d.ts +2 -1
- package/build/rules/AlwaysSkipPrivacyPolicyRule.js +1 -1
- package/build/rules/AlwaysSkipPrivacyPolicyRule.js.map +1 -1
- package/build/rules/PrivacyPolicyRule.d.ts +2 -1
- package/build/rules/PrivacyPolicyRule.js.map +1 -1
- package/build/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.js +1 -0
- package/build/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.js.map +1 -1
- package/build/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.js +1 -0
- package/build/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.js.map +1 -1
- package/build/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.js +1 -0
- package/build/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.js.map +1 -1
- package/build/utils/testing/PrivacyPolicyRuleTestUtils.d.ts +2 -0
- package/build/utils/testing/PrivacyPolicyRuleTestUtils.js +6 -6
- package/build/utils/testing/PrivacyPolicyRuleTestUtils.js.map +1 -1
- package/build/utils/testing/StubCacheAdapter.d.ts +6 -9
- package/build/utils/testing/StubCacheAdapter.js +0 -6
- package/build/utils/testing/StubCacheAdapter.js.map +1 -1
- package/package.json +1 -1
- package/src/ComposedEntityCacheAdapter.ts +86 -0
- package/src/ComposedSecondaryEntityCache.ts +63 -0
- package/src/Entity.ts +6 -4
- package/src/EntityAssociationLoader.ts +4 -4
- package/src/EntityCacheAdapter.ts +2 -10
- package/src/EntityFieldDefinition.ts +8 -0
- package/src/EntityFields.ts +45 -0
- package/src/EntityLoader.ts +5 -1
- package/src/EntityLoaderFactory.ts +4 -2
- package/src/EntityMutationInfo.ts +13 -3
- package/src/EntityMutator.ts +44 -21
- package/src/EntityMutatorFactory.ts +10 -4
- package/src/EntityPrivacyPolicy.ts +31 -1
- package/src/GenericSecondaryEntityCache.ts +98 -0
- package/src/IEntityGenericCacher.ts +15 -0
- package/src/ReadonlyEntity.ts +1 -1
- package/src/ViewerScopedEntityLoaderFactory.ts +8 -3
- package/src/ViewerScopedEntityMutatorFactory.ts +22 -7
- package/src/__tests__/ComposedCacheAdapter-test.ts +280 -0
- package/src/__tests__/ComposedSecondaryEntityCache-test.ts +101 -0
- package/src/__tests__/EntityCommonUseCases-test.ts +2 -1
- package/src/__tests__/EntityEdges-test.ts +286 -45
- package/src/__tests__/EntityLoader-constructor-test.ts +3 -1
- package/src/__tests__/EntityLoader-test.ts +26 -1
- package/src/__tests__/EntityMutator-test.ts +99 -37
- package/src/__tests__/EntityPrivacyPolicy-test.ts +66 -7
- package/src/__tests__/EntitySecondaryCacheLoader-test.ts +1 -0
- package/src/__tests__/ViewerScopedEntityLoaderFactory-test.ts +4 -2
- package/src/__tests__/ViewerScopedEntityMutatorFactory-test.ts +6 -2
- package/src/index.ts +2 -0
- package/src/internal/ReadThroughEntityCache.ts +6 -28
- package/src/internal/__tests__/ReadThroughEntityCache-test.ts +0 -44
- package/src/rules/AlwaysAllowPrivacyPolicyRule.ts +2 -0
- package/src/rules/AlwaysDenyPrivacyPolicyRule.ts +2 -0
- package/src/rules/AlwaysSkipPrivacyPolicyRule.ts +2 -0
- package/src/rules/PrivacyPolicyRule.ts +2 -0
- package/src/rules/__tests__/AlwaysAllowPrivacyPolicyRule-test.ts +2 -0
- package/src/rules/__tests__/AlwaysDenyPrivacyPolicyRule-test.ts +2 -0
- package/src/rules/__tests__/AlwaysSkipPrivacyPolicyRule-test.ts +2 -0
- package/src/utils/testing/PrivacyPolicyRuleTestUtils.ts +14 -6
- package/src/utils/testing/StubCacheAdapter.ts +11 -17
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import invariant from 'invariant';
|
|
2
|
+
import nullthrows from 'nullthrows';
|
|
3
|
+
|
|
4
|
+
import ComposedSecondaryEntityCache from '../ComposedSecondaryEntityCache';
|
|
5
|
+
import { ISecondaryEntityCache } from '../EntitySecondaryCacheLoader';
|
|
6
|
+
|
|
7
|
+
type TestFields = { id: string };
|
|
8
|
+
type TestLoadParams = { lp: string };
|
|
9
|
+
|
|
10
|
+
class TestEntitySecondaryCache implements ISecondaryEntityCache<TestFields, TestLoadParams> {
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly prefilledResults: Map<Readonly<TestLoadParams>, Readonly<TestFields>>
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
async loadManyThroughAsync(
|
|
16
|
+
loadParamsArray: readonly Readonly<TestLoadParams>[],
|
|
17
|
+
fetcher: (
|
|
18
|
+
fetcherLoadParamsArray: readonly Readonly<TestLoadParams>[]
|
|
19
|
+
) => Promise<ReadonlyMap<Readonly<TestLoadParams>, Readonly<TestFields> | null>>
|
|
20
|
+
): Promise<ReadonlyMap<Readonly<TestLoadParams>, Readonly<TestFields> | null>> {
|
|
21
|
+
// this does an unusual method of calling fetcher, but there's no constraint that says fetcher can only be called once
|
|
22
|
+
// so this tests that
|
|
23
|
+
|
|
24
|
+
const retMap = new Map<Readonly<TestLoadParams>, Readonly<TestFields> | null>();
|
|
25
|
+
for (const loadParams of loadParamsArray) {
|
|
26
|
+
if (this.prefilledResults.has(loadParams)) {
|
|
27
|
+
retMap.set(loadParams, nullthrows(this.prefilledResults.get(loadParams)));
|
|
28
|
+
} else {
|
|
29
|
+
const fetcherResult = await fetcher([loadParams]);
|
|
30
|
+
const toSet = fetcherResult.get(loadParams);
|
|
31
|
+
invariant(toSet !== undefined, 'should be set');
|
|
32
|
+
retMap.set(loadParams, toSet);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return retMap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async invalidateManyAsync(loadParamsArray: readonly Readonly<TestLoadParams>[]): Promise<void> {
|
|
39
|
+
for (const loadParams of loadParamsArray) {
|
|
40
|
+
this.prefilledResults.delete(loadParams);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe(ComposedSecondaryEntityCache, () => {
|
|
46
|
+
it('composes correctly', async () => {
|
|
47
|
+
// TODO(wschurman): investigate whether we can use immutable or something to do better object equality for the map keys
|
|
48
|
+
const lp1 = { lp: '1' };
|
|
49
|
+
const lp2 = { lp: '2' };
|
|
50
|
+
const lp3 = { lp: '3' };
|
|
51
|
+
|
|
52
|
+
const primarySecondaryEntityCache = new TestEntitySecondaryCache(
|
|
53
|
+
new Map([[lp1, { id: 'primary-1' }]])
|
|
54
|
+
);
|
|
55
|
+
const fallbackSecondaryEntityCache = new TestEntitySecondaryCache(
|
|
56
|
+
new Map([[lp2, { id: 'fallback-2' }]])
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const composedSecondaryEntityCache = new ComposedSecondaryEntityCache([
|
|
60
|
+
primarySecondaryEntityCache,
|
|
61
|
+
fallbackSecondaryEntityCache,
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const results = await composedSecondaryEntityCache.loadManyThroughAsync(
|
|
65
|
+
[lp1, lp2, lp3],
|
|
66
|
+
async (fetcherLoadParamsArray) =>
|
|
67
|
+
new Map(fetcherLoadParamsArray.map((flp) => [flp, { id: `db-fetched-${flp.lp}` }]))
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(results.get(lp1)).toEqual({ id: 'primary-1' });
|
|
71
|
+
expect(results.get(lp2)).toEqual({ id: 'fallback-2' });
|
|
72
|
+
expect(results.get(lp3)).toEqual({ id: 'db-fetched-3' });
|
|
73
|
+
|
|
74
|
+
await composedSecondaryEntityCache.invalidateManyAsync([lp1, lp2, lp3]);
|
|
75
|
+
|
|
76
|
+
const resultsAfterInvalidate = await composedSecondaryEntityCache.loadManyThroughAsync(
|
|
77
|
+
[lp1, lp2, lp3],
|
|
78
|
+
async (fetcherLoadParamsArray) =>
|
|
79
|
+
new Map(fetcherLoadParamsArray.map((flp) => [flp, { id: `db-fetched-${flp.lp}` }]))
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(resultsAfterInvalidate.get(lp1)).toEqual({ id: 'db-fetched-1' });
|
|
83
|
+
expect(resultsAfterInvalidate.get(lp2)).toEqual({ id: 'db-fetched-2' });
|
|
84
|
+
expect(resultsAfterInvalidate.get(lp3)).toEqual({ id: 'db-fetched-3' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('handles n=0 compose case', async () => {
|
|
88
|
+
const lp1 = { lp: '1' };
|
|
89
|
+
const composedSecondaryEntityCache = new ComposedSecondaryEntityCache<
|
|
90
|
+
TestLoadParams,
|
|
91
|
+
TestFields
|
|
92
|
+
>([]);
|
|
93
|
+
const results = await composedSecondaryEntityCache.loadManyThroughAsync(
|
|
94
|
+
[lp1],
|
|
95
|
+
async (fetcherLoadParamsArray) =>
|
|
96
|
+
new Map(fetcherLoadParamsArray.map((flp) => [flp, { id: `db-fetched-${flp.lp}` }]))
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(results.get(lp1)).toEqual({ id: 'db-fetched-1' });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -5,7 +5,7 @@ import Entity from '../Entity';
|
|
|
5
5
|
import EntityCompanionProvider, { EntityCompanionDefinition } from '../EntityCompanionProvider';
|
|
6
6
|
import EntityConfiguration from '../EntityConfiguration';
|
|
7
7
|
import { UUIDField } from '../EntityFields';
|
|
8
|
-
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
|
|
8
|
+
import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
|
|
9
9
|
import { EntityQueryContext } from '../EntityQueryContext';
|
|
10
10
|
import ViewerContext from '../ViewerContext';
|
|
11
11
|
import { enforceResultsAsync } from '../entityUtils';
|
|
@@ -51,6 +51,7 @@ class DenyIfNotOwnerPrivacyPolicyRule extends PrivacyPolicyRule<
|
|
|
51
51
|
async evaluateAsync(
|
|
52
52
|
viewerContext: TestUserViewerContext,
|
|
53
53
|
_queryContext: EntityQueryContext,
|
|
54
|
+
_evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
|
54
55
|
entity: BlahEntity
|
|
55
56
|
): Promise<RuleEvaluationResult> {
|
|
56
57
|
if (viewerContext.getUserID() === entity.getField('ownerID')) {
|
|
@@ -5,10 +5,13 @@ import { EntityEdgeDeletionBehavior } from '../EntityFieldDefinition';
|
|
|
5
5
|
import { UUIDField } from '../EntityFields';
|
|
6
6
|
import { EntityTriggerMutationInfo, EntityMutationType } from '../EntityMutationInfo';
|
|
7
7
|
import { EntityMutationTrigger } from '../EntityMutationTriggerConfiguration';
|
|
8
|
-
import EntityPrivacyPolicy
|
|
9
|
-
|
|
8
|
+
import EntityPrivacyPolicy, {
|
|
9
|
+
EntityPrivacyPolicyEvaluationContext,
|
|
10
|
+
EntityAuthorizationAction,
|
|
11
|
+
} from '../EntityPrivacyPolicy';
|
|
12
|
+
import { EntityTransactionalQueryContext, EntityQueryContext } from '../EntityQueryContext';
|
|
10
13
|
import { CacheStatus } from '../internal/ReadThroughEntityCache';
|
|
11
|
-
import
|
|
14
|
+
import PrivacyPolicyRule, { RuleEvaluationResult } from '../rules/PrivacyPolicyRule';
|
|
12
15
|
import TestViewerContext from '../testfixtures/TestViewerContext';
|
|
13
16
|
import { InMemoryFullCacheStubCacheAdapter } from '../utils/testing/StubCacheAdapter';
|
|
14
17
|
import { createUnitTestEntityCompanionProvider } from '../utils/testing/createUnitTestEntityCompanionProvider';
|
|
@@ -27,35 +30,83 @@ interface GrandChildFields {
|
|
|
27
30
|
parent_id: string;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
|
31
|
-
any,
|
|
32
|
-
string,
|
|
33
|
-
TestViewerContext,
|
|
34
|
-
any,
|
|
35
|
-
any
|
|
36
|
-
> {
|
|
37
|
-
protected override readonly readRules = [
|
|
38
|
-
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
|
|
39
|
-
];
|
|
40
|
-
protected override readonly createRules = [
|
|
41
|
-
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
|
|
42
|
-
];
|
|
43
|
-
protected override readonly updateRules = [
|
|
44
|
-
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
|
|
45
|
-
];
|
|
46
|
-
protected override readonly deleteRules = [
|
|
47
|
-
new AlwaysAllowPrivacyPolicyRule<any, string, TestViewerContext, any, any>(),
|
|
48
|
-
];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
33
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
52
34
|
const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) => {
|
|
53
35
|
const triggerExecutionCounts = {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
36
|
+
ParentEntity: 0,
|
|
37
|
+
ChildEntity: 0,
|
|
38
|
+
GrandChildEntity: 0,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const privacyPolicyEvaluationRecords = {
|
|
42
|
+
shouldRecord: false,
|
|
43
|
+
ParentEntity: {
|
|
44
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
45
|
+
[EntityAuthorizationAction.READ]: [],
|
|
46
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
47
|
+
[EntityAuthorizationAction.DELETE]: [],
|
|
48
|
+
},
|
|
49
|
+
ChildEntity: {
|
|
50
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
51
|
+
[EntityAuthorizationAction.READ]: [],
|
|
52
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
53
|
+
[EntityAuthorizationAction.DELETE]: [],
|
|
54
|
+
},
|
|
55
|
+
GrandChildEntity: {
|
|
56
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
57
|
+
[EntityAuthorizationAction.READ]: [],
|
|
58
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
59
|
+
[EntityAuthorizationAction.DELETE]: [],
|
|
60
|
+
},
|
|
57
61
|
};
|
|
58
62
|
|
|
63
|
+
class AlwaysAllowPrivacyPolicyRuleThatRecords extends PrivacyPolicyRule<
|
|
64
|
+
any,
|
|
65
|
+
string,
|
|
66
|
+
TestViewerContext,
|
|
67
|
+
any,
|
|
68
|
+
any
|
|
69
|
+
> {
|
|
70
|
+
constructor(private readonly action: EntityAuthorizationAction) {
|
|
71
|
+
super();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async evaluateAsync(
|
|
75
|
+
_viewerContext: TestViewerContext,
|
|
76
|
+
_queryContext: EntityQueryContext,
|
|
77
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext,
|
|
78
|
+
entity: any
|
|
79
|
+
): Promise<RuleEvaluationResult> {
|
|
80
|
+
if (privacyPolicyEvaluationRecords.shouldRecord) {
|
|
81
|
+
(privacyPolicyEvaluationRecords as any)[entity.constructor.name][this.action].push(
|
|
82
|
+
evaluationContext
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return RuleEvaluationResult.ALLOW;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
|
90
|
+
any,
|
|
91
|
+
string,
|
|
92
|
+
TestViewerContext,
|
|
93
|
+
any,
|
|
94
|
+
any
|
|
95
|
+
> {
|
|
96
|
+
protected override readonly readRules = [
|
|
97
|
+
new AlwaysAllowPrivacyPolicyRuleThatRecords(EntityAuthorizationAction.READ),
|
|
98
|
+
];
|
|
99
|
+
protected override readonly createRules = [
|
|
100
|
+
new AlwaysAllowPrivacyPolicyRuleThatRecords(EntityAuthorizationAction.CREATE),
|
|
101
|
+
];
|
|
102
|
+
protected override readonly updateRules = [
|
|
103
|
+
new AlwaysAllowPrivacyPolicyRuleThatRecords(EntityAuthorizationAction.UPDATE),
|
|
104
|
+
];
|
|
105
|
+
protected override readonly deleteRules = [
|
|
106
|
+
new AlwaysAllowPrivacyPolicyRuleThatRecords(EntityAuthorizationAction.DELETE),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
59
110
|
class ParentCheckInfoTrigger extends EntityMutationTrigger<
|
|
60
111
|
ParentFields,
|
|
61
112
|
string,
|
|
@@ -76,7 +127,7 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
|
|
76
127
|
throw new Error('Parent entity should not have casade delete cause');
|
|
77
128
|
}
|
|
78
129
|
|
|
79
|
-
triggerExecutionCounts.
|
|
130
|
+
triggerExecutionCounts.ParentEntity++;
|
|
80
131
|
}
|
|
81
132
|
}
|
|
82
133
|
|
|
@@ -111,7 +162,7 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
|
|
111
162
|
throw new Error('Child entity should not have two-level casade delete cause');
|
|
112
163
|
}
|
|
113
164
|
|
|
114
|
-
triggerExecutionCounts.
|
|
165
|
+
triggerExecutionCounts.ChildEntity++;
|
|
115
166
|
}
|
|
116
167
|
}
|
|
117
168
|
|
|
@@ -165,7 +216,7 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
|
|
165
216
|
throw new Error('GrandChild entity should not have three-level casade delete cause');
|
|
166
217
|
}
|
|
167
218
|
|
|
168
|
-
triggerExecutionCounts.
|
|
219
|
+
triggerExecutionCounts.GrandChildEntity++;
|
|
169
220
|
}
|
|
170
221
|
}
|
|
171
222
|
|
|
@@ -297,14 +348,20 @@ const makeEntityClasses = (edgeDeletionBehavior: EntityEdgeDeletionBehavior) =>
|
|
|
297
348
|
ChildEntity,
|
|
298
349
|
GrandChildEntity,
|
|
299
350
|
triggerExecutionCounts,
|
|
351
|
+
privacyPolicyEvaluationRecords,
|
|
300
352
|
};
|
|
301
353
|
};
|
|
302
354
|
|
|
303
355
|
describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
304
356
|
describe('EntityEdgeDeletionBehavior.CASCADE_DELETE', () => {
|
|
305
357
|
it('deletes', async () => {
|
|
306
|
-
const {
|
|
307
|
-
|
|
358
|
+
const {
|
|
359
|
+
ParentEntity,
|
|
360
|
+
ChildEntity,
|
|
361
|
+
GrandChildEntity,
|
|
362
|
+
triggerExecutionCounts,
|
|
363
|
+
privacyPolicyEvaluationRecords,
|
|
364
|
+
} = makeEntityClasses(EntityEdgeDeletionBehavior.CASCADE_DELETE);
|
|
308
365
|
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
309
366
|
const viewerContext = new TestViewerContext(companionProvider);
|
|
310
367
|
|
|
@@ -326,7 +383,9 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
|
326
383
|
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
|
|
327
384
|
).resolves.not.toBeNull();
|
|
328
385
|
|
|
386
|
+
privacyPolicyEvaluationRecords.shouldRecord = true;
|
|
329
387
|
await ParentEntity.enforceDeleteAsync(parent);
|
|
388
|
+
privacyPolicyEvaluationRecords.shouldRecord = false;
|
|
330
389
|
|
|
331
390
|
await expect(
|
|
332
391
|
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
|
@@ -340,17 +399,82 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
|
340
399
|
|
|
341
400
|
// two calls for each trigger, one beforeDelete, one afterDelete
|
|
342
401
|
expect(triggerExecutionCounts).toMatchObject({
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
402
|
+
ParentEntity: 2,
|
|
403
|
+
ChildEntity: 2,
|
|
404
|
+
GrandChildEntity: 2,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(privacyPolicyEvaluationRecords).toMatchObject({
|
|
408
|
+
ParentEntity: {
|
|
409
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
410
|
+
[EntityAuthorizationAction.READ]: [],
|
|
411
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
412
|
+
// one DELETE auth action for parent (since it's being deleted)
|
|
413
|
+
[EntityAuthorizationAction.DELETE]: [{ cascadingDeleteCause: null }],
|
|
414
|
+
},
|
|
415
|
+
ChildEntity: {
|
|
416
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
417
|
+
// one READ auth action for child in order to delete via cascade
|
|
418
|
+
[EntityAuthorizationAction.READ]: [
|
|
419
|
+
{
|
|
420
|
+
cascadingDeleteCause: {
|
|
421
|
+
entity: expect.any(ParentEntity),
|
|
422
|
+
cascadingDeleteCause: null,
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
427
|
+
// one DELETE auth action for child (since it's being deleted via cascade)
|
|
428
|
+
[EntityAuthorizationAction.DELETE]: [
|
|
429
|
+
{
|
|
430
|
+
cascadingDeleteCause: {
|
|
431
|
+
entity: expect.any(ParentEntity),
|
|
432
|
+
cascadingDeleteCause: null,
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
GrandChildEntity: {
|
|
438
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
439
|
+
// one READ auth action for grandchild in order to delete via cascade
|
|
440
|
+
[EntityAuthorizationAction.READ]: [
|
|
441
|
+
{
|
|
442
|
+
cascadingDeleteCause: {
|
|
443
|
+
entity: expect.any(ChildEntity),
|
|
444
|
+
cascadingDeleteCause: {
|
|
445
|
+
entity: expect.any(ParentEntity),
|
|
446
|
+
cascadingDeleteCause: null,
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
452
|
+
// one DELETE auth action for grandchild (since it's being deleted via cascade)
|
|
453
|
+
[EntityAuthorizationAction.DELETE]: [
|
|
454
|
+
{
|
|
455
|
+
cascadingDeleteCause: {
|
|
456
|
+
entity: expect.any(ChildEntity),
|
|
457
|
+
cascadingDeleteCause: {
|
|
458
|
+
entity: expect.any(ParentEntity),
|
|
459
|
+
cascadingDeleteCause: null,
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
},
|
|
346
465
|
});
|
|
347
466
|
});
|
|
348
467
|
});
|
|
349
468
|
|
|
350
469
|
describe('EntityEdgeDeletionBehavior.SET_NULL', () => {
|
|
351
470
|
it('sets null', async () => {
|
|
352
|
-
const {
|
|
353
|
-
|
|
471
|
+
const {
|
|
472
|
+
ParentEntity,
|
|
473
|
+
ChildEntity,
|
|
474
|
+
GrandChildEntity,
|
|
475
|
+
triggerExecutionCounts,
|
|
476
|
+
privacyPolicyEvaluationRecords,
|
|
477
|
+
} = makeEntityClasses(EntityEdgeDeletionBehavior.SET_NULL);
|
|
354
478
|
|
|
355
479
|
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
356
480
|
const viewerContext = new TestViewerContext(companionProvider);
|
|
@@ -373,7 +497,9 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
|
373
497
|
GrandChildEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(grandchild.getID())
|
|
374
498
|
).resolves.not.toBeNull();
|
|
375
499
|
|
|
500
|
+
privacyPolicyEvaluationRecords.shouldRecord = true;
|
|
376
501
|
await ParentEntity.enforceDeleteAsync(parent);
|
|
502
|
+
privacyPolicyEvaluationRecords.shouldRecord = false;
|
|
377
503
|
|
|
378
504
|
await expect(
|
|
379
505
|
ParentEntity.loader(viewerContext).enforcing().loadByIDNullableAsync(parent.getID())
|
|
@@ -392,17 +518,70 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
|
392
518
|
// two calls for only parent trigger, one beforeDelete, one afterDelete
|
|
393
519
|
// when using set null the descendants aren't deleted
|
|
394
520
|
expect(triggerExecutionCounts).toMatchObject({
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
521
|
+
ParentEntity: 2,
|
|
522
|
+
ChildEntity: 0,
|
|
523
|
+
GrandChildEntity: 0,
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
expect(privacyPolicyEvaluationRecords).toMatchObject({
|
|
527
|
+
ParentEntity: {
|
|
528
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
529
|
+
[EntityAuthorizationAction.READ]: [],
|
|
530
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
531
|
+
// one DELETE auth action for parent (since it's being deleted)
|
|
532
|
+
[EntityAuthorizationAction.DELETE]: [{ cascadingDeleteCause: null }],
|
|
533
|
+
},
|
|
534
|
+
ChildEntity: {
|
|
535
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
536
|
+
|
|
537
|
+
// two READs auth action for Child during parent deletion:
|
|
538
|
+
// 1. Read to initiate the SET_NULL (to update the entity)
|
|
539
|
+
// 1. Read automatically post-update
|
|
540
|
+
// no other entities are read since it is not cascaded past first entity
|
|
541
|
+
[EntityAuthorizationAction.READ]: [
|
|
542
|
+
{
|
|
543
|
+
cascadingDeleteCause: {
|
|
544
|
+
entity: expect.any(ParentEntity),
|
|
545
|
+
cascadingDeleteCause: null,
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
cascadingDeleteCause: {
|
|
550
|
+
entity: expect.any(ParentEntity),
|
|
551
|
+
cascadingDeleteCause: null,
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
// one UPDATE to set null
|
|
556
|
+
[EntityAuthorizationAction.UPDATE]: [
|
|
557
|
+
{
|
|
558
|
+
cascadingDeleteCause: {
|
|
559
|
+
entity: expect.any(ParentEntity),
|
|
560
|
+
cascadingDeleteCause: null,
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
[EntityAuthorizationAction.DELETE]: [],
|
|
565
|
+
},
|
|
566
|
+
GrandChildEntity: {
|
|
567
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
568
|
+
[EntityAuthorizationAction.READ]: [],
|
|
569
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
570
|
+
[EntityAuthorizationAction.DELETE]: [],
|
|
571
|
+
},
|
|
398
572
|
});
|
|
399
573
|
});
|
|
400
574
|
});
|
|
401
575
|
|
|
402
576
|
describe('EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE', () => {
|
|
403
577
|
it('invalidates the cache', async () => {
|
|
404
|
-
const {
|
|
405
|
-
|
|
578
|
+
const {
|
|
579
|
+
ParentEntity,
|
|
580
|
+
ChildEntity,
|
|
581
|
+
GrandChildEntity,
|
|
582
|
+
triggerExecutionCounts,
|
|
583
|
+
privacyPolicyEvaluationRecords,
|
|
584
|
+
} = makeEntityClasses(EntityEdgeDeletionBehavior.CASCADE_DELETE_INVALIDATE_CACHE);
|
|
406
585
|
|
|
407
586
|
const companionProvider = createUnitTestEntityCompanionProvider();
|
|
408
587
|
const viewerContext = new TestViewerContext(companionProvider);
|
|
@@ -447,7 +626,9 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
|
447
626
|
]);
|
|
448
627
|
expect(grandChildCachedBefore.get(child.getID())?.status).toEqual(CacheStatus.HIT);
|
|
449
628
|
|
|
629
|
+
privacyPolicyEvaluationRecords.shouldRecord = true;
|
|
450
630
|
await ParentEntity.enforceDeleteAsync(parent);
|
|
631
|
+
privacyPolicyEvaluationRecords.shouldRecord = false;
|
|
451
632
|
|
|
452
633
|
const childCachedAfter = await childCacheAdapter.loadManyAsync('parent_id', [parent.getID()]);
|
|
453
634
|
expect(childCachedAfter.get(parent.getID())?.status).toEqual(CacheStatus.MISS);
|
|
@@ -469,9 +650,69 @@ describe('EntityMutator.processEntityDeletionForInboundEdgesAsync', () => {
|
|
|
469
650
|
|
|
470
651
|
// two calls for each trigger, one beforeDelete, one afterDelete
|
|
471
652
|
expect(triggerExecutionCounts).toMatchObject({
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
653
|
+
ParentEntity: 2,
|
|
654
|
+
ChildEntity: 2,
|
|
655
|
+
GrandChildEntity: 2,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
expect(privacyPolicyEvaluationRecords).toMatchObject({
|
|
659
|
+
ParentEntity: {
|
|
660
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
661
|
+
[EntityAuthorizationAction.READ]: [],
|
|
662
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
663
|
+
// one DELETE auth action for parent (since it's being deleted)
|
|
664
|
+
[EntityAuthorizationAction.DELETE]: [{ cascadingDeleteCause: null }],
|
|
665
|
+
},
|
|
666
|
+
ChildEntity: {
|
|
667
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
668
|
+
// one READ auth action for child in order to delete via cascade
|
|
669
|
+
[EntityAuthorizationAction.READ]: [
|
|
670
|
+
{
|
|
671
|
+
cascadingDeleteCause: {
|
|
672
|
+
entity: expect.any(ParentEntity),
|
|
673
|
+
cascadingDeleteCause: null,
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
],
|
|
677
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
678
|
+
// one DELETE auth action for child (since it's being deleted via cascade)
|
|
679
|
+
[EntityAuthorizationAction.DELETE]: [
|
|
680
|
+
{
|
|
681
|
+
cascadingDeleteCause: {
|
|
682
|
+
entity: expect.any(ParentEntity),
|
|
683
|
+
cascadingDeleteCause: null,
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
},
|
|
688
|
+
GrandChildEntity: {
|
|
689
|
+
[EntityAuthorizationAction.CREATE]: [],
|
|
690
|
+
// one READ auth action for grandchild in order to delete via cascade
|
|
691
|
+
[EntityAuthorizationAction.READ]: [
|
|
692
|
+
{
|
|
693
|
+
cascadingDeleteCause: {
|
|
694
|
+
entity: expect.any(ChildEntity),
|
|
695
|
+
cascadingDeleteCause: {
|
|
696
|
+
entity: expect.any(ParentEntity),
|
|
697
|
+
cascadingDeleteCause: null,
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
[EntityAuthorizationAction.UPDATE]: [],
|
|
703
|
+
// one DELETE auth action for grandchild (since it's being deleted via cascade)
|
|
704
|
+
[EntityAuthorizationAction.DELETE]: [
|
|
705
|
+
{
|
|
706
|
+
cascadingDeleteCause: {
|
|
707
|
+
entity: expect.any(ChildEntity),
|
|
708
|
+
cascadingDeleteCause: {
|
|
709
|
+
entity: expect.any(ParentEntity),
|
|
710
|
+
cascadingDeleteCause: null,
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
},
|
|
475
716
|
});
|
|
476
717
|
});
|
|
477
718
|
});
|
|
@@ -5,7 +5,7 @@ import { EntityCompanionDefinition } from '../EntityCompanionProvider';
|
|
|
5
5
|
import EntityConfiguration from '../EntityConfiguration';
|
|
6
6
|
import { StringField } from '../EntityFields';
|
|
7
7
|
import EntityLoader from '../EntityLoader';
|
|
8
|
-
import EntityPrivacyPolicy from '../EntityPrivacyPolicy';
|
|
8
|
+
import EntityPrivacyPolicy, { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
|
|
9
9
|
import ViewerContext from '../ViewerContext';
|
|
10
10
|
import EntityDataManager from '../internal/EntityDataManager';
|
|
11
11
|
import ReadThroughEntityCache from '../internal/ReadThroughEntityCache';
|
|
@@ -118,6 +118,7 @@ export const testEntityCompanion = new EntityCompanionDefinition({
|
|
|
118
118
|
describe(EntityLoader, () => {
|
|
119
119
|
it('handles thrown errors and literals from constructor', async () => {
|
|
120
120
|
const viewerContext = instance(mock(ViewerContext));
|
|
121
|
+
const privacyPolicyEvaluationContext = instance(mock<EntityPrivacyPolicyEvaluationContext>());
|
|
121
122
|
const metricsAdapter = instance(mock<IEntityMetricsAdapter>());
|
|
122
123
|
const queryContext = StubQueryContextProvider.getQueryContext();
|
|
123
124
|
|
|
@@ -154,6 +155,7 @@ describe(EntityLoader, () => {
|
|
|
154
155
|
const entityLoader = new EntityLoader(
|
|
155
156
|
viewerContext,
|
|
156
157
|
queryContext,
|
|
158
|
+
privacyPolicyEvaluationContext,
|
|
157
159
|
testEntityConfiguration,
|
|
158
160
|
TestEntity,
|
|
159
161
|
privacyPolicy,
|