@expo/entity 0.54.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.
Files changed (25) hide show
  1. package/build/src/index.d.ts +4 -0
  2. package/build/src/index.js +4 -0
  3. package/build/src/index.js.map +1 -1
  4. package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.d.ts +10 -0
  5. package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.js +19 -0
  6. package/build/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.js.map +1 -0
  7. package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.d.ts +10 -0
  8. package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.js +19 -0
  9. package/build/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.js.map +1 -0
  10. package/build/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.d.ts +66 -0
  11. package/build/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.js +75 -0
  12. package/build/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.js.map +1 -0
  13. package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.d.ts +12 -0
  14. package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.js +23 -0
  15. package/build/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.js.map +1 -0
  16. package/package.json +3 -3
  17. package/src/index.ts +4 -0
  18. package/src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.ts +47 -0
  19. package/src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.ts +47 -0
  20. package/src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.ts +177 -0
  21. package/src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.ts +46 -0
  22. package/src/rules/__tests__/AllowIfAllSubRulesAllowPrivacyPolicyRule-test.ts +64 -0
  23. package/src/rules/__tests__/AllowIfAnySubRuleAllowsPrivacyPolicyRule-test.ts +64 -0
  24. package/src/rules/__tests__/AllowIfInParentCascadeDeletionPrivacyPolicyRule-test.ts +258 -0
  25. package/src/rules/__tests__/EvaluateIfEntityFieldPredicatePrivacyPolicyRule-test.ts +47 -0
@@ -64,9 +64,13 @@ export * from './internal/SingleFieldHolder';
64
64
  export * from './metrics/EntityMetricsUtils';
65
65
  export * from './metrics/IEntityMetricsAdapter';
66
66
  export * from './metrics/NoOpEntityMetricsAdapter';
67
+ export * from './rules/AllowIfAllSubRulesAllowPrivacyPolicyRule';
68
+ export * from './rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule';
69
+ export * from './rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule';
67
70
  export * from './rules/AlwaysAllowPrivacyPolicyRule';
68
71
  export * from './rules/AlwaysDenyPrivacyPolicyRule';
69
72
  export * from './rules/AlwaysSkipPrivacyPolicyRule';
73
+ export * from './rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule';
70
74
  export * from './rules/PrivacyPolicyRule';
71
75
  export * from './utils/EntityCreationUtils';
72
76
  export * from './utils/EntityPrivacyUtils';
@@ -81,9 +81,13 @@ __exportStar(require("./internal/SingleFieldHolder"), exports);
81
81
  __exportStar(require("./metrics/EntityMetricsUtils"), exports);
82
82
  __exportStar(require("./metrics/IEntityMetricsAdapter"), exports);
83
83
  __exportStar(require("./metrics/NoOpEntityMetricsAdapter"), exports);
84
+ __exportStar(require("./rules/AllowIfAllSubRulesAllowPrivacyPolicyRule"), exports);
85
+ __exportStar(require("./rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule"), exports);
86
+ __exportStar(require("./rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule"), exports);
84
87
  __exportStar(require("./rules/AlwaysAllowPrivacyPolicyRule"), exports);
85
88
  __exportStar(require("./rules/AlwaysDenyPrivacyPolicyRule"), exports);
86
89
  __exportStar(require("./rules/AlwaysSkipPrivacyPolicyRule"), exports);
90
+ __exportStar(require("./rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule"), exports);
87
91
  __exportStar(require("./rules/PrivacyPolicyRule"), exports);
88
92
  __exportStar(require("./utils/EntityCreationUtils"), exports);
89
93
  __exportStar(require("./utils/EntityPrivacyUtils"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA,iCAAiC;AACjC;;;GAGG;;;;;;;;;;;;;;;;AAEH,oFAAkE;AAClE,yEAAuD;AACvD,0EAAwD;AACxD,+DAA6C;AAC7C,iEAA+C;AAC/C,qEAAmD;AACnD,2DAAyC;AACzC,2DAAyC;AACzC,0DAAwC;AACxC,2DAAyC;AACzC,2CAAyB;AACzB,4DAA0C;AAC1C,oDAAkC;AAClC,4DAA0C;AAC1C,wDAAsC;AACtC,kDAAgC;AAChC,0DAAwC;AACxC,kDAAgC;AAChC,0DAAwC;AACxC,iDAA+B;AAC/B,iDAA+B;AAC/B,wDAAsC;AACtC,sDAAoC;AACpC,uDAAqC;AACrC,uEAAqD;AACrD,yEAAuD;AACvD,yDAAuC;AACvC,wDAAsC;AACtC,uDAAqC;AACrC,+DAA6C;AAC7C,+DAA6C;AAC7C,kDAAgC;AAChC,gDAA8B;AAC9B,8DAA4C;AAC5C,gEAA8C;AAC9C,wDAAsC;AACtC,gEAA8C;AAC9C,mEAAiD;AACjD,yDAAuC;AACvC,mDAAiC;AACjC,kDAAgC;AAChC,gEAA8C;AAC9C,wEAAsD;AACtD,oEAAkD;AAClD,qEAAmD;AACnD,mEAAiD;AACjD,sEAAoD;AACpD,uDAAqC;AACrC,wEAAsD;AACtD,oEAAkD;AAClD,+DAA6C;AAC7C,kEAAgD;AAChD,oEAAkD;AAClD,+DAA6C;AAC7C,4EAA0D;AAC1D,kEAAgD;AAChD,wEAAsD;AACtD,oEAAkD;AAClD,+DAA6C;AAC7C,+DAA6C;AAC7C,kEAAgD;AAChD,qEAAmD;AACnD,uEAAqD;AACrD,sEAAoD;AACpD,sEAAoD;AACpD,4DAA0C;AAC1C,8DAA4C;AAC5C,6DAA2C;AAC3C,mFAAiE;AACjE,2DAAyC;AACzC,yEAAuD;AACvD,2DAAyC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA,iCAAiC;AACjC;;;GAGG;;;;;;;;;;;;;;;;AAEH,oFAAkE;AAClE,yEAAuD;AACvD,0EAAwD;AACxD,+DAA6C;AAC7C,iEAA+C;AAC/C,qEAAmD;AACnD,2DAAyC;AACzC,2DAAyC;AACzC,0DAAwC;AACxC,2DAAyC;AACzC,2CAAyB;AACzB,4DAA0C;AAC1C,oDAAkC;AAClC,4DAA0C;AAC1C,wDAAsC;AACtC,kDAAgC;AAChC,0DAAwC;AACxC,kDAAgC;AAChC,0DAAwC;AACxC,iDAA+B;AAC/B,iDAA+B;AAC/B,wDAAsC;AACtC,sDAAoC;AACpC,uDAAqC;AACrC,uEAAqD;AACrD,yEAAuD;AACvD,yDAAuC;AACvC,wDAAsC;AACtC,uDAAqC;AACrC,+DAA6C;AAC7C,+DAA6C;AAC7C,kDAAgC;AAChC,gDAA8B;AAC9B,8DAA4C;AAC5C,gEAA8C;AAC9C,wDAAsC;AACtC,gEAA8C;AAC9C,mEAAiD;AACjD,yDAAuC;AACvC,mDAAiC;AACjC,kDAAgC;AAChC,gEAA8C;AAC9C,wEAAsD;AACtD,oEAAkD;AAClD,qEAAmD;AACnD,mEAAiD;AACjD,sEAAoD;AACpD,uDAAqC;AACrC,wEAAsD;AACtD,oEAAkD;AAClD,+DAA6C;AAC7C,kEAAgD;AAChD,oEAAkD;AAClD,+DAA6C;AAC7C,4EAA0D;AAC1D,kEAAgD;AAChD,wEAAsD;AACtD,oEAAkD;AAClD,+DAA6C;AAC7C,+DAA6C;AAC7C,kEAAgD;AAChD,qEAAmD;AACnD,mFAAiE;AACjE,mFAAiE;AACjE,0FAAwE;AACxE,uEAAqD;AACrD,sEAAoD;AACpD,sEAAoD;AACpD,0FAAwE;AACxE,4DAA0C;AAC1C,8DAA4C;AAC5C,6DAA2C;AAC3C,mFAAiE;AACjE,2DAAyC;AACzC,yEAAuD;AACvD,2DAAyC"}
@@ -0,0 +1,10 @@
1
+ import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
2
+ import { EntityQueryContext } from '../EntityQueryContext';
3
+ import { ReadonlyEntity } from '../ReadonlyEntity';
4
+ import { ViewerContext } from '../ViewerContext';
5
+ import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule';
6
+ export declare class AllowIfAllSubRulesAllowPrivacyPolicyRule<TFields extends object, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>, TSelectedFields extends keyof TFields = keyof TFields> extends PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
7
+ private readonly subRules;
8
+ constructor(subRules: PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>[]);
9
+ evaluateAsync(viewerContext: TViewerContext, queryContext: EntityQueryContext, evaluationContext: EntityPrivacyPolicyEvaluationContext<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>, entity: TEntity): Promise<RuleEvaluationResult>;
10
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AllowIfAllSubRulesAllowPrivacyPolicyRule = void 0;
4
+ const PrivacyPolicyRule_1 = require("./PrivacyPolicyRule");
5
+ class AllowIfAllSubRulesAllowPrivacyPolicyRule extends PrivacyPolicyRule_1.PrivacyPolicyRule {
6
+ subRules;
7
+ constructor(subRules) {
8
+ super();
9
+ this.subRules = subRules;
10
+ }
11
+ async evaluateAsync(viewerContext, queryContext, evaluationContext, entity) {
12
+ const results = await Promise.all(this.subRules.map((subRule) => subRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity)));
13
+ return results.every((result) => result === PrivacyPolicyRule_1.RuleEvaluationResult.ALLOW)
14
+ ? PrivacyPolicyRule_1.RuleEvaluationResult.ALLOW
15
+ : PrivacyPolicyRule_1.RuleEvaluationResult.SKIP;
16
+ }
17
+ }
18
+ exports.AllowIfAllSubRulesAllowPrivacyPolicyRule = AllowIfAllSubRulesAllowPrivacyPolicyRule;
19
+ //# sourceMappingURL=AllowIfAllSubRulesAllowPrivacyPolicyRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AllowIfAllSubRulesAllowPrivacyPolicyRule.js","sourceRoot":"","sources":["../../../src/rules/AllowIfAllSubRulesAllowPrivacyPolicyRule.ts"],"names":[],"mappings":";;;AAIA,2DAA8E;AAE9E,MAAa,wCAMX,SAAQ,qCAA8E;IAEnE;IADnB,YACmB,QAMd;QAEH,KAAK,EAAE,CAAC;QARS,aAAQ,GAAR,QAAQ,CAMtB;IAGL,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,aAA6B,EAC7B,YAAgC,EAChC,iBAMC,EACD,MAAe;QAEf,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAC5B,OAAO,CAAC,aAAa,CAAC,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAC9E,CACF,CAAC;QACF,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,wCAAoB,CAAC,KAAK,CAAC;YACrE,CAAC,CAAC,wCAAoB,CAAC,KAAK;YAC5B,CAAC,CAAC,wCAAoB,CAAC,IAAI,CAAC;IAChC,CAAC;CACF;AAxCD,4FAwCC"}
@@ -0,0 +1,10 @@
1
+ import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
2
+ import { EntityQueryContext } from '../EntityQueryContext';
3
+ import { ReadonlyEntity } from '../ReadonlyEntity';
4
+ import { ViewerContext } from '../ViewerContext';
5
+ import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule';
6
+ export declare class AllowIfAnySubRuleAllowsPrivacyPolicyRule<TFields extends object, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>, TSelectedFields extends keyof TFields = keyof TFields> extends PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
7
+ private readonly subRules;
8
+ constructor(subRules: PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>[]);
9
+ evaluateAsync(viewerContext: TViewerContext, queryContext: EntityQueryContext, evaluationContext: EntityPrivacyPolicyEvaluationContext<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>, entity: TEntity): Promise<RuleEvaluationResult>;
10
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AllowIfAnySubRuleAllowsPrivacyPolicyRule = void 0;
4
+ const PrivacyPolicyRule_1 = require("./PrivacyPolicyRule");
5
+ class AllowIfAnySubRuleAllowsPrivacyPolicyRule extends PrivacyPolicyRule_1.PrivacyPolicyRule {
6
+ subRules;
7
+ constructor(subRules) {
8
+ super();
9
+ this.subRules = subRules;
10
+ }
11
+ async evaluateAsync(viewerContext, queryContext, evaluationContext, entity) {
12
+ const results = await Promise.all(this.subRules.map((subRule) => subRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity)));
13
+ return results.includes(PrivacyPolicyRule_1.RuleEvaluationResult.ALLOW)
14
+ ? PrivacyPolicyRule_1.RuleEvaluationResult.ALLOW
15
+ : PrivacyPolicyRule_1.RuleEvaluationResult.SKIP;
16
+ }
17
+ }
18
+ exports.AllowIfAnySubRuleAllowsPrivacyPolicyRule = AllowIfAnySubRuleAllowsPrivacyPolicyRule;
19
+ //# sourceMappingURL=AllowIfAnySubRuleAllowsPrivacyPolicyRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AllowIfAnySubRuleAllowsPrivacyPolicyRule.js","sourceRoot":"","sources":["../../../src/rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule.ts"],"names":[],"mappings":";;;AAIA,2DAA8E;AAE9E,MAAa,wCAMX,SAAQ,qCAA8E;IAEnE;IADnB,YACmB,QAMd;QAEH,KAAK,EAAE,CAAC;QARS,aAAQ,GAAR,QAAQ,CAMtB;IAGL,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,aAA6B,EAC7B,YAAgC,EAChC,iBAMC,EACD,MAAe;QAEf,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAC5B,OAAO,CAAC,aAAa,CAAC,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,CAAC,CAC9E,CACF,CAAC;QACF,OAAO,OAAO,CAAC,QAAQ,CAAC,wCAAoB,CAAC,KAAK,CAAC;YACjD,CAAC,CAAC,wCAAoB,CAAC,KAAK;YAC5B,CAAC,CAAC,wCAAoB,CAAC,IAAI,CAAC;IAChC,CAAC;CACF;AAxCD,4FAwCC"}
@@ -0,0 +1,66 @@
1
+ import { IEntityClass } from '../Entity';
2
+ import { EntityPrivacyPolicy, EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
3
+ import { EntityQueryContext } from '../EntityQueryContext';
4
+ import { ReadonlyEntity } from '../ReadonlyEntity';
5
+ import { ViewerContext } from '../ViewerContext';
6
+ import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule';
7
+ /**
8
+ * Directive for specifying the parent relationship in AllowIfInParentCascadeDeletionPrivacyPolicyRule.
9
+ */
10
+ export interface AllowIfInParentCascadeDeletionDirective<TViewerContext extends ViewerContext, TFields, TParentFields extends object, TParentIDField extends keyof NonNullable<Pick<TParentFields, TParentSelectedFields>>, TParentEntity extends ReadonlyEntity<TParentFields, TParentIDField, TViewerContext, TParentSelectedFields>, TParentPrivacyPolicy extends EntityPrivacyPolicy<TParentFields, TParentIDField, TViewerContext, TParentEntity, TParentSelectedFields>, TSelectedFields extends keyof TFields = keyof TFields, TParentSelectedFields extends keyof TParentFields = keyof TParentFields> {
11
+ /**
12
+ * Class of parent entity that should trigger a cascade set null update to a field within
13
+ * the entity being authorized.
14
+ */
15
+ parentEntityClass: IEntityClass<TParentFields, TParentIDField, TViewerContext, TParentEntity, TParentPrivacyPolicy, TParentSelectedFields>;
16
+ /**
17
+ * Field of the current entity with references the deleting instace of parentEntityClass.
18
+ */
19
+ fieldIdentifyingParentEntity: keyof Pick<TFields, TSelectedFields>;
20
+ /**
21
+ * Field in parentEntityClass referenced by the value of fieldIdentifyingParentEntity.
22
+ * If not provided, ID is assumed.
23
+ */
24
+ parentEntityLookupByField?: keyof Pick<TParentFields, TParentSelectedFields>;
25
+ }
26
+ /**
27
+ * A generic privacy policy rule that allows when an entity is being authorized
28
+ * as part of a cascading delete from a parent entity. Handles two cases:
29
+ * - When the field has not yet been null'ed out due to a cascading set null. This is often
30
+ * required for read rules to authorize the initial re-read of the entity being update set null'ed.
31
+ * - When the field has been null'ed out due to a cascading set null. This is often required
32
+ * the update rules for the field nullification.
33
+ *
34
+ * These two cases could theoretically be handled by two separate (stricter) rules, but are combined
35
+ * to simplify configuration since practically there are few cases where having them be combined would
36
+ * preset an issue.
37
+ *
38
+ * @example
39
+ * Billing info owned by an account, but records who created the billing info in creating_user_id. User is a member of that account.
40
+ * User can delete themselves, and the billing info's creating_user_id field is cascade set null'ed when the user is deleted.
41
+ *
42
+ * ```ts
43
+ * class BillingInfoEntityPrivacyPolicy extends EntityPrivacyPolicy<...> {
44
+ * protected override readonly readRules = [
45
+ * ...,
46
+ * new AllowIfInParentCascadeDeletionPrivacyPolicyRule<...>({
47
+ * fieldIdentifyingParentEntity: 'creating_user_id',
48
+ * parentEntityClass: UserEntity,
49
+ * }),
50
+ * ];
51
+ *
52
+ * protected override readonly updateRules = [
53
+ * ...,
54
+ * new AllowIfInParentCascadeDeletionPrivacyPolicyRule<...>({
55
+ * fieldIdentifyingParentEntity: 'creating_user_id',
56
+ * parentEntityClass: UserEntity,
57
+ * }),
58
+ * ];
59
+ * }
60
+ * ```
61
+ */
62
+ export declare class AllowIfInParentCascadeDeletionPrivacyPolicyRule<TFields extends object, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>, TFields2 extends object, TIDField2 extends keyof NonNullable<Pick<TFields2, TSelectedFields2>>, TEntity2 extends ReadonlyEntity<TFields2, TIDField2, TViewerContext, TSelectedFields2>, TPrivacyPolicy2 extends EntityPrivacyPolicy<TFields2, TIDField2, TViewerContext, TEntity2, TSelectedFields2>, TSelectedFields extends keyof TFields = keyof TFields, TSelectedFields2 extends keyof TFields2 = keyof TFields2> extends PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
63
+ private readonly directive;
64
+ constructor(directive: AllowIfInParentCascadeDeletionDirective<TViewerContext, TFields, TFields2, TIDField2, TEntity2, TPrivacyPolicy2, TSelectedFields, TSelectedFields2>);
65
+ evaluateAsync(_viewerContext: TViewerContext, _queryContext: EntityQueryContext, evaluationContext: EntityPrivacyPolicyEvaluationContext<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>, entity: TEntity): Promise<RuleEvaluationResult>;
66
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AllowIfInParentCascadeDeletionPrivacyPolicyRule = void 0;
4
+ const PrivacyPolicyRule_1 = require("./PrivacyPolicyRule");
5
+ /**
6
+ * A generic privacy policy rule that allows when an entity is being authorized
7
+ * as part of a cascading delete from a parent entity. Handles two cases:
8
+ * - When the field has not yet been null'ed out due to a cascading set null. This is often
9
+ * required for read rules to authorize the initial re-read of the entity being update set null'ed.
10
+ * - When the field has been null'ed out due to a cascading set null. This is often required
11
+ * the update rules for the field nullification.
12
+ *
13
+ * These two cases could theoretically be handled by two separate (stricter) rules, but are combined
14
+ * to simplify configuration since practically there are few cases where having them be combined would
15
+ * preset an issue.
16
+ *
17
+ * @example
18
+ * Billing info owned by an account, but records who created the billing info in creating_user_id. User is a member of that account.
19
+ * User can delete themselves, and the billing info's creating_user_id field is cascade set null'ed when the user is deleted.
20
+ *
21
+ * ```ts
22
+ * class BillingInfoEntityPrivacyPolicy extends EntityPrivacyPolicy<...> {
23
+ * protected override readonly readRules = [
24
+ * ...,
25
+ * new AllowIfInParentCascadeDeletionPrivacyPolicyRule<...>({
26
+ * fieldIdentifyingParentEntity: 'creating_user_id',
27
+ * parentEntityClass: UserEntity,
28
+ * }),
29
+ * ];
30
+ *
31
+ * protected override readonly updateRules = [
32
+ * ...,
33
+ * new AllowIfInParentCascadeDeletionPrivacyPolicyRule<...>({
34
+ * fieldIdentifyingParentEntity: 'creating_user_id',
35
+ * parentEntityClass: UserEntity,
36
+ * }),
37
+ * ];
38
+ * }
39
+ * ```
40
+ */
41
+ class AllowIfInParentCascadeDeletionPrivacyPolicyRule extends PrivacyPolicyRule_1.PrivacyPolicyRule {
42
+ directive;
43
+ constructor(directive) {
44
+ super();
45
+ this.directive = directive;
46
+ }
47
+ async evaluateAsync(_viewerContext, _queryContext, evaluationContext, entity) {
48
+ const parentEntityClass = this.directive.parentEntityClass;
49
+ const deleteCause = evaluationContext.cascadingDeleteCause;
50
+ if (!deleteCause || !(deleteCause.entity instanceof parentEntityClass)) {
51
+ return PrivacyPolicyRule_1.RuleEvaluationResult.SKIP;
52
+ }
53
+ const entityBeingDeleted = deleteCause.entity;
54
+ // allow if parent foreign key field matches specified field in the entity being authorized
55
+ const valueInThisEntityReferencingParent = entity.getField(this.directive.fieldIdentifyingParentEntity);
56
+ const valueInParent = this.directive.parentEntityLookupByField
57
+ ? entityBeingDeleted.getField(this.directive.parentEntityLookupByField)
58
+ : entityBeingDeleted.getID();
59
+ if (valueInThisEntityReferencingParent &&
60
+ valueInThisEntityReferencingParent === valueInParent) {
61
+ return PrivacyPolicyRule_1.RuleEvaluationResult.ALLOW;
62
+ }
63
+ // allow if parent foreign key field matches specified field in the entity being authorized, and the
64
+ // field in the entity being authorized has been null'ed out due to cascading set null
65
+ const valueInPreviousValueOfThisEntityReferencingParent = evaluationContext.previousValue?.getField(this.directive.fieldIdentifyingParentEntity);
66
+ if (valueInPreviousValueOfThisEntityReferencingParent &&
67
+ valueInPreviousValueOfThisEntityReferencingParent === valueInParent &&
68
+ valueInThisEntityReferencingParent === null) {
69
+ return PrivacyPolicyRule_1.RuleEvaluationResult.ALLOW;
70
+ }
71
+ return PrivacyPolicyRule_1.RuleEvaluationResult.SKIP;
72
+ }
73
+ }
74
+ exports.AllowIfInParentCascadeDeletionPrivacyPolicyRule = AllowIfInParentCascadeDeletionPrivacyPolicyRule;
75
+ //# sourceMappingURL=AllowIfInParentCascadeDeletionPrivacyPolicyRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AllowIfInParentCascadeDeletionPrivacyPolicyRule.js","sourceRoot":"","sources":["../../../src/rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule.ts"],"names":[],"mappings":";;;AAKA,2DAA8E;AAmD9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAa,+CAiBX,SAAQ,qCAA8E;IAEnE;IADnB,YACmB,SAShB;QAED,KAAK,EAAE,CAAC;QAXS,cAAS,GAAT,SAAS,CASzB;IAGH,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,cAA8B,EAC9B,aAAiC,EACjC,iBAMC,EACD,MAAe;QAEf,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC;QAE3D,MAAM,WAAW,GAAG,iBAAiB,CAAC,oBAAoB,CAAC;QAC3D,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,WAAW,CAAC,MAAM,YAAY,iBAAiB,CAAC,EAAE,CAAC;YACvE,OAAO,wCAAoB,CAAC,IAAI,CAAC;QACnC,CAAC;QAED,MAAM,kBAAkB,GAAG,WAAW,CAAC,MAAM,CAAC;QAE9C,2FAA2F;QAC3F,MAAM,kCAAkC,GAAG,MAAM,CAAC,QAAQ,CACxD,IAAI,CAAC,SAAS,CAAC,4BAA4B,CAC5C,CAAC;QACF,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC,yBAAyB;YAC5D,CAAC,CAAC,kBAAkB,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,yBAAyB,CAAC;YACvE,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,CAAC;QAE/B,IACE,kCAAkC;YAClC,kCAAkC,KAAK,aAAa,EACpD,CAAC;YACD,OAAO,wCAAoB,CAAC,KAAK,CAAC;QACpC,CAAC;QAED,oGAAoG;QACpG,uFAAuF;QACvF,MAAM,iDAAiD,GACrD,iBAAiB,CAAC,aAAa,EAAE,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,4BAA4B,CAAC,CAAC;QAEzF,IACE,iDAAiD;YACjD,iDAAiD,KAAK,aAAa;YACnE,kCAAkC,KAAK,IAAI,EAC3C,CAAC;YACD,OAAO,wCAAoB,CAAC,KAAK,CAAC;QACpC,CAAC;QAED,OAAO,wCAAoB,CAAC,IAAI,CAAC;IACnC,CAAC;CACF;AApFD,0GAoFC"}
@@ -0,0 +1,12 @@
1
+ import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
2
+ import { EntityQueryContext } from '../EntityQueryContext';
3
+ import { ReadonlyEntity } from '../ReadonlyEntity';
4
+ import { ViewerContext } from '../ViewerContext';
5
+ import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule';
6
+ export declare class EvaluateIfEntityFieldPredicatePrivacyPolicyRule<TFields extends object, TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>, TViewerContext extends ViewerContext, TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>, N extends TSelectedFields, TSelectedFields extends keyof TFields = keyof TFields> extends PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
7
+ private readonly fieldName;
8
+ private readonly shouldEvaluatePredicate;
9
+ private readonly rule;
10
+ constructor(fieldName: N, shouldEvaluatePredicate: (fieldValue: TFields[N]) => boolean, rule: PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>);
11
+ evaluateAsync(viewerContext: TViewerContext, queryContext: EntityQueryContext, evaluationContext: EntityPrivacyPolicyEvaluationContext<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>, entity: TEntity): Promise<RuleEvaluationResult>;
12
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EvaluateIfEntityFieldPredicatePrivacyPolicyRule = void 0;
4
+ const PrivacyPolicyRule_1 = require("./PrivacyPolicyRule");
5
+ class EvaluateIfEntityFieldPredicatePrivacyPolicyRule extends PrivacyPolicyRule_1.PrivacyPolicyRule {
6
+ fieldName;
7
+ shouldEvaluatePredicate;
8
+ rule;
9
+ constructor(fieldName, shouldEvaluatePredicate, rule) {
10
+ super();
11
+ this.fieldName = fieldName;
12
+ this.shouldEvaluatePredicate = shouldEvaluatePredicate;
13
+ this.rule = rule;
14
+ }
15
+ async evaluateAsync(viewerContext, queryContext, evaluationContext, entity) {
16
+ const fieldValue = entity.getField(this.fieldName);
17
+ return this.shouldEvaluatePredicate(fieldValue)
18
+ ? await this.rule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity)
19
+ : PrivacyPolicyRule_1.RuleEvaluationResult.SKIP;
20
+ }
21
+ }
22
+ exports.EvaluateIfEntityFieldPredicatePrivacyPolicyRule = EvaluateIfEntityFieldPredicatePrivacyPolicyRule;
23
+ //# sourceMappingURL=EvaluateIfEntityFieldPredicatePrivacyPolicyRule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EvaluateIfEntityFieldPredicatePrivacyPolicyRule.js","sourceRoot":"","sources":["../../../src/rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule.ts"],"names":[],"mappings":";;;AAIA,2DAA8E;AAE9E,MAAa,+CAOX,SAAQ,qCAA8E;IAEnE;IACA;IACA;IAHnB,YACmB,SAAY,EACZ,uBAA4D,EAC5D,IAMhB;QAED,KAAK,EAAE,CAAC;QAVS,cAAS,GAAT,SAAS,CAAG;QACZ,4BAAuB,GAAvB,uBAAuB,CAAqC;QAC5D,SAAI,GAAJ,IAAI,CAMpB;IAGH,CAAC;IAED,KAAK,CAAC,aAAa,CACjB,aAA6B,EAC7B,YAAgC,EAChC,iBAMC,EACD,MAAe;QAEf,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC,uBAAuB,CAAC,UAAU,CAAC;YAC7C,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,CAAC;YACvF,CAAC,CAAC,wCAAoB,CAAC,IAAI,CAAC;IAChC,CAAC;CACF;AAvCD,0GAuCC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/entity",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "description": "A privacy-first data model",
5
5
  "files": [
6
6
  "build",
@@ -36,10 +36,10 @@
36
36
  "@types/invariant": "2.2.37",
37
37
  "@types/lodash": "4.17.23",
38
38
  "@types/node": "24.10.9",
39
- "lodash": "4.17.21",
39
+ "lodash": "4.17.23",
40
40
  "ts-mockito": "2.6.1",
41
41
  "typescript": "5.9.3",
42
42
  "uuid": "13.0.0"
43
43
  },
44
- "gitHead": "283c02eedbd4f5721afad96f31ada85bf92afb41"
44
+ "gitHead": "de7548680dbffb722fb6f5c92106392319782fef"
45
45
  }
package/src/index.ts CHANGED
@@ -66,9 +66,13 @@ export * from './internal/SingleFieldHolder';
66
66
  export * from './metrics/EntityMetricsUtils';
67
67
  export * from './metrics/IEntityMetricsAdapter';
68
68
  export * from './metrics/NoOpEntityMetricsAdapter';
69
+ export * from './rules/AllowIfAllSubRulesAllowPrivacyPolicyRule';
70
+ export * from './rules/AllowIfAnySubRuleAllowsPrivacyPolicyRule';
71
+ export * from './rules/AllowIfInParentCascadeDeletionPrivacyPolicyRule';
69
72
  export * from './rules/AlwaysAllowPrivacyPolicyRule';
70
73
  export * from './rules/AlwaysDenyPrivacyPolicyRule';
71
74
  export * from './rules/AlwaysSkipPrivacyPolicyRule';
75
+ export * from './rules/EvaluateIfEntityFieldPredicatePrivacyPolicyRule';
72
76
  export * from './rules/PrivacyPolicyRule';
73
77
  export * from './utils/EntityCreationUtils';
74
78
  export * from './utils/EntityPrivacyUtils';
@@ -0,0 +1,47 @@
1
+ import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
2
+ import { EntityQueryContext } from '../EntityQueryContext';
3
+ import { ReadonlyEntity } from '../ReadonlyEntity';
4
+ import { ViewerContext } from '../ViewerContext';
5
+ import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule';
6
+
7
+ export class AllowIfAllSubRulesAllowPrivacyPolicyRule<
8
+ TFields extends object,
9
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
10
+ TViewerContext extends ViewerContext,
11
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
12
+ TSelectedFields extends keyof TFields = keyof TFields,
13
+ > extends PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
14
+ constructor(
15
+ private readonly subRules: PrivacyPolicyRule<
16
+ TFields,
17
+ TIDField,
18
+ TViewerContext,
19
+ TEntity,
20
+ TSelectedFields
21
+ >[],
22
+ ) {
23
+ super();
24
+ }
25
+
26
+ async evaluateAsync(
27
+ viewerContext: TViewerContext,
28
+ queryContext: EntityQueryContext,
29
+ evaluationContext: EntityPrivacyPolicyEvaluationContext<
30
+ TFields,
31
+ TIDField,
32
+ TViewerContext,
33
+ TEntity,
34
+ TSelectedFields
35
+ >,
36
+ entity: TEntity,
37
+ ): Promise<RuleEvaluationResult> {
38
+ const results = await Promise.all(
39
+ this.subRules.map((subRule) =>
40
+ subRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
41
+ ),
42
+ );
43
+ return results.every((result) => result === RuleEvaluationResult.ALLOW)
44
+ ? RuleEvaluationResult.ALLOW
45
+ : RuleEvaluationResult.SKIP;
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
2
+ import { EntityQueryContext } from '../EntityQueryContext';
3
+ import { ReadonlyEntity } from '../ReadonlyEntity';
4
+ import { ViewerContext } from '../ViewerContext';
5
+ import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule';
6
+
7
+ export class AllowIfAnySubRuleAllowsPrivacyPolicyRule<
8
+ TFields extends object,
9
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
10
+ TViewerContext extends ViewerContext,
11
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
12
+ TSelectedFields extends keyof TFields = keyof TFields,
13
+ > extends PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
14
+ constructor(
15
+ private readonly subRules: PrivacyPolicyRule<
16
+ TFields,
17
+ TIDField,
18
+ TViewerContext,
19
+ TEntity,
20
+ TSelectedFields
21
+ >[],
22
+ ) {
23
+ super();
24
+ }
25
+
26
+ async evaluateAsync(
27
+ viewerContext: TViewerContext,
28
+ queryContext: EntityQueryContext,
29
+ evaluationContext: EntityPrivacyPolicyEvaluationContext<
30
+ TFields,
31
+ TIDField,
32
+ TViewerContext,
33
+ TEntity,
34
+ TSelectedFields
35
+ >,
36
+ entity: TEntity,
37
+ ): Promise<RuleEvaluationResult> {
38
+ const results = await Promise.all(
39
+ this.subRules.map((subRule) =>
40
+ subRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
41
+ ),
42
+ );
43
+ return results.includes(RuleEvaluationResult.ALLOW)
44
+ ? RuleEvaluationResult.ALLOW
45
+ : RuleEvaluationResult.SKIP;
46
+ }
47
+ }
@@ -0,0 +1,177 @@
1
+ import { IEntityClass } from '../Entity';
2
+ import { EntityPrivacyPolicy, EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
3
+ import { EntityQueryContext } from '../EntityQueryContext';
4
+ import { ReadonlyEntity } from '../ReadonlyEntity';
5
+ import { ViewerContext } from '../ViewerContext';
6
+ import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule';
7
+
8
+ /**
9
+ * Directive for specifying the parent relationship in AllowIfInParentCascadeDeletionPrivacyPolicyRule.
10
+ */
11
+ export interface AllowIfInParentCascadeDeletionDirective<
12
+ TViewerContext extends ViewerContext,
13
+ TFields,
14
+ TParentFields extends object,
15
+ TParentIDField extends keyof NonNullable<Pick<TParentFields, TParentSelectedFields>>,
16
+ TParentEntity extends ReadonlyEntity<
17
+ TParentFields,
18
+ TParentIDField,
19
+ TViewerContext,
20
+ TParentSelectedFields
21
+ >,
22
+ TParentPrivacyPolicy extends EntityPrivacyPolicy<
23
+ TParentFields,
24
+ TParentIDField,
25
+ TViewerContext,
26
+ TParentEntity,
27
+ TParentSelectedFields
28
+ >,
29
+ TSelectedFields extends keyof TFields = keyof TFields,
30
+ TParentSelectedFields extends keyof TParentFields = keyof TParentFields,
31
+ > {
32
+ /**
33
+ * Class of parent entity that should trigger a cascade set null update to a field within
34
+ * the entity being authorized.
35
+ */
36
+ parentEntityClass: IEntityClass<
37
+ TParentFields,
38
+ TParentIDField,
39
+ TViewerContext,
40
+ TParentEntity,
41
+ TParentPrivacyPolicy,
42
+ TParentSelectedFields
43
+ >;
44
+
45
+ /**
46
+ * Field of the current entity with references the deleting instace of parentEntityClass.
47
+ */
48
+ fieldIdentifyingParentEntity: keyof Pick<TFields, TSelectedFields>;
49
+
50
+ /**
51
+ * Field in parentEntityClass referenced by the value of fieldIdentifyingParentEntity.
52
+ * If not provided, ID is assumed.
53
+ */
54
+ parentEntityLookupByField?: keyof Pick<TParentFields, TParentSelectedFields>;
55
+ }
56
+
57
+ /**
58
+ * A generic privacy policy rule that allows when an entity is being authorized
59
+ * as part of a cascading delete from a parent entity. Handles two cases:
60
+ * - When the field has not yet been null'ed out due to a cascading set null. This is often
61
+ * required for read rules to authorize the initial re-read of the entity being update set null'ed.
62
+ * - When the field has been null'ed out due to a cascading set null. This is often required
63
+ * the update rules for the field nullification.
64
+ *
65
+ * These two cases could theoretically be handled by two separate (stricter) rules, but are combined
66
+ * to simplify configuration since practically there are few cases where having them be combined would
67
+ * preset an issue.
68
+ *
69
+ * @example
70
+ * Billing info owned by an account, but records who created the billing info in creating_user_id. User is a member of that account.
71
+ * User can delete themselves, and the billing info's creating_user_id field is cascade set null'ed when the user is deleted.
72
+ *
73
+ * ```ts
74
+ * class BillingInfoEntityPrivacyPolicy extends EntityPrivacyPolicy<...> {
75
+ * protected override readonly readRules = [
76
+ * ...,
77
+ * new AllowIfInParentCascadeDeletionPrivacyPolicyRule<...>({
78
+ * fieldIdentifyingParentEntity: 'creating_user_id',
79
+ * parentEntityClass: UserEntity,
80
+ * }),
81
+ * ];
82
+ *
83
+ * protected override readonly updateRules = [
84
+ * ...,
85
+ * new AllowIfInParentCascadeDeletionPrivacyPolicyRule<...>({
86
+ * fieldIdentifyingParentEntity: 'creating_user_id',
87
+ * parentEntityClass: UserEntity,
88
+ * }),
89
+ * ];
90
+ * }
91
+ * ```
92
+ */
93
+ export class AllowIfInParentCascadeDeletionPrivacyPolicyRule<
94
+ TFields extends object,
95
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
96
+ TViewerContext extends ViewerContext,
97
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
98
+ TFields2 extends object,
99
+ TIDField2 extends keyof NonNullable<Pick<TFields2, TSelectedFields2>>,
100
+ TEntity2 extends ReadonlyEntity<TFields2, TIDField2, TViewerContext, TSelectedFields2>,
101
+ TPrivacyPolicy2 extends EntityPrivacyPolicy<
102
+ TFields2,
103
+ TIDField2,
104
+ TViewerContext,
105
+ TEntity2,
106
+ TSelectedFields2
107
+ >,
108
+ TSelectedFields extends keyof TFields = keyof TFields,
109
+ TSelectedFields2 extends keyof TFields2 = keyof TFields2,
110
+ > extends PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
111
+ constructor(
112
+ private readonly directive: AllowIfInParentCascadeDeletionDirective<
113
+ TViewerContext,
114
+ TFields,
115
+ TFields2,
116
+ TIDField2,
117
+ TEntity2,
118
+ TPrivacyPolicy2,
119
+ TSelectedFields,
120
+ TSelectedFields2
121
+ >,
122
+ ) {
123
+ super();
124
+ }
125
+
126
+ async evaluateAsync(
127
+ _viewerContext: TViewerContext,
128
+ _queryContext: EntityQueryContext,
129
+ evaluationContext: EntityPrivacyPolicyEvaluationContext<
130
+ TFields,
131
+ TIDField,
132
+ TViewerContext,
133
+ TEntity,
134
+ TSelectedFields
135
+ >,
136
+ entity: TEntity,
137
+ ): Promise<RuleEvaluationResult> {
138
+ const parentEntityClass = this.directive.parentEntityClass;
139
+
140
+ const deleteCause = evaluationContext.cascadingDeleteCause;
141
+ if (!deleteCause || !(deleteCause.entity instanceof parentEntityClass)) {
142
+ return RuleEvaluationResult.SKIP;
143
+ }
144
+
145
+ const entityBeingDeleted = deleteCause.entity;
146
+
147
+ // allow if parent foreign key field matches specified field in the entity being authorized
148
+ const valueInThisEntityReferencingParent = entity.getField(
149
+ this.directive.fieldIdentifyingParentEntity,
150
+ );
151
+ const valueInParent = this.directive.parentEntityLookupByField
152
+ ? entityBeingDeleted.getField(this.directive.parentEntityLookupByField)
153
+ : entityBeingDeleted.getID();
154
+
155
+ if (
156
+ valueInThisEntityReferencingParent &&
157
+ valueInThisEntityReferencingParent === valueInParent
158
+ ) {
159
+ return RuleEvaluationResult.ALLOW;
160
+ }
161
+
162
+ // allow if parent foreign key field matches specified field in the entity being authorized, and the
163
+ // field in the entity being authorized has been null'ed out due to cascading set null
164
+ const valueInPreviousValueOfThisEntityReferencingParent =
165
+ evaluationContext.previousValue?.getField(this.directive.fieldIdentifyingParentEntity);
166
+
167
+ if (
168
+ valueInPreviousValueOfThisEntityReferencingParent &&
169
+ valueInPreviousValueOfThisEntityReferencingParent === valueInParent &&
170
+ valueInThisEntityReferencingParent === null
171
+ ) {
172
+ return RuleEvaluationResult.ALLOW;
173
+ }
174
+
175
+ return RuleEvaluationResult.SKIP;
176
+ }
177
+ }
@@ -0,0 +1,46 @@
1
+ import { EntityPrivacyPolicyEvaluationContext } from '../EntityPrivacyPolicy';
2
+ import { EntityQueryContext } from '../EntityQueryContext';
3
+ import { ReadonlyEntity } from '../ReadonlyEntity';
4
+ import { ViewerContext } from '../ViewerContext';
5
+ import { PrivacyPolicyRule, RuleEvaluationResult } from './PrivacyPolicyRule';
6
+
7
+ export class EvaluateIfEntityFieldPredicatePrivacyPolicyRule<
8
+ TFields extends object,
9
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
10
+ TViewerContext extends ViewerContext,
11
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
12
+ N extends TSelectedFields,
13
+ TSelectedFields extends keyof TFields = keyof TFields,
14
+ > extends PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> {
15
+ constructor(
16
+ private readonly fieldName: N,
17
+ private readonly shouldEvaluatePredicate: (fieldValue: TFields[N]) => boolean,
18
+ private readonly rule: PrivacyPolicyRule<
19
+ TFields,
20
+ TIDField,
21
+ TViewerContext,
22
+ TEntity,
23
+ TSelectedFields
24
+ >,
25
+ ) {
26
+ super();
27
+ }
28
+
29
+ async evaluateAsync(
30
+ viewerContext: TViewerContext,
31
+ queryContext: EntityQueryContext,
32
+ evaluationContext: EntityPrivacyPolicyEvaluationContext<
33
+ TFields,
34
+ TIDField,
35
+ TViewerContext,
36
+ TEntity,
37
+ TSelectedFields
38
+ >,
39
+ entity: TEntity,
40
+ ): Promise<RuleEvaluationResult> {
41
+ const fieldValue = entity.getField(this.fieldName);
42
+ return this.shouldEvaluatePredicate(fieldValue)
43
+ ? await this.rule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity)
44
+ : RuleEvaluationResult.SKIP;
45
+ }
46
+ }
@@ -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
+ );