@frontegg/entitlements-javascript-commons 1.0.1 → 1.1.0-alpha.2

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 (93) hide show
  1. package/dist/plans/index.d.ts +2 -0
  2. package/dist/plans/index.js +18 -0
  3. package/dist/plans/index.js.map +1 -0
  4. package/dist/plans/plan.evaluator.d.ts +2 -0
  5. package/dist/plans/plan.evaluator.js +21 -0
  6. package/dist/plans/plan.evaluator.js.map +1 -0
  7. package/dist/plans/types.d.ts +8 -0
  8. package/dist/plans/types.js +3 -0
  9. package/dist/plans/types.js.map +1 -0
  10. package/dist/user-entitlements/index.d.ts +3 -3
  11. package/dist/user-entitlements/index.js +3 -3
  12. package/dist/user-entitlements/index.js.map +1 -1
  13. package/dist/user-entitlements/is-entitled-to-feature/evaluators/direct-entitlement.evaluator.d.ts +2 -0
  14. package/dist/user-entitlements/is-entitled-to-feature/evaluators/direct-entitlement.evaluator.js +20 -0
  15. package/dist/user-entitlements/is-entitled-to-feature/evaluators/direct-entitlement.evaluator.js.map +1 -0
  16. package/dist/user-entitlements/is-entitled-to-feature/evaluators/feature-flag.evaluator.d.ts +2 -0
  17. package/dist/user-entitlements/is-entitled-to-feature/evaluators/feature-flag.evaluator.js +23 -0
  18. package/dist/user-entitlements/is-entitled-to-feature/evaluators/feature-flag.evaluator.js.map +1 -0
  19. package/dist/user-entitlements/is-entitled-to-feature/evaluators/index.d.ts +2 -0
  20. package/dist/user-entitlements/is-entitled-to-feature/evaluators/index.js +11 -0
  21. package/dist/user-entitlements/is-entitled-to-feature/evaluators/index.js.map +1 -0
  22. package/dist/user-entitlements/is-entitled-to-feature/evaluators/plan-targeting-rules.evaluator.d.ts +2 -0
  23. package/dist/user-entitlements/is-entitled-to-feature/evaluators/plan-targeting-rules.evaluator.js +29 -0
  24. package/dist/user-entitlements/is-entitled-to-feature/evaluators/plan-targeting-rules.evaluator.js.map +1 -0
  25. package/dist/user-entitlements/is-entitled-to-feature/evaluators/types.d.ts +2 -0
  26. package/dist/user-entitlements/is-entitled-to-feature/evaluators/types.js +3 -0
  27. package/dist/user-entitlements/is-entitled-to-feature/evaluators/types.js.map +1 -0
  28. package/dist/user-entitlements/is-entitled-to-feature/index.d.ts +1 -0
  29. package/dist/user-entitlements/is-entitled-to-feature/index.js +18 -0
  30. package/dist/user-entitlements/is-entitled-to-feature/index.js.map +1 -0
  31. package/dist/user-entitlements/{is-entitled.evaluator.d.ts → is-entitled-to-feature/is-entitled-to-feature.evaluator.d.ts} +1 -2
  32. package/dist/user-entitlements/is-entitled-to-feature/is-entitled-to-feature.evaluator.js +17 -0
  33. package/dist/user-entitlements/is-entitled-to-feature/is-entitled-to-feature.evaluator.js.map +1 -0
  34. package/dist/user-entitlements/is-entitled-to-permission/index.d.ts +1 -0
  35. package/dist/user-entitlements/is-entitled-to-permission/index.js +18 -0
  36. package/dist/user-entitlements/is-entitled-to-permission/index.js.map +1 -0
  37. package/dist/user-entitlements/is-entitled-to-permission/is-entitled-to-permission.evaluator.d.ts +2 -0
  38. package/dist/user-entitlements/is-entitled-to-permission/is-entitled-to-permission.evaluator.js +30 -0
  39. package/dist/user-entitlements/is-entitled-to-permission/is-entitled-to-permission.evaluator.js.map +1 -0
  40. package/dist/user-entitlements/types.d.ts +7 -2
  41. package/dist/user-entitlements/types.js.map +1 -1
  42. package/dist/user-entitlements/{attributes.utils.d.ts → utils/attributes.utils.d.ts} +1 -1
  43. package/dist/user-entitlements/utils/attributes.utils.js.map +1 -0
  44. package/dist/user-entitlements/utils/entitlement-results.utils.d.ts +3 -0
  45. package/dist/user-entitlements/utils/entitlement-results.utils.js +25 -0
  46. package/dist/user-entitlements/utils/entitlement-results.utils.js.map +1 -0
  47. package/dist/user-entitlements/utils/flatten.utils.js.map +1 -0
  48. package/dist/user-entitlements/utils/index.d.ts +3 -0
  49. package/dist/user-entitlements/utils/index.js +20 -0
  50. package/dist/user-entitlements/utils/index.js.map +1 -0
  51. package/dist/user-entitlements/{permissions.utils.d.ts → utils/permissions.utils.d.ts} +1 -1
  52. package/dist/user-entitlements/utils/permissions.utils.js.map +1 -0
  53. package/docs/CHANGELOG.md +14 -0
  54. package/package.json +1 -1
  55. package/src/plans/index.ts +2 -0
  56. package/src/plans/plan.evaluator.ts +20 -0
  57. package/src/plans/tests/plans.evaluator.spec.ts +121 -0
  58. package/src/plans/types.ts +11 -0
  59. package/src/user-entitlements/index.ts +3 -3
  60. package/src/user-entitlements/is-entitled-to-feature/evaluators/direct-entitlement.evaluator.ts +28 -0
  61. package/src/user-entitlements/is-entitled-to-feature/evaluators/feature-flag.evaluator.ts +24 -0
  62. package/src/user-entitlements/is-entitled-to-feature/evaluators/index.ts +8 -0
  63. package/src/user-entitlements/is-entitled-to-feature/evaluators/plan-targeting-rules.evaluator.ts +30 -0
  64. package/src/user-entitlements/is-entitled-to-feature/evaluators/tests/direct-entitlements.evaluator.spec.ts +81 -0
  65. package/src/user-entitlements/is-entitled-to-feature/evaluators/tests/feature-flag.evaluator.spec.ts +57 -0
  66. package/src/user-entitlements/is-entitled-to-feature/evaluators/tests/plan-targeting-rules.evaluator.spec.ts +70 -0
  67. package/src/user-entitlements/is-entitled-to-feature/evaluators/types.ts +7 -0
  68. package/src/user-entitlements/is-entitled-to-feature/index.ts +1 -0
  69. package/src/user-entitlements/is-entitled-to-feature/is-entitled-to-feature.evaluator.ts +18 -0
  70. package/src/user-entitlements/is-entitled-to-feature/tests/is-entitled-to-feature.evaluator.spec.ts +68 -0
  71. package/src/user-entitlements/is-entitled-to-permission/index.ts +1 -0
  72. package/src/user-entitlements/is-entitled-to-permission/is-entitled-to-permission.evaluator.ts +38 -0
  73. package/src/user-entitlements/is-entitled-to-permission/tests/is-entitled-to-permission.evaluator.spec.ts +79 -0
  74. package/src/user-entitlements/types.ts +11 -4
  75. package/src/user-entitlements/{attributes.utils.ts → utils/attributes.utils.ts} +1 -1
  76. package/src/user-entitlements/utils/entitlement-results.utils.ts +22 -0
  77. package/src/user-entitlements/utils/index.ts +3 -0
  78. package/src/user-entitlements/{permissions.utils.ts → utils/permissions.utils.ts} +1 -1
  79. package/src/user-entitlements/{tests → utils/tests}/attributes.utils.spec.ts +1 -1
  80. package/src/user-entitlements/utils/tests/entitlement-results.utils.spec.ts +46 -0
  81. package/src/user-entitlements/{tests → utils/tests}/permissions.utils.spec.ts +1 -1
  82. package/dist/user-entitlements/attributes.utils.js.map +0 -1
  83. package/dist/user-entitlements/flatten.utils.js.map +0 -1
  84. package/dist/user-entitlements/is-entitled.evaluator.js +0 -59
  85. package/dist/user-entitlements/is-entitled.evaluator.js.map +0 -1
  86. package/dist/user-entitlements/permissions.utils.js.map +0 -1
  87. package/src/user-entitlements/is-entitled.evaluator.ts +0 -82
  88. package/src/user-entitlements/tests/is-entitled.evaluator.spec.ts +0 -298
  89. /package/dist/user-entitlements/{attributes.utils.js → utils/attributes.utils.js} +0 -0
  90. /package/dist/user-entitlements/{flatten.utils.d.ts → utils/flatten.utils.d.ts} +0 -0
  91. /package/dist/user-entitlements/{flatten.utils.js → utils/flatten.utils.js} +0 -0
  92. /package/dist/user-entitlements/{permissions.utils.js → utils/permissions.utils.js} +0 -0
  93. /package/src/user-entitlements/{flatten.utils.ts → utils/flatten.utils.ts} +0 -0
@@ -0,0 +1,121 @@
1
+ import { ConditionLogicEnum, TreatmentEnum } from '../../rules';
2
+ import { OperationEnum } from '../../operations/types';
3
+ import { Plan } from '../types';
4
+ import { evaluatePlan } from '../plan.evaluator';
5
+
6
+ type PlanCreator = (data?: Partial<Plan>) => Plan;
7
+ const planFactory: PlanCreator = (data?: Partial<Plan>) => ({
8
+ defaultTreatment: TreatmentEnum.False,
9
+ ...data,
10
+ });
11
+
12
+ describe('evaluatePlan', () => {
13
+ describe('default treatment', () => {
14
+ it('should return false when no rule is valid and defaultTreatment is false', () => {
15
+ const plan: Plan = planFactory({
16
+ rules: [
17
+ {
18
+ treatment: TreatmentEnum.True,
19
+ conditions: [
20
+ {
21
+ attribute: 'test',
22
+ op: 'not supported' as any,
23
+ value: { list: ['test'] },
24
+ negate: false,
25
+ },
26
+ ],
27
+ conditionLogic: ConditionLogicEnum.And,
28
+ },
29
+ ],
30
+ });
31
+
32
+ const result = evaluatePlan(plan, {});
33
+
34
+ expect(result).toEqual({ treatment: TreatmentEnum.False });
35
+ });
36
+
37
+ it('should return true when no rule is valid and defaultTreatment is true', () => {
38
+ const plan: Plan = planFactory({
39
+ defaultTreatment: TreatmentEnum.True,
40
+ rules: [
41
+ {
42
+ treatment: TreatmentEnum.True,
43
+ conditions: [
44
+ {
45
+ attribute: 'test',
46
+ op: 'not supported' as any,
47
+ value: { list: ['test'] },
48
+ negate: false,
49
+ },
50
+ ],
51
+ conditionLogic: ConditionLogicEnum.And,
52
+ },
53
+ ],
54
+ });
55
+
56
+ const result = evaluatePlan(plan, {});
57
+
58
+ expect(result).toEqual({ treatment: TreatmentEnum.True });
59
+ });
60
+ });
61
+
62
+ describe('complete evaluation', () => {
63
+ it('should return false according to first treatable rule', () => {
64
+ const plan: Plan = planFactory({
65
+ rules: [
66
+ {
67
+ treatment: TreatmentEnum.False,
68
+ conditions: [
69
+ {
70
+ attribute: 'attribute',
71
+ op: OperationEnum.InList,
72
+ value: { list: ['test'] },
73
+ negate: false,
74
+ },
75
+ ],
76
+ conditionLogic: ConditionLogicEnum.And,
77
+ },
78
+ ],
79
+ });
80
+
81
+ const result = evaluatePlan(plan, { attribute: 'test' });
82
+
83
+ expect(result).toEqual({ treatment: TreatmentEnum.False });
84
+ });
85
+
86
+ it('should return true according to first treatable rule', () => {
87
+ const plan: Plan = planFactory({
88
+ rules: [
89
+ {
90
+ treatment: TreatmentEnum.False,
91
+ conditions: [
92
+ {
93
+ attribute: 'test',
94
+ op: 'not supported' as any,
95
+ value: { list: ['test'] },
96
+ negate: false,
97
+ },
98
+ ],
99
+ conditionLogic: ConditionLogicEnum.And,
100
+ },
101
+ {
102
+ treatment: TreatmentEnum.True,
103
+ conditions: [
104
+ {
105
+ attribute: 'attribute',
106
+ op: OperationEnum.InList,
107
+ value: { list: ['test'] },
108
+ negate: false,
109
+ },
110
+ ],
111
+ conditionLogic: ConditionLogicEnum.And,
112
+ },
113
+ ],
114
+ });
115
+
116
+ const result = evaluatePlan(plan, { attribute: 'test' });
117
+
118
+ expect(result).toEqual({ treatment: TreatmentEnum.True });
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,11 @@
1
+ import { Rule, TreatmentEnum } from '../rules';
2
+
3
+ export interface Plan {
4
+ defaultTreatment: TreatmentEnum;
5
+ rules?: Rule[];
6
+ }
7
+
8
+ export interface PlanEvaluationResult {
9
+ treatment: TreatmentEnum;
10
+ }
11
+
@@ -1,4 +1,4 @@
1
- export * from './is-entitled.evaluator';
1
+ export * from './is-entitled-to-feature';
2
+ export * from './is-entitled-to-permission';
2
3
  export * from './types';
3
- export * from './attributes.utils';
4
- export * from './permissions.utils';
4
+ export * from './utils';
@@ -0,0 +1,28 @@
1
+ import {
2
+ Attributes,
3
+ EntitlementResult,
4
+ NO_EXPIRATION_TIME,
5
+ NotEntitledJustification,
6
+ UserEntitlementsContext,
7
+ } from '../../types';
8
+
9
+ export function directEntitlementEvalutor(
10
+ featureKey: string,
11
+ userEntitlementsContext: UserEntitlementsContext,
12
+ attributes: Attributes = {},
13
+ ): EntitlementResult {
14
+ const feature = userEntitlementsContext.features[featureKey];
15
+ let hasExpired = false;
16
+ if (feature && feature.expireTime !== null) {
17
+ hasExpired = feature.expireTime !== NO_EXPIRATION_TIME && feature.expireTime < Date.now();
18
+
19
+ if (!hasExpired) {
20
+ return { isEntitled: true };
21
+ }
22
+ }
23
+
24
+ return {
25
+ isEntitled: false,
26
+ justification: hasExpired ? NotEntitledJustification.BUNDLE_EXPIRED : NotEntitledJustification.MISSING_FEATURE,
27
+ };
28
+ }
@@ -0,0 +1,24 @@
1
+ import { Attributes, EntitlementResult, NotEntitledJustification, UserEntitlementsContext } from '../../types';
2
+ import { evaluateFeatureFlag } from '../../../feature-flags';
3
+ import { prepareAttributes } from '../../utils/attributes.utils';
4
+ import { TreatmentEnum } from '../../../rules';
5
+
6
+ export function featureFlagEvaluator(
7
+ featureKey: string,
8
+ userEntitlementsContext: UserEntitlementsContext,
9
+ attributes: Attributes = {},
10
+ ): EntitlementResult {
11
+ const feature = userEntitlementsContext.features[featureKey];
12
+ if (feature && feature.featureFlag) {
13
+ const preparedAttributes = prepareAttributes(attributes);
14
+ const { treatment } = evaluateFeatureFlag(feature.featureFlag, preparedAttributes);
15
+ if (treatment === TreatmentEnum.True) {
16
+ return { isEntitled: true };
17
+ }
18
+ }
19
+
20
+ return {
21
+ isEntitled: false,
22
+ justification: NotEntitledJustification.MISSING_FEATURE,
23
+ };
24
+ }
@@ -0,0 +1,8 @@
1
+ import { directEntitlementEvalutor } from './direct-entitlement.evaluator';
2
+ import { featureFlagEvaluator } from './feature-flag.evaluator';
3
+ import { planTargetingRulesEvalutor } from './plan-targeting-rules.evaluator';
4
+ import { IsEntitledEvaluator } from './types';
5
+
6
+ export function getIsEntitledEvaluators(): IsEntitledEvaluator[] {
7
+ return [directEntitlementEvalutor, featureFlagEvaluator, planTargetingRulesEvalutor];
8
+ }
@@ -0,0 +1,30 @@
1
+ import { Attributes, EntitlementResult, NotEntitledJustification, UserEntitlementsContext } from '../../types';
2
+ import { prepareAttributes } from '../../utils/attributes.utils';
3
+ import { TreatmentEnum } from '../../../rules';
4
+ import { evaluatePlan } from '../../../plans';
5
+
6
+ export function planTargetingRulesEvalutor(
7
+ featureKey: string,
8
+ userEntitlementsContext: UserEntitlementsContext,
9
+ attributes: Attributes = {},
10
+ ): EntitlementResult {
11
+ const feature = userEntitlementsContext.features[featureKey];
12
+ if (feature && feature.planIds && feature.planIds.length > 0) {
13
+ const preparedAttributes = prepareAttributes(attributes);
14
+ const plans = userEntitlementsContext.plans;
15
+ for (const planId of feature.planIds) {
16
+ const plan = plans[planId];
17
+ if (plan) {
18
+ const { treatment } = evaluatePlan(plan, preparedAttributes);
19
+ if (treatment === TreatmentEnum.True) {
20
+ return { isEntitled: true };
21
+ }
22
+ }
23
+ }
24
+ }
25
+
26
+ return {
27
+ isEntitled: false,
28
+ justification: NotEntitledJustification.MISSING_FEATURE,
29
+ };
30
+ }
@@ -0,0 +1,81 @@
1
+ import { NO_EXPIRATION_TIME, NotEntitledJustification } from '../../../types';
2
+ import { directEntitlementEvalutor } from '../direct-entitlement.evaluator';
3
+
4
+ describe('directEntitlementEvalutor', () => {
5
+ describe('entitled', () => {
6
+ test('feature is entitled with no expiration date', async () => {
7
+ const result = directEntitlementEvalutor('feature-key', {
8
+ features: {
9
+ 'feature-key': {
10
+ expireTime: NO_EXPIRATION_TIME,
11
+ linkedPermissions: [],
12
+ planIds: [],
13
+ },
14
+ },
15
+ permissions: {},
16
+ plans: {},
17
+ });
18
+
19
+ expect(result).toEqual({ isEntitled: true });
20
+ });
21
+
22
+ test('feature is entitled with a valid expiration date', async () => {
23
+ const result = directEntitlementEvalutor('feature-key', {
24
+ features: {
25
+ 'feature-key': {
26
+ expireTime: Date.now() + 3600,
27
+ linkedPermissions: [],
28
+ planIds: [],
29
+ },
30
+ },
31
+ permissions: {},
32
+ plans: {},
33
+ });
34
+
35
+ expect(result).toEqual({ isEntitled: true });
36
+ });
37
+ });
38
+ describe('not-entitled', () => {
39
+ test('feature is not entitled', async () => {
40
+ const result = directEntitlementEvalutor('feature-key', {
41
+ features: {
42
+ 'feature-key': {
43
+ expireTime: null,
44
+ linkedPermissions: [],
45
+ planIds: [],
46
+ },
47
+ },
48
+ permissions: {},
49
+ plans: {},
50
+ });
51
+
52
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
53
+ });
54
+
55
+ test('feature is entitled with an expired expiration date', async () => {
56
+ const result = directEntitlementEvalutor('feature-key', {
57
+ features: {
58
+ 'feature-key': {
59
+ expireTime: Date.now() - 3600,
60
+ linkedPermissions: [],
61
+ planIds: [],
62
+ },
63
+ },
64
+ permissions: {},
65
+ plans: {},
66
+ });
67
+
68
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.BUNDLE_EXPIRED });
69
+ });
70
+
71
+ test('feature does not exist', async () => {
72
+ const result = directEntitlementEvalutor('feature-key', {
73
+ features: {},
74
+ permissions: {},
75
+ plans: {},
76
+ });
77
+
78
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
79
+ });
80
+ });
81
+ });
@@ -0,0 +1,57 @@
1
+ import * as AttributesUtils from '../../../utils/attributes.utils';
2
+ import * as FeatureFlagEvaluator from '../../../../feature-flags/feature-flag.evaluator';
3
+ import { TreatmentEnum } from '../../../../rules';
4
+ import { featureFlagEvaluator } from '../feature-flag.evaluator';
5
+ import { NotEntitledJustification } from '../../../types';
6
+ describe('featureFlagEvaluator', () => {
7
+ beforeAll(async () => {
8
+ jest.spyOn(AttributesUtils, 'prepareAttributes').mockReturnValue({ testAttribute: 'test-value' });
9
+ });
10
+ describe('entitled', () => {
11
+ test('feature flag evaluation return truthy', async () => {
12
+ jest.spyOn(FeatureFlagEvaluator, 'evaluateFeatureFlag').mockReturnValue({ treatment: TreatmentEnum.True });
13
+ const result = featureFlagEvaluator('feature-key', {
14
+ features: {
15
+ 'feature-key': {
16
+ expireTime: null,
17
+ linkedPermissions: [],
18
+ planIds: [],
19
+ featureFlag: { defaultTreatment: TreatmentEnum.True, on: true, offTreatment: TreatmentEnum.False },
20
+ },
21
+ },
22
+ permissions: {},
23
+ plans: {},
24
+ });
25
+
26
+ expect(result).toEqual({ isEntitled: true });
27
+ });
28
+ });
29
+ describe('not-entitled', () => {
30
+ test('feature flag evaluation return falsy', async () => {
31
+ jest.spyOn(FeatureFlagEvaluator, 'evaluateFeatureFlag').mockReturnValue({ treatment: TreatmentEnum.False });
32
+ const result = featureFlagEvaluator('feature-key', {
33
+ features: {
34
+ 'feature-key': {
35
+ expireTime: null,
36
+ linkedPermissions: [],
37
+ planIds: [],
38
+ featureFlag: { defaultTreatment: TreatmentEnum.False, on: true, offTreatment: TreatmentEnum.False },
39
+ },
40
+ },
41
+ permissions: {},
42
+ plans: {},
43
+ });
44
+
45
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
46
+ });
47
+ test('feataure does not exist', async () => {
48
+ const result = featureFlagEvaluator('feature-key', {
49
+ features: {},
50
+ permissions: {},
51
+ plans: {},
52
+ });
53
+
54
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,70 @@
1
+ import * as PlanEvaluator from '../../../../plans/plan.evaluator';
2
+ import { TreatmentEnum } from '../../../../rules';
3
+ import { NotEntitledJustification } from '../../../types';
4
+ import { planTargetingRulesEvalutor } from '../plan-targeting-rules.evaluator';
5
+ describe('planTargetingRulesEvalutor', () => {
6
+ describe('entitled', () => {
7
+ test('some of the plans targeting rules return truthy', async () => {
8
+ jest.spyOn(PlanEvaluator, 'evaluatePlan').mockReturnValueOnce({ treatment: TreatmentEnum.True });
9
+ jest.spyOn(PlanEvaluator, 'evaluatePlan').mockReturnValueOnce({ treatment: TreatmentEnum.False });
10
+
11
+ const result = planTargetingRulesEvalutor('feature-key', {
12
+ features: {
13
+ 'feature-key': {
14
+ expireTime: null,
15
+ linkedPermissions: [],
16
+ planIds: ['plan-1', 'plan-2'],
17
+ },
18
+ },
19
+ permissions: {},
20
+ plans: {
21
+ 'plan-1': {
22
+ defaultTreatment: TreatmentEnum.True,
23
+ },
24
+ 'plan-2': {
25
+ defaultTreatment: TreatmentEnum.False,
26
+ },
27
+ },
28
+ });
29
+
30
+ expect(result).toEqual({ isEntitled: true });
31
+ });
32
+ });
33
+ describe('not-entitled', () => {
34
+ test('all of the plans targeting rules return falsy', async () => {
35
+ jest.spyOn(PlanEvaluator, 'evaluatePlan').mockReturnValueOnce({ treatment: TreatmentEnum.False });
36
+ jest.spyOn(PlanEvaluator, 'evaluatePlan').mockReturnValueOnce({ treatment: TreatmentEnum.False });
37
+
38
+ const result = planTargetingRulesEvalutor('feature-key', {
39
+ features: {
40
+ 'feature-key': {
41
+ expireTime: null,
42
+ linkedPermissions: [],
43
+ planIds: ['plan-1', 'plan-2'],
44
+ },
45
+ },
46
+ permissions: {},
47
+ plans: {
48
+ 'plan-1': {
49
+ defaultTreatment: TreatmentEnum.False,
50
+ },
51
+ 'plan-2': {
52
+ defaultTreatment: TreatmentEnum.False,
53
+ },
54
+ },
55
+ });
56
+
57
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
58
+ });
59
+
60
+ test('feature does not exist', async () => {
61
+ const result = planTargetingRulesEvalutor('feature-key', {
62
+ features: {},
63
+ permissions: {},
64
+ plans: {},
65
+ });
66
+
67
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,7 @@
1
+ import { Attributes, EntitlementResult, UserEntitlementsContext } from '../../types';
2
+
3
+ export type IsEntitledEvaluator = (
4
+ featureKey: string,
5
+ userEntitlementsContext: UserEntitlementsContext,
6
+ attributes: Attributes,
7
+ ) => EntitlementResult;
@@ -0,0 +1 @@
1
+ export * from './is-entitled-to-feature.evaluator';
@@ -0,0 +1,18 @@
1
+ import { EntitlementResult, NotEntitledJustification, UserEntitlementsContext, Attributes } from '../types';
2
+ import { getResult, shouldContinue } from '../utils';
3
+ import { getIsEntitledEvaluators } from './evaluators';
4
+ export function evaluateIsEntitledToFeature(
5
+ featureKey: string,
6
+ userEntitlementsContext: UserEntitlementsContext,
7
+ attributes: Attributes = {},
8
+ ): EntitlementResult {
9
+ const entitlementResults: EntitlementResult[] = [];
10
+ for (const evaluator of getIsEntitledEvaluators()) {
11
+ entitlementResults.push(evaluator(featureKey, userEntitlementsContext, attributes));
12
+ if (!shouldContinue(entitlementResults)) {
13
+ break;
14
+ }
15
+ }
16
+
17
+ return getResult(entitlementResults);
18
+ }
@@ -0,0 +1,68 @@
1
+ import * as IsEntitledEvaluators from '../evaluators';
2
+ import { evaluateIsEntitledToFeature } from '../is-entitled-to-feature.evaluator';
3
+ import { EntitlementResult, NotEntitledJustification, NO_EXPIRATION_TIME, UserEntitlementsContext } from '../../types';
4
+
5
+ describe('evaluateIsEntitledToFeature', () => {
6
+ const mockTruthyEvaluator = (): EntitlementResult => ({ isEntitled: true });
7
+ const mockFalsyMissingFeatureEvaluator = (): EntitlementResult => ({
8
+ isEntitled: false,
9
+ justification: NotEntitledJustification.MISSING_PERMISSION,
10
+ });
11
+ const mockFalsyExpiredEvaluator = (): EntitlementResult => ({
12
+ isEntitled: false,
13
+ justification: NotEntitledJustification.BUNDLE_EXPIRED,
14
+ });
15
+
16
+ describe('entitled', () => {
17
+ test('given all evaluators return truthy, feature is entitled', async () => {
18
+ jest
19
+ .spyOn(IsEntitledEvaluators, 'getIsEntitledEvaluators')
20
+ .mockReturnValue([mockTruthyEvaluator, mockTruthyEvaluator]);
21
+ const result = evaluateIsEntitledToFeature('feauture-key', {
22
+ features: {},
23
+ permissions: {},
24
+ plans: {},
25
+ });
26
+
27
+ expect(result).toEqual({ isEntitled: true });
28
+ });
29
+ test('given some evaluators return truthy, feature is entitled', async () => {
30
+ jest
31
+ .spyOn(IsEntitledEvaluators, 'getIsEntitledEvaluators')
32
+ .mockReturnValue([mockTruthyEvaluator, mockFalsyMissingFeatureEvaluator]);
33
+ const result = evaluateIsEntitledToFeature('feauture-key', {
34
+ features: {},
35
+ permissions: {},
36
+ plans: {},
37
+ });
38
+
39
+ expect(result).toEqual({ isEntitled: true });
40
+ });
41
+ });
42
+ describe('not-entitled', () => {
43
+ test('given all evaluators return falsy with missing feature, feature is not entitled, justified with missing feature', async () => {
44
+ jest
45
+ .spyOn(IsEntitledEvaluators, 'getIsEntitledEvaluators')
46
+ .mockReturnValue([mockFalsyMissingFeatureEvaluator, mockFalsyMissingFeatureEvaluator]);
47
+ const result = evaluateIsEntitledToFeature('feauture-key', {
48
+ features: {},
49
+ permissions: {},
50
+ plans: {},
51
+ });
52
+
53
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
54
+ });
55
+ test('given all evaluators return falsy, some with missing feature, some with bundle expired, feature is not entitled, justified with bundle expired', async () => {
56
+ jest
57
+ .spyOn(IsEntitledEvaluators, 'getIsEntitledEvaluators')
58
+ .mockReturnValue([mockFalsyMissingFeatureEvaluator, mockFalsyExpiredEvaluator]);
59
+ const result = evaluateIsEntitledToFeature('feauture-key', {
60
+ features: {},
61
+ permissions: {},
62
+ plans: {},
63
+ });
64
+
65
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.BUNDLE_EXPIRED });
66
+ });
67
+ });
68
+ });
@@ -0,0 +1 @@
1
+ export * from './is-entitled-to-permission.evaluator';
@@ -0,0 +1,38 @@
1
+ import { evaluateIsEntitledToFeature } from '../is-entitled-to-feature/is-entitled-to-feature.evaluator';
2
+ import { UserEntitlementsContext, Attributes, EntitlementResult, NotEntitledJustification } from '../types';
3
+ import { getResult, shouldContinue } from '../utils';
4
+ import { checkPermission } from '../utils/permissions.utils';
5
+
6
+ export function evaluateIsEntitledToPermissions(
7
+ permissionKey: string,
8
+ userEntitlementsContext: UserEntitlementsContext,
9
+ attributes?: Attributes,
10
+ ): EntitlementResult {
11
+ const hasPermission = checkPermission(userEntitlementsContext.permissions, permissionKey);
12
+ if (!hasPermission) {
13
+ return { isEntitled: false, justification: NotEntitledJustification.MISSING_PERMISSION };
14
+ }
15
+
16
+ const linkedFeatures = getLinkedFeatures(permissionKey, userEntitlementsContext);
17
+
18
+ if (!linkedFeatures.length) {
19
+ return { isEntitled: true };
20
+ }
21
+
22
+ const entitlementResults: EntitlementResult[] = [];
23
+ for (const featureKey of linkedFeatures) {
24
+ entitlementResults.push(evaluateIsEntitledToFeature(featureKey, userEntitlementsContext, attributes));
25
+
26
+ if (!shouldContinue(entitlementResults)) {
27
+ break;
28
+ }
29
+ }
30
+
31
+ return getResult(entitlementResults);
32
+ }
33
+
34
+ function getLinkedFeatures(permissionKey: string, userEntitlementsContext: UserEntitlementsContext): string[] {
35
+ return Object.keys(userEntitlementsContext.features).filter((featureKey) =>
36
+ userEntitlementsContext.features[featureKey].linkedPermissions.includes(permissionKey),
37
+ );
38
+ }
@@ -0,0 +1,79 @@
1
+ import { evaluateIsEntitledToPermissions } from '../is-entitled-to-permission.evaluator';
2
+ import { NO_EXPIRATION_TIME, NotEntitledJustification, UserEntitlementsContext } from '../../types';
3
+ import * as IsEntitledToFeatureEvaluator from '../../is-entitled-to-feature/is-entitled-to-feature.evaluator';
4
+ describe('evaluateIsEntitledToPermission', () => {
5
+ describe('entitled', () => {
6
+ test('permission granted, no linked feature/s to it', async () => {
7
+ const userEntitlementContext: UserEntitlementsContext = {
8
+ features: {},
9
+ plans: {},
10
+
11
+ permissions: { 'test.permission': true },
12
+ };
13
+ const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
14
+
15
+ expect(result).toEqual({ isEntitled: true });
16
+ });
17
+ test('permission granted with linked feature/s, feature is entitled', async () => {
18
+ jest.spyOn(IsEntitledToFeatureEvaluator, 'evaluateIsEntitledToFeature').mockReturnValue({ isEntitled: true });
19
+
20
+ const userEntitlementContext: UserEntitlementsContext = {
21
+ features: {
22
+ 'test-feature': { planIds: [], expireTime: NO_EXPIRATION_TIME, linkedPermissions: ['test.permission'] },
23
+ },
24
+ plans: {},
25
+
26
+ permissions: { 'test.permission': true },
27
+ };
28
+
29
+ const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
30
+
31
+ expect(result).toEqual({ isEntitled: true });
32
+ });
33
+ });
34
+
35
+ describe('not entitled', () => {
36
+ test('permission not granted', async () => {
37
+ const userEntitlementContext: UserEntitlementsContext = {
38
+ features: {},
39
+ plans: {},
40
+ permissions: {},
41
+ };
42
+ const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
43
+
44
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_PERMISSION });
45
+ });
46
+ test('permission granted with linked feature/s, no feature is entiteld', async () => {
47
+ jest
48
+ .spyOn(IsEntitledToFeatureEvaluator, 'evaluateIsEntitledToFeature')
49
+ .mockReturnValue({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
50
+
51
+ const userEntitlementContext: UserEntitlementsContext = {
52
+ features: { 'test-feature': { planIds: [], expireTime: null, linkedPermissions: ['test.permission'] } },
53
+ plans: {},
54
+ permissions: { 'test.permission': true },
55
+ };
56
+
57
+ const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
58
+
59
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.MISSING_FEATURE });
60
+ });
61
+ test('permission granted with linked feature/s, no feature is entiteld with expired bundle', async () => {
62
+ jest
63
+ .spyOn(IsEntitledToFeatureEvaluator, 'evaluateIsEntitledToFeature')
64
+ .mockReturnValue({ isEntitled: false, justification: NotEntitledJustification.BUNDLE_EXPIRED });
65
+ });
66
+
67
+ const userEntitlementContext: UserEntitlementsContext = {
68
+ features: {
69
+ 'test-feature': { planIds: [], expireTime: Date.now() - 3600, linkedPermissions: ['test.permission'] },
70
+ },
71
+ plans: {},
72
+ permissions: { 'test.permission': true },
73
+ };
74
+
75
+ const result = evaluateIsEntitledToPermissions('test.permission', userEntitlementContext, {});
76
+
77
+ expect(result).toEqual({ isEntitled: false, justification: NotEntitledJustification.BUNDLE_EXPIRED });
78
+ });
79
+ });