@expo/entity 0.53.0 → 0.55.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/src/ComposedEntityCacheAdapter.js.map +1 -1
- package/build/src/ComposedSecondaryEntityCache.js.map +1 -1
- package/build/src/EntityDatabaseAdapter.js.map +1 -1
- package/build/src/EntityFieldDefinition.js.map +1 -1
- package/build/src/EntityLoaderUtils.js +14 -18
- package/build/src/EntityLoaderUtils.js.map +1 -1
- package/build/src/GenericEntityCacheAdapter.js.map +1 -1
- package/build/src/GenericSecondaryEntityCache.js.map +1 -1
- package/build/src/entityUtils.js +7 -2
- package/build/src/entityUtils.js.map +1 -1
- package/build/src/errors/EntityCacheAdapterError.d.ts +2 -2
- package/build/src/errors/EntityCacheAdapterError.js +12 -2
- package/build/src/errors/EntityCacheAdapterError.js.map +1 -1
- package/build/src/errors/EntityDatabaseAdapterError.d.ts +24 -24
- package/build/src/errors/EntityDatabaseAdapterError.js +111 -24
- package/build/src/errors/EntityDatabaseAdapterError.js.map +1 -1
- package/build/src/errors/EntityError.d.ts +2 -4
- package/build/src/errors/EntityError.js +5 -8
- package/build/src/errors/EntityError.js.map +1 -1
- package/build/src/errors/EntityInvalidFieldValueError.d.ts +2 -2
- package/build/src/errors/EntityInvalidFieldValueError.js +9 -2
- package/build/src/errors/EntityInvalidFieldValueError.js.map +1 -1
- package/build/src/errors/EntityNotAuthorizedError.d.ts +2 -2
- package/build/src/errors/EntityNotAuthorizedError.js +9 -2
- package/build/src/errors/EntityNotAuthorizedError.js.map +1 -1
- package/build/src/errors/EntityNotFoundError.d.ts +2 -2
- package/build/src/errors/EntityNotFoundError.js +9 -2
- package/build/src/errors/EntityNotFoundError.js.map +1 -1
- package/build/src/index.d.ts +4 -0
- package/build/src/index.js +4 -0
- package/build/src/index.js.map +1 -1
- package/build/src/internal/CompositeFieldHolder.js.map +1 -1
- package/build/src/internal/CompositeFieldValueMap.js.map +1 -1
- package/build/src/internal/SingleFieldHolder.js.map +1 -1
- package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.d.ts +10 -0
- package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.js +19 -0
- package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.js.map +1 -0
- package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.d.ts +10 -0
- package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.js +19 -0
- package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.js.map +1 -0
- package/build/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.d.ts +66 -0
- package/build/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.js +75 -0
- package/build/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.js.map +1 -0
- package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.d.ts +12 -0
- package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.js +23 -0
- package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.js.map +1 -0
- package/build/src/utils/EntityPrivacyUtils.js +34 -15
- package/build/src/utils/EntityPrivacyUtils.js.map +1 -1
- package/package.json +10 -13
- package/src/ComposedEntityCacheAdapter.ts +1 -2
- package/src/ComposedSecondaryEntityCache.ts +4 -3
- package/src/EntityDatabaseAdapter.ts +3 -2
- package/src/EntityFieldDefinition.ts +1 -2
- package/src/EntityLoaderUtils.ts +17 -20
- package/src/GenericEntityCacheAdapter.ts +1 -2
- package/src/GenericSecondaryEntityCache.ts +1 -2
- package/src/__tests__/ComposedCacheAdapter-test.ts +4 -3
- package/src/__tests__/EntityFields-test.ts +2 -8
- package/src/entityUtils.ts +7 -4
- package/src/errors/EntityCacheAdapterError.ts +16 -3
- package/src/errors/EntityDatabaseAdapterError.ts +137 -25
- package/src/errors/EntityError.ts +7 -8
- package/src/errors/EntityInvalidFieldValueError.ts +11 -2
- package/src/errors/EntityNotAuthorizedError.ts +11 -2
- package/src/errors/EntityNotFoundError.ts +11 -2
- package/src/errors/__tests__/EntityDatabaseAdapterError-test.ts +68 -11
- package/src/errors/__tests__/EntityError-test.ts +36 -0
- package/src/index.ts +4 -0
- package/src/internal/CompositeFieldHolder.ts +7 -10
- package/src/internal/CompositeFieldValueMap.ts +1 -2
- package/src/internal/SingleFieldHolder.ts +10 -6
- package/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.ts +47 -0
- package/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.ts +47 -0
- package/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.ts +177 -0
- package/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.ts +46 -0
- package/src/rules/__tests__/AllowIfAllSubRulesAllowPrivacyPolicyRule-test.ts +64 -0
- package/src/rules/__tests__/AllowIfAnySubRuleAllowsPrivacyPolicyRule-test.ts +64 -0
- package/src/rules/__tests__/AllowIfInParentCascadeDeletionPrivacyPolicyRule-test.ts +258 -0
- package/src/rules/__tests__/EvaluateIfEntityFieldPredicatePrivacyPolicyRule-test.ts +47 -0
- package/src/utils/EntityPrivacyUtils.ts +61 -20
- package/src/utils/__testfixtures__/StubCacheAdapter.ts +2 -4
- package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +1 -1
- package/src/utils/__tests__/EntityPrivacyUtils-test.ts +295 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { anything, instance, mock } from 'ts-mockito';
|
|
2
|
+
|
|
3
|
+
import { EntityPrivacyPolicyEvaluationContext } from '../../EntityPrivacyPolicy';
|
|
4
|
+
import { EntityQueryContext } from '../../EntityQueryContext';
|
|
5
|
+
import { ViewerContext } from '../../ViewerContext';
|
|
6
|
+
import { describePrivacyPolicyRule } from '../../utils/__testfixtures__/PrivacyPolicyRuleTestUtils';
|
|
7
|
+
import { AllowIfAllSubRulesAllowPrivacyPolicyRule } from '../AllowIfAllSubRulesAllowPrivacyPolicyRule';
|
|
8
|
+
import { AlwaysAllowPrivacyPolicyRule } from '../AlwaysAllowPrivacyPolicyRule';
|
|
9
|
+
import { AlwaysDenyPrivacyPolicyRule } from '../AlwaysDenyPrivacyPolicyRule';
|
|
10
|
+
import { AlwaysSkipPrivacyPolicyRule } from '../AlwaysSkipPrivacyPolicyRule';
|
|
11
|
+
|
|
12
|
+
describePrivacyPolicyRule(
|
|
13
|
+
new AllowIfAllSubRulesAllowPrivacyPolicyRule([
|
|
14
|
+
new AlwaysAllowPrivacyPolicyRule(),
|
|
15
|
+
new AlwaysSkipPrivacyPolicyRule(),
|
|
16
|
+
]),
|
|
17
|
+
{
|
|
18
|
+
skipCases: [
|
|
19
|
+
{
|
|
20
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
21
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
22
|
+
evaluationContext:
|
|
23
|
+
instance(mock<EntityPrivacyPolicyEvaluationContext<any, any, any, any, any>>()),
|
|
24
|
+
entity: anything(),
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
describePrivacyPolicyRule(
|
|
31
|
+
new AllowIfAllSubRulesAllowPrivacyPolicyRule([
|
|
32
|
+
new AlwaysAllowPrivacyPolicyRule(),
|
|
33
|
+
new AlwaysDenyPrivacyPolicyRule(),
|
|
34
|
+
]),
|
|
35
|
+
{
|
|
36
|
+
skipCases: [
|
|
37
|
+
{
|
|
38
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
39
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
40
|
+
evaluationContext:
|
|
41
|
+
instance(mock<EntityPrivacyPolicyEvaluationContext<any, any, any, any, any>>()),
|
|
42
|
+
entity: anything(),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
describePrivacyPolicyRule(
|
|
49
|
+
new AllowIfAllSubRulesAllowPrivacyPolicyRule([
|
|
50
|
+
new AlwaysAllowPrivacyPolicyRule(),
|
|
51
|
+
new AlwaysAllowPrivacyPolicyRule(),
|
|
52
|
+
]),
|
|
53
|
+
{
|
|
54
|
+
allowCases: [
|
|
55
|
+
{
|
|
56
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
57
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
58
|
+
evaluationContext:
|
|
59
|
+
instance(mock<EntityPrivacyPolicyEvaluationContext<any, any, any, any, any>>()),
|
|
60
|
+
entity: anything(),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { anything, instance, mock } from 'ts-mockito';
|
|
2
|
+
|
|
3
|
+
import { EntityPrivacyPolicyEvaluationContext } from '../../EntityPrivacyPolicy';
|
|
4
|
+
import { EntityQueryContext } from '../../EntityQueryContext';
|
|
5
|
+
import { ViewerContext } from '../../ViewerContext';
|
|
6
|
+
import { describePrivacyPolicyRule } from '../../utils/__testfixtures__/PrivacyPolicyRuleTestUtils';
|
|
7
|
+
import { AllowIfAnySubRuleAllowsPrivacyPolicyRule } from '../AllowIfAnySubRuleAllowsPrivacyPolicyRule';
|
|
8
|
+
import { AlwaysAllowPrivacyPolicyRule } from '../AlwaysAllowPrivacyPolicyRule';
|
|
9
|
+
import { AlwaysDenyPrivacyPolicyRule } from '../AlwaysDenyPrivacyPolicyRule';
|
|
10
|
+
import { AlwaysSkipPrivacyPolicyRule } from '../AlwaysSkipPrivacyPolicyRule';
|
|
11
|
+
|
|
12
|
+
describePrivacyPolicyRule(
|
|
13
|
+
new AllowIfAnySubRuleAllowsPrivacyPolicyRule([
|
|
14
|
+
new AlwaysAllowPrivacyPolicyRule(),
|
|
15
|
+
new AlwaysSkipPrivacyPolicyRule(),
|
|
16
|
+
]),
|
|
17
|
+
{
|
|
18
|
+
allowCases: [
|
|
19
|
+
{
|
|
20
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
21
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
22
|
+
evaluationContext:
|
|
23
|
+
instance(mock<EntityPrivacyPolicyEvaluationContext<any, any, any, any, any>>()),
|
|
24
|
+
entity: anything(),
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
describePrivacyPolicyRule(
|
|
31
|
+
new AllowIfAnySubRuleAllowsPrivacyPolicyRule([
|
|
32
|
+
new AlwaysAllowPrivacyPolicyRule(),
|
|
33
|
+
new AlwaysDenyPrivacyPolicyRule(),
|
|
34
|
+
]),
|
|
35
|
+
{
|
|
36
|
+
allowCases: [
|
|
37
|
+
{
|
|
38
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
39
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
40
|
+
evaluationContext:
|
|
41
|
+
instance(mock<EntityPrivacyPolicyEvaluationContext<any, any, any, any, any>>()),
|
|
42
|
+
entity: anything(),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
describePrivacyPolicyRule(
|
|
49
|
+
new AllowIfAnySubRuleAllowsPrivacyPolicyRule([
|
|
50
|
+
new AlwaysSkipPrivacyPolicyRule(),
|
|
51
|
+
new AlwaysSkipPrivacyPolicyRule(),
|
|
52
|
+
]),
|
|
53
|
+
{
|
|
54
|
+
skipCases: [
|
|
55
|
+
{
|
|
56
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
57
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
58
|
+
evaluationContext:
|
|
59
|
+
instance(mock<EntityPrivacyPolicyEvaluationContext<any, any, any, any, any>>()),
|
|
60
|
+
entity: anything(),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
);
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { instance, mock, when } from 'ts-mockito';
|
|
2
|
+
|
|
3
|
+
import { EntityCompanionDefinition } from '../../EntityCompanionProvider';
|
|
4
|
+
import { EntityPrivacyPolicy } from '../../EntityPrivacyPolicy';
|
|
5
|
+
import { EntityQueryContext } from '../../EntityQueryContext';
|
|
6
|
+
import { ReadonlyEntity } from '../../ReadonlyEntity';
|
|
7
|
+
import { ViewerContext } from '../../ViewerContext';
|
|
8
|
+
import { describePrivacyPolicyRule } from '../../utils/__testfixtures__/PrivacyPolicyRuleTestUtils';
|
|
9
|
+
import { AllowIfInParentCascadeDeletionPrivacyPolicyRule } from '../AllowIfInParentCascadeDeletionPrivacyPolicyRule';
|
|
10
|
+
|
|
11
|
+
// Define test field types
|
|
12
|
+
type ParentFields = {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ChildFields = {
|
|
18
|
+
id: string;
|
|
19
|
+
parent_id: string | null;
|
|
20
|
+
parent_name: string | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Create a mock privacy policy for the parent entity
|
|
24
|
+
class TestParentPrivacyPolicy extends EntityPrivacyPolicy<
|
|
25
|
+
ParentFields,
|
|
26
|
+
'id',
|
|
27
|
+
ViewerContext,
|
|
28
|
+
TestParentEntity
|
|
29
|
+
> {
|
|
30
|
+
protected override readonly readRules = [];
|
|
31
|
+
protected override readonly createRules = [];
|
|
32
|
+
protected override readonly updateRules = [];
|
|
33
|
+
protected override readonly deleteRules = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Create a mock parent entity class with required static method
|
|
37
|
+
class TestParentEntity extends ReadonlyEntity<ParentFields, 'id', ViewerContext> {
|
|
38
|
+
static defineCompanionDefinition(): EntityCompanionDefinition<
|
|
39
|
+
ParentFields,
|
|
40
|
+
'id',
|
|
41
|
+
ViewerContext,
|
|
42
|
+
TestParentEntity,
|
|
43
|
+
TestParentPrivacyPolicy
|
|
44
|
+
> {
|
|
45
|
+
throw new Error('Not implemented for test');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create a mock child entity class
|
|
50
|
+
class TestChildEntity extends ReadonlyEntity<ChildFields, 'id', ViewerContext> {}
|
|
51
|
+
|
|
52
|
+
// Mock parent entities
|
|
53
|
+
const parentEntityMock = mock(TestParentEntity);
|
|
54
|
+
when(parentEntityMock.getID()).thenReturn('5');
|
|
55
|
+
const parentEntity = instance(parentEntityMock);
|
|
56
|
+
Object.setPrototypeOf(parentEntity, TestParentEntity.prototype);
|
|
57
|
+
|
|
58
|
+
const otherParentEntityMock = mock(TestParentEntity);
|
|
59
|
+
when(otherParentEntityMock.getID()).thenReturn('6');
|
|
60
|
+
const otherParentEntity = instance(otherParentEntityMock);
|
|
61
|
+
Object.setPrototypeOf(otherParentEntity, TestParentEntity.prototype);
|
|
62
|
+
|
|
63
|
+
// Mock a non-parent entity (different class)
|
|
64
|
+
class UnrelatedOtherEntity extends ReadonlyEntity<{ id: string }, 'id', ViewerContext> {}
|
|
65
|
+
const unrelatedOtherEntityMock = mock(UnrelatedOtherEntity);
|
|
66
|
+
Object.setPrototypeOf(unrelatedOtherEntityMock, UnrelatedOtherEntity.prototype);
|
|
67
|
+
|
|
68
|
+
// Mock child entities
|
|
69
|
+
const childEntityMock = mock(TestChildEntity);
|
|
70
|
+
when(childEntityMock.getField('parent_id')).thenReturn('5');
|
|
71
|
+
|
|
72
|
+
const childEntityMockWithNullifiedField = mock(TestChildEntity);
|
|
73
|
+
when(childEntityMockWithNullifiedField.getField('parent_id')).thenReturn(null);
|
|
74
|
+
|
|
75
|
+
const childEntityDifferentParentMock = mock(TestChildEntity);
|
|
76
|
+
when(childEntityDifferentParentMock.getField('parent_id')).thenReturn('6');
|
|
77
|
+
|
|
78
|
+
describePrivacyPolicyRule(
|
|
79
|
+
new AllowIfInParentCascadeDeletionPrivacyPolicyRule<
|
|
80
|
+
ChildFields,
|
|
81
|
+
'id',
|
|
82
|
+
ViewerContext,
|
|
83
|
+
TestChildEntity,
|
|
84
|
+
ParentFields,
|
|
85
|
+
'id',
|
|
86
|
+
TestParentEntity,
|
|
87
|
+
TestParentPrivacyPolicy
|
|
88
|
+
>({
|
|
89
|
+
fieldIdentifyingParentEntity: 'parent_id',
|
|
90
|
+
parentEntityClass: TestParentEntity,
|
|
91
|
+
}),
|
|
92
|
+
{
|
|
93
|
+
allowCases: [
|
|
94
|
+
// parent id matches parent being deleted, field not yet nullified
|
|
95
|
+
{
|
|
96
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
97
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
98
|
+
evaluationContext: {
|
|
99
|
+
previousValue: instance(childEntityMock),
|
|
100
|
+
cascadingDeleteCause: {
|
|
101
|
+
entity: parentEntity,
|
|
102
|
+
cascadingDeleteCause: null,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
entity: instance(childEntityMock),
|
|
106
|
+
},
|
|
107
|
+
// parent id matches parent being deleted, field null in current version but filled in previous version
|
|
108
|
+
{
|
|
109
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
110
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
111
|
+
evaluationContext: {
|
|
112
|
+
previousValue: instance(childEntityMock),
|
|
113
|
+
cascadingDeleteCause: {
|
|
114
|
+
entity: parentEntity,
|
|
115
|
+
cascadingDeleteCause: null,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
entity: instance(childEntityMockWithNullifiedField),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
skipCases: [
|
|
122
|
+
// no cascading delete
|
|
123
|
+
{
|
|
124
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
125
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
126
|
+
evaluationContext: {
|
|
127
|
+
previousValue: null,
|
|
128
|
+
cascadingDeleteCause: null,
|
|
129
|
+
},
|
|
130
|
+
entity: instance(childEntityMock),
|
|
131
|
+
},
|
|
132
|
+
// cascading delete not from parent entity class
|
|
133
|
+
{
|
|
134
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
135
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
136
|
+
evaluationContext: {
|
|
137
|
+
previousValue: null,
|
|
138
|
+
cascadingDeleteCause: {
|
|
139
|
+
entity: instance(unrelatedOtherEntityMock),
|
|
140
|
+
cascadingDeleteCause: null,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
entity: instance(childEntityMock),
|
|
144
|
+
},
|
|
145
|
+
// cascading delete from different parent, field not nullified
|
|
146
|
+
{
|
|
147
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
148
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
149
|
+
evaluationContext: {
|
|
150
|
+
previousValue: null,
|
|
151
|
+
cascadingDeleteCause: {
|
|
152
|
+
entity: otherParentEntity,
|
|
153
|
+
cascadingDeleteCause: null,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
entity: instance(childEntityMock),
|
|
157
|
+
},
|
|
158
|
+
// entity belongs to different parent
|
|
159
|
+
{
|
|
160
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
161
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
162
|
+
evaluationContext: {
|
|
163
|
+
previousValue: null,
|
|
164
|
+
cascadingDeleteCause: {
|
|
165
|
+
entity: parentEntity,
|
|
166
|
+
cascadingDeleteCause: null,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
entity: instance(childEntityDifferentParentMock),
|
|
170
|
+
},
|
|
171
|
+
// parent id field undefined (null) and no previous value
|
|
172
|
+
{
|
|
173
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
174
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
175
|
+
evaluationContext: {
|
|
176
|
+
previousValue: null,
|
|
177
|
+
cascadingDeleteCause: {
|
|
178
|
+
entity: parentEntity,
|
|
179
|
+
cascadingDeleteCause: null,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
entity: instance(childEntityMockWithNullifiedField),
|
|
183
|
+
},
|
|
184
|
+
// parent id now null but previous value different parent
|
|
185
|
+
{
|
|
186
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
187
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
188
|
+
evaluationContext: {
|
|
189
|
+
previousValue: instance(childEntityDifferentParentMock),
|
|
190
|
+
cascadingDeleteCause: {
|
|
191
|
+
entity: parentEntity,
|
|
192
|
+
cascadingDeleteCause: null,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
entity: instance(childEntityMockWithNullifiedField),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Test with custom lookup field (parentEntityLookupByField)
|
|
202
|
+
const parentEntityWithNameMock = mock(TestParentEntity);
|
|
203
|
+
when(parentEntityWithNameMock.getField('name')).thenReturn('test-name');
|
|
204
|
+
const parentEntityWithName = instance(parentEntityWithNameMock);
|
|
205
|
+
Object.setPrototypeOf(parentEntityWithName, TestParentEntity.prototype);
|
|
206
|
+
|
|
207
|
+
const childEntityWithNameRefMock = mock(TestChildEntity);
|
|
208
|
+
when(childEntityWithNameRefMock.getField('parent_name')).thenReturn('test-name');
|
|
209
|
+
|
|
210
|
+
const childEntityWithNameRefMockWithNullifiedField = mock(TestChildEntity);
|
|
211
|
+
when(childEntityWithNameRefMockWithNullifiedField.getField('parent_name')).thenReturn(null);
|
|
212
|
+
|
|
213
|
+
describePrivacyPolicyRule(
|
|
214
|
+
new AllowIfInParentCascadeDeletionPrivacyPolicyRule<
|
|
215
|
+
ChildFields,
|
|
216
|
+
'id',
|
|
217
|
+
ViewerContext,
|
|
218
|
+
TestChildEntity,
|
|
219
|
+
ParentFields,
|
|
220
|
+
'id',
|
|
221
|
+
TestParentEntity,
|
|
222
|
+
TestParentPrivacyPolicy
|
|
223
|
+
>({
|
|
224
|
+
fieldIdentifyingParentEntity: 'parent_name',
|
|
225
|
+
parentEntityClass: TestParentEntity,
|
|
226
|
+
parentEntityLookupByField: 'name',
|
|
227
|
+
}),
|
|
228
|
+
{
|
|
229
|
+
allowCases: [
|
|
230
|
+
// parent name matches parent being deleted, field not yet nullified
|
|
231
|
+
{
|
|
232
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
233
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
234
|
+
evaluationContext: {
|
|
235
|
+
previousValue: instance(childEntityWithNameRefMock),
|
|
236
|
+
cascadingDeleteCause: {
|
|
237
|
+
entity: parentEntityWithName,
|
|
238
|
+
cascadingDeleteCause: null,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
entity: instance(childEntityWithNameRefMock),
|
|
242
|
+
},
|
|
243
|
+
// parent name matches parent being deleted, field null in current version but filled in previous version
|
|
244
|
+
{
|
|
245
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
246
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
247
|
+
evaluationContext: {
|
|
248
|
+
previousValue: instance(childEntityWithNameRefMock),
|
|
249
|
+
cascadingDeleteCause: {
|
|
250
|
+
entity: parentEntityWithName,
|
|
251
|
+
cascadingDeleteCause: null,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
entity: instance(childEntityWithNameRefMockWithNullifiedField),
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mock, instance, when } from 'ts-mockito';
|
|
2
|
+
|
|
3
|
+
import { EntityPrivacyPolicyEvaluationContext } from '../../EntityPrivacyPolicy';
|
|
4
|
+
import { EntityQueryContext } from '../../EntityQueryContext';
|
|
5
|
+
import { ViewerContext } from '../../ViewerContext';
|
|
6
|
+
import { describePrivacyPolicyRule } from '../../utils/__testfixtures__/PrivacyPolicyRuleTestUtils';
|
|
7
|
+
import { TestEntity, TestFields } from '../../utils/__testfixtures__/TestEntity';
|
|
8
|
+
import { AlwaysAllowPrivacyPolicyRule } from '../AlwaysAllowPrivacyPolicyRule';
|
|
9
|
+
import { EvaluateIfEntityFieldPredicatePrivacyPolicyRule } from '../EvaluateIfEntityFieldPredicatePrivacyPolicyRule';
|
|
10
|
+
|
|
11
|
+
const entityBlahMock = mock(TestEntity);
|
|
12
|
+
when(entityBlahMock.getField('testIndexedField')).thenReturn('1');
|
|
13
|
+
const entityBlah = instance(entityBlahMock);
|
|
14
|
+
|
|
15
|
+
const entityFooMock = mock(TestEntity);
|
|
16
|
+
when(entityFooMock.getField('testIndexedField')).thenReturn('2');
|
|
17
|
+
const entityFoo = instance(entityFooMock);
|
|
18
|
+
|
|
19
|
+
describePrivacyPolicyRule<TestFields, 'customIdField', ViewerContext, TestEntity>(
|
|
20
|
+
new EvaluateIfEntityFieldPredicatePrivacyPolicyRule<
|
|
21
|
+
TestFields,
|
|
22
|
+
'customIdField',
|
|
23
|
+
ViewerContext,
|
|
24
|
+
TestEntity,
|
|
25
|
+
'testIndexedField'
|
|
26
|
+
>('testIndexedField', (val) => val === '1', new AlwaysAllowPrivacyPolicyRule()),
|
|
27
|
+
{
|
|
28
|
+
allowCases: [
|
|
29
|
+
{
|
|
30
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
31
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
32
|
+
evaluationContext:
|
|
33
|
+
instance(mock<EntityPrivacyPolicyEvaluationContext<any, any, any, any, any>>()),
|
|
34
|
+
entity: entityBlah,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
skipCases: [
|
|
38
|
+
{
|
|
39
|
+
viewerContext: instance(mock(ViewerContext)),
|
|
40
|
+
queryContext: instance(mock(EntityQueryContext)),
|
|
41
|
+
evaluationContext:
|
|
42
|
+
instance(mock<EntityPrivacyPolicyEvaluationContext<any, any, any, any, any>>()),
|
|
43
|
+
entity: entityFoo,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
);
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
EntityEdgeDeletionAuthorizationInferenceBehavior,
|
|
6
6
|
EntityEdgeDeletionBehavior,
|
|
7
7
|
} from '../EntityFieldDefinition';
|
|
8
|
-
import {
|
|
9
|
-
import { EntityPrivacyPolicy } from '../EntityPrivacyPolicy';
|
|
8
|
+
import { EntityPrivacyPolicy, EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
|
|
10
9
|
import { EntityQueryContext } from '../EntityQueryContext';
|
|
10
|
+
import { ReadonlyEntity } from '../ReadonlyEntity';
|
|
11
11
|
import { ViewerContext } from '../ViewerContext';
|
|
12
12
|
import { failedResults, partitionArray } from '../entityUtils';
|
|
13
13
|
import { EntityNotAuthorizedError } from '../errors/EntityNotAuthorizedError';
|
|
@@ -74,7 +74,7 @@ export async function canViewerUpdateAsync<
|
|
|
74
74
|
const result = await canViewerUpdateInternalAsync(
|
|
75
75
|
entityClass,
|
|
76
76
|
sourceEntity,
|
|
77
|
-
|
|
77
|
+
{ previousValue: sourceEntity, cascadingDeleteCause: null },
|
|
78
78
|
queryContext,
|
|
79
79
|
);
|
|
80
80
|
return result.allowed;
|
|
@@ -121,7 +121,7 @@ export async function getCanViewerUpdateResultAsync<
|
|
|
121
121
|
return await canViewerUpdateInternalAsync(
|
|
122
122
|
entityClass,
|
|
123
123
|
sourceEntity,
|
|
124
|
-
|
|
124
|
+
{ previousValue: sourceEntity, cascadingDeleteCause: null },
|
|
125
125
|
queryContext,
|
|
126
126
|
);
|
|
127
127
|
}
|
|
@@ -149,7 +149,13 @@ async function canViewerUpdateInternalAsync<
|
|
|
149
149
|
TSelectedFields
|
|
150
150
|
>,
|
|
151
151
|
sourceEntity: TEntity,
|
|
152
|
-
|
|
152
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext<
|
|
153
|
+
TFields,
|
|
154
|
+
TIDField,
|
|
155
|
+
TViewerContext,
|
|
156
|
+
TEntity,
|
|
157
|
+
TSelectedFields
|
|
158
|
+
>,
|
|
153
159
|
queryContext: EntityQueryContext,
|
|
154
160
|
): Promise<EntityPrivacyEvaluationResult> {
|
|
155
161
|
const companion = sourceEntity
|
|
@@ -160,7 +166,7 @@ async function canViewerUpdateInternalAsync<
|
|
|
160
166
|
privacyPolicy.authorizeUpdateAsync(
|
|
161
167
|
sourceEntity.getViewerContext(),
|
|
162
168
|
queryContext,
|
|
163
|
-
|
|
169
|
+
evaluationContext,
|
|
164
170
|
sourceEntity,
|
|
165
171
|
companion.getMetricsAdapter(),
|
|
166
172
|
),
|
|
@@ -217,7 +223,7 @@ export async function canViewerDeleteAsync<
|
|
|
217
223
|
const result = await canViewerDeleteInternalAsync(
|
|
218
224
|
entityClass,
|
|
219
225
|
sourceEntity,
|
|
220
|
-
|
|
226
|
+
{ previousValue: null, cascadingDeleteCause: null },
|
|
221
227
|
queryContext,
|
|
222
228
|
);
|
|
223
229
|
return result.allowed;
|
|
@@ -264,7 +270,7 @@ export async function getCanViewerDeleteResultAsync<
|
|
|
264
270
|
return await canViewerDeleteInternalAsync(
|
|
265
271
|
entityClass,
|
|
266
272
|
sourceEntity,
|
|
267
|
-
|
|
273
|
+
{ previousValue: null, cascadingDeleteCause: null },
|
|
268
274
|
queryContext,
|
|
269
275
|
);
|
|
270
276
|
}
|
|
@@ -292,7 +298,13 @@ async function canViewerDeleteInternalAsync<
|
|
|
292
298
|
TSelectedFields
|
|
293
299
|
>,
|
|
294
300
|
sourceEntity: TEntity,
|
|
295
|
-
|
|
301
|
+
evaluationContext: EntityPrivacyPolicyEvaluationContext<
|
|
302
|
+
TFields,
|
|
303
|
+
TIDField,
|
|
304
|
+
TViewerContext,
|
|
305
|
+
TEntity,
|
|
306
|
+
TSelectedFields
|
|
307
|
+
>,
|
|
296
308
|
queryContext: EntityQueryContext,
|
|
297
309
|
): Promise<EntityPrivacyEvaluationResult> {
|
|
298
310
|
const viewerContext = sourceEntity.getViewerContext();
|
|
@@ -306,7 +318,7 @@ async function canViewerDeleteInternalAsync<
|
|
|
306
318
|
privacyPolicy.authorizeDeleteAsync(
|
|
307
319
|
sourceEntity.getViewerContext(),
|
|
308
320
|
queryContext,
|
|
309
|
-
|
|
321
|
+
evaluationContext,
|
|
310
322
|
sourceEntity,
|
|
311
323
|
viewerScopedCompanion.getMetricsAdapter(),
|
|
312
324
|
),
|
|
@@ -321,7 +333,7 @@ async function canViewerDeleteInternalAsync<
|
|
|
321
333
|
|
|
322
334
|
const newCascadingDeleteCause = {
|
|
323
335
|
entity: sourceEntity,
|
|
324
|
-
cascadingDeleteCause,
|
|
336
|
+
cascadingDeleteCause: evaluationContext.cascadingDeleteCause,
|
|
325
337
|
};
|
|
326
338
|
|
|
327
339
|
// Take entity X which is proposed to be deleted, look at inbound edges (entities that reference X).
|
|
@@ -366,7 +378,7 @@ async function canViewerDeleteInternalAsync<
|
|
|
366
378
|
const edgeDeletionPermissionInferenceBehavior =
|
|
367
379
|
association.edgeDeletionAuthorizationInferenceBehavior;
|
|
368
380
|
|
|
369
|
-
let entityResultsToCheckForInboundEdge: readonly Result<any
|
|
381
|
+
let entityResultsToCheckForInboundEdge: readonly Result<ReadonlyEntity<any, any, any, any>>[];
|
|
370
382
|
|
|
371
383
|
if (
|
|
372
384
|
edgeDeletionPermissionInferenceBehavior ===
|
|
@@ -419,7 +431,7 @@ async function canViewerDeleteInternalAsync<
|
|
|
419
431
|
canViewerDeleteInternalAsync(
|
|
420
432
|
inboundEdge,
|
|
421
433
|
entity,
|
|
422
|
-
newCascadingDeleteCause,
|
|
434
|
+
{ previousValue: null, cascadingDeleteCause: newCascadingDeleteCause },
|
|
423
435
|
queryContext,
|
|
424
436
|
),
|
|
425
437
|
),
|
|
@@ -435,14 +447,43 @@ async function canViewerDeleteInternalAsync<
|
|
|
435
447
|
|
|
436
448
|
case EntityEdgeDeletionBehavior.SET_NULL:
|
|
437
449
|
case EntityEdgeDeletionBehavior.SET_NULL_INVALIDATE_CACHE_ONLY: {
|
|
450
|
+
// create synthetic entities with the reference field set to null to properly evaluate
|
|
451
|
+
// privacy policy as it would be after the cascading SET NULL operation
|
|
452
|
+
const previousAndSyntheticEntitiesForInboundEdge = entitiesForInboundEdge.map(
|
|
453
|
+
(entity) => {
|
|
454
|
+
const entityLoader = viewerContext
|
|
455
|
+
.getViewerScopedEntityCompanionForClass(inboundEdge)
|
|
456
|
+
.getLoaderFactory()
|
|
457
|
+
.forLoad(queryContext, {
|
|
458
|
+
previousValue: entity,
|
|
459
|
+
cascadingDeleteCause: newCascadingDeleteCause,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const allFields = entity.getAllDatabaseFields();
|
|
463
|
+
const syntheticFields = {
|
|
464
|
+
...allFields,
|
|
465
|
+
[fieldName]: null,
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
previousValue: entity,
|
|
470
|
+
syntheticallyUpdatedValue: entityLoader.utils.constructEntity(syntheticFields),
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
|
|
438
475
|
const canUpdateEvaluationResults = await Promise.all(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
476
|
+
previousAndSyntheticEntitiesForInboundEdge.map(
|
|
477
|
+
({ previousValue, syntheticallyUpdatedValue }) =>
|
|
478
|
+
canViewerUpdateInternalAsync(
|
|
479
|
+
inboundEdge,
|
|
480
|
+
syntheticallyUpdatedValue,
|
|
481
|
+
{
|
|
482
|
+
previousValue,
|
|
483
|
+
cascadingDeleteCause: newCascadingDeleteCause,
|
|
484
|
+
},
|
|
485
|
+
queryContext,
|
|
486
|
+
),
|
|
446
487
|
),
|
|
447
488
|
);
|
|
448
489
|
|
|
@@ -17,8 +17,7 @@ export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvi
|
|
|
17
17
|
export class NoCacheStubCacheAdapter<
|
|
18
18
|
TFields extends Record<string, any>,
|
|
19
19
|
TIDField extends keyof TFields,
|
|
20
|
-
> implements IEntityCacheAdapter<TFields, TIDField>
|
|
21
|
-
{
|
|
20
|
+
> implements IEntityCacheAdapter<TFields, TIDField> {
|
|
22
21
|
public async loadManyAsync<
|
|
23
22
|
TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
|
|
24
23
|
TSerializedLoadValue,
|
|
@@ -74,8 +73,7 @@ export class InMemoryFullCacheStubCacheAdapterProvider implements IEntityCacheAd
|
|
|
74
73
|
export class InMemoryFullCacheStubCacheAdapter<
|
|
75
74
|
TFields extends Record<string, any>,
|
|
76
75
|
TIDField extends keyof TFields,
|
|
77
|
-
> implements IEntityCacheAdapter<TFields, TIDField>
|
|
78
|
-
{
|
|
76
|
+
> implements IEntityCacheAdapter<TFields, TIDField> {
|
|
79
77
|
constructor(
|
|
80
78
|
private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
|
81
79
|
private readonly cache: Map<string, Readonly<TFields> | typeof DOES_NOT_EXIST>,
|