@rolloutctrl/evaluator 0.0.1 → 0.0.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.
package/dist/index.d.mts CHANGED
@@ -17,13 +17,14 @@ interface Segment {
17
17
  }
18
18
  interface Strategy {
19
19
  id: string;
20
+ flagKey: string;
20
21
  enabled: boolean;
21
22
  priority: number;
22
23
  matchType: MatchType;
23
24
  rolloutPercentage: number | null;
24
25
  rolloutStickinessField: string | null;
25
- startsAt: Date | null;
26
- endsAt: Date | null;
26
+ startsAt: Date | string | null;
27
+ endsAt: Date | string | null;
27
28
  segment: Segment | null;
28
29
  rules: Rule[];
29
30
  }
@@ -64,7 +65,7 @@ declare function evaluateRules(rules: Rule[], matchType: MatchType, context: Eva
64
65
 
65
66
  declare function evaluateSegment(segment: Segment, context: EvaluationContext): boolean;
66
67
 
67
- declare function evaluateRollout(strategyId: string, percentage: number, stickinessField: string | null, context: EvaluationContext): boolean;
68
+ declare function evaluateRollout(flagKey: string, percentage: number, stickinessField: string | null, context: EvaluationContext): boolean;
68
69
 
69
70
  declare function evaluateStrategy(strategy: Strategy, context: EvaluationContext): boolean;
70
71
  declare function evaluateActionStrategy(strategy: ActionStrategy, context: EvaluationContext): ActionEffect | null;
package/dist/index.d.ts CHANGED
@@ -17,13 +17,14 @@ interface Segment {
17
17
  }
18
18
  interface Strategy {
19
19
  id: string;
20
+ flagKey: string;
20
21
  enabled: boolean;
21
22
  priority: number;
22
23
  matchType: MatchType;
23
24
  rolloutPercentage: number | null;
24
25
  rolloutStickinessField: string | null;
25
- startsAt: Date | null;
26
- endsAt: Date | null;
26
+ startsAt: Date | string | null;
27
+ endsAt: Date | string | null;
27
28
  segment: Segment | null;
28
29
  rules: Rule[];
29
30
  }
@@ -64,7 +65,7 @@ declare function evaluateRules(rules: Rule[], matchType: MatchType, context: Eva
64
65
 
65
66
  declare function evaluateSegment(segment: Segment, context: EvaluationContext): boolean;
66
67
 
67
- declare function evaluateRollout(strategyId: string, percentage: number, stickinessField: string | null, context: EvaluationContext): boolean;
68
+ declare function evaluateRollout(flagKey: string, percentage: number, stickinessField: string | null, context: EvaluationContext): boolean;
68
69
 
69
70
  declare function evaluateStrategy(strategy: Strategy, context: EvaluationContext): boolean;
70
71
  declare function evaluateActionStrategy(strategy: ActionStrategy, context: EvaluationContext): ActionEffect | null;
package/dist/index.js CHANGED
@@ -44,48 +44,61 @@ function getFieldValue(context, field) {
44
44
  }
45
45
  return current;
46
46
  }
47
+ function normalizeRuleValue(value) {
48
+ if (typeof value === "string") {
49
+ const trimmed = value.trim();
50
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
51
+ try {
52
+ return JSON.parse(trimmed);
53
+ } catch {
54
+ }
55
+ }
56
+ }
57
+ return value;
58
+ }
47
59
  function toNumber(value) {
48
60
  const n = Number(value);
49
61
  return isNaN(n) ? null : n;
50
62
  }
51
63
  function applyOperator(operator, contextValue, ruleValue) {
64
+ const normalizedValue = normalizeRuleValue(ruleValue);
52
65
  switch (operator) {
53
66
  case "EQUALS":
54
- return String(contextValue) === String(ruleValue);
67
+ return String(contextValue) === String(normalizedValue);
55
68
  case "IN": {
56
- const list = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
57
- return list.map(String).includes(String(contextValue));
69
+ const list = Array.isArray(normalizedValue) ? normalizedValue : [normalizedValue];
70
+ return list.includes(contextValue);
58
71
  }
59
72
  case "INCLUDES": {
60
73
  if (!Array.isArray(contextValue)) return false;
61
- return contextValue.map(String).includes(String(ruleValue));
74
+ return contextValue.includes(normalizedValue);
62
75
  }
63
76
  case "GT": {
64
77
  const a = toNumber(contextValue);
65
- const b = toNumber(ruleValue);
78
+ const b = toNumber(normalizedValue);
66
79
  return a !== null && b !== null && a > b;
67
80
  }
68
81
  case "LT": {
69
82
  const a = toNumber(contextValue);
70
- const b = toNumber(ruleValue);
83
+ const b = toNumber(normalizedValue);
71
84
  return a !== null && b !== null && a < b;
72
85
  }
73
86
  case "GTE": {
74
87
  const a = toNumber(contextValue);
75
- const b = toNumber(ruleValue);
88
+ const b = toNumber(normalizedValue);
76
89
  return a !== null && b !== null && a >= b;
77
90
  }
78
91
  case "LTE": {
79
92
  const a = toNumber(contextValue);
80
- const b = toNumber(ruleValue);
93
+ const b = toNumber(normalizedValue);
81
94
  return a !== null && b !== null && a <= b;
82
95
  }
83
96
  case "CONTAINS":
84
- return typeof contextValue === "string" && contextValue.includes(String(ruleValue));
97
+ return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.includes(normalizedValue);
85
98
  case "STARTS_WITH":
86
- return typeof contextValue === "string" && contextValue.startsWith(String(ruleValue));
99
+ return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.startsWith(normalizedValue);
87
100
  case "ENDS_WITH":
88
- return typeof contextValue === "string" && contextValue.endsWith(String(ruleValue));
101
+ return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.endsWith(normalizedValue);
89
102
  default:
90
103
  return false;
91
104
  }
@@ -98,7 +111,13 @@ function evaluateOperator(operator, not, field, ruleValue, context) {
98
111
 
99
112
  // src/rule.ts
100
113
  function evaluateRule(rule, context) {
101
- return evaluateOperator(rule.operator, rule.not, rule.field, rule.value, context);
114
+ return evaluateOperator(
115
+ rule.operator,
116
+ rule.not,
117
+ rule.field,
118
+ rule.value,
119
+ context
120
+ );
102
121
  }
103
122
  function evaluateRules(rules, matchType, context) {
104
123
  if (rules.length === 0) return true;
@@ -116,6 +135,9 @@ function evaluateSegment(segment, context) {
116
135
  }
117
136
 
118
137
  // src/rollout.ts
138
+ function getNestedValue(input, path) {
139
+ return path.split(".").reduce((obj, key) => obj?.[key], input);
140
+ }
119
141
  function hashString(input) {
120
142
  let hash = 5381;
121
143
  for (let i = 0; i < input.length; i++) {
@@ -124,11 +146,12 @@ function hashString(input) {
124
146
  }
125
147
  return hash;
126
148
  }
127
- function evaluateRollout(strategyId, percentage, stickinessField, context) {
149
+ function evaluateRollout(flagKey, percentage, stickinessField, context) {
128
150
  if (percentage <= 0) return false;
129
151
  if (percentage >= 100) return true;
130
- const stickyValue = stickinessField !== null ? String(context[stickinessField] ?? "") : "";
131
- const seed = `${strategyId}:${stickyValue}`;
152
+ const stickinessValue = stickinessField ? getNestedValue(context, stickinessField) : context["userId"];
153
+ if (stickinessValue === void 0 || stickinessValue === null) return false;
154
+ const seed = `${flagKey}:${String(stickinessValue)}`;
132
155
  const hash = hashString(seed);
133
156
  const bucket = hash % 100;
134
157
  return bucket < percentage;
@@ -137,8 +160,8 @@ function evaluateRollout(strategyId, percentage, stickinessField, context) {
137
160
  // src/strategy.ts
138
161
  function isWithinTimeWindow(startsAt, endsAt) {
139
162
  const now = /* @__PURE__ */ new Date();
140
- if (startsAt !== null && now < startsAt) return false;
141
- if (endsAt !== null && now > endsAt) return false;
163
+ if (startsAt && new Date(startsAt) > now) return false;
164
+ if (endsAt && new Date(endsAt) < now) return false;
142
165
  return true;
143
166
  }
144
167
  function evaluateStrategy(strategy, context) {
@@ -147,11 +170,12 @@ function evaluateStrategy(strategy, context) {
147
170
  if (strategy.segment !== null) {
148
171
  if (!evaluateSegment(strategy.segment, context)) return false;
149
172
  } else if (strategy.rules.length > 0) {
150
- if (!evaluateRules(strategy.rules, strategy.matchType, context)) return false;
173
+ if (!evaluateRules(strategy.rules, strategy.matchType, context))
174
+ return false;
151
175
  }
152
176
  if (strategy.rolloutPercentage !== null) {
153
177
  return evaluateRollout(
154
- strategy.id,
178
+ strategy.flagKey,
155
179
  strategy.rolloutPercentage,
156
180
  strategy.rolloutStickinessField,
157
181
  context
@@ -164,7 +188,8 @@ function evaluateActionStrategy(strategy, context) {
164
188
  if (strategy.segment !== null) {
165
189
  if (!evaluateSegment(strategy.segment, context)) return null;
166
190
  } else if (strategy.rules.length > 0) {
167
- if (!evaluateRules(strategy.rules, strategy.matchType, context)) return null;
191
+ if (!evaluateRules(strategy.rules, strategy.matchType, context))
192
+ return null;
168
193
  }
169
194
  return strategy.effect;
170
195
  }
@@ -174,7 +199,12 @@ function evaluateFeatureFlag(flagEnv, context) {
174
199
  if (!flagEnv.enabled) {
175
200
  return { enabled: false, strategyId: null };
176
201
  }
177
- const sorted = [...flagEnv.strategies].sort((a, b) => b.priority - a.priority);
202
+ if (!flagEnv.strategies || flagEnv.strategies.length === 0) {
203
+ return { enabled: true, strategyId: null };
204
+ }
205
+ const sorted = [...flagEnv.strategies].sort(
206
+ (a, b) => b.priority - a.priority
207
+ );
178
208
  for (const strategy of sorted) {
179
209
  if (evaluateStrategy(strategy, context)) {
180
210
  return { enabled: true, strategyId: strategy.id };
package/dist/index.mjs CHANGED
@@ -10,48 +10,61 @@ function getFieldValue(context, field) {
10
10
  }
11
11
  return current;
12
12
  }
13
+ function normalizeRuleValue(value) {
14
+ if (typeof value === "string") {
15
+ const trimmed = value.trim();
16
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
17
+ try {
18
+ return JSON.parse(trimmed);
19
+ } catch {
20
+ }
21
+ }
22
+ }
23
+ return value;
24
+ }
13
25
  function toNumber(value) {
14
26
  const n = Number(value);
15
27
  return isNaN(n) ? null : n;
16
28
  }
17
29
  function applyOperator(operator, contextValue, ruleValue) {
30
+ const normalizedValue = normalizeRuleValue(ruleValue);
18
31
  switch (operator) {
19
32
  case "EQUALS":
20
- return String(contextValue) === String(ruleValue);
33
+ return String(contextValue) === String(normalizedValue);
21
34
  case "IN": {
22
- const list = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
23
- return list.map(String).includes(String(contextValue));
35
+ const list = Array.isArray(normalizedValue) ? normalizedValue : [normalizedValue];
36
+ return list.includes(contextValue);
24
37
  }
25
38
  case "INCLUDES": {
26
39
  if (!Array.isArray(contextValue)) return false;
27
- return contextValue.map(String).includes(String(ruleValue));
40
+ return contextValue.includes(normalizedValue);
28
41
  }
29
42
  case "GT": {
30
43
  const a = toNumber(contextValue);
31
- const b = toNumber(ruleValue);
44
+ const b = toNumber(normalizedValue);
32
45
  return a !== null && b !== null && a > b;
33
46
  }
34
47
  case "LT": {
35
48
  const a = toNumber(contextValue);
36
- const b = toNumber(ruleValue);
49
+ const b = toNumber(normalizedValue);
37
50
  return a !== null && b !== null && a < b;
38
51
  }
39
52
  case "GTE": {
40
53
  const a = toNumber(contextValue);
41
- const b = toNumber(ruleValue);
54
+ const b = toNumber(normalizedValue);
42
55
  return a !== null && b !== null && a >= b;
43
56
  }
44
57
  case "LTE": {
45
58
  const a = toNumber(contextValue);
46
- const b = toNumber(ruleValue);
59
+ const b = toNumber(normalizedValue);
47
60
  return a !== null && b !== null && a <= b;
48
61
  }
49
62
  case "CONTAINS":
50
- return typeof contextValue === "string" && contextValue.includes(String(ruleValue));
63
+ return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.includes(normalizedValue);
51
64
  case "STARTS_WITH":
52
- return typeof contextValue === "string" && contextValue.startsWith(String(ruleValue));
65
+ return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.startsWith(normalizedValue);
53
66
  case "ENDS_WITH":
54
- return typeof contextValue === "string" && contextValue.endsWith(String(ruleValue));
67
+ return typeof contextValue === "string" && typeof normalizedValue === "string" && contextValue.endsWith(normalizedValue);
55
68
  default:
56
69
  return false;
57
70
  }
@@ -64,7 +77,13 @@ function evaluateOperator(operator, not, field, ruleValue, context) {
64
77
 
65
78
  // src/rule.ts
66
79
  function evaluateRule(rule, context) {
67
- return evaluateOperator(rule.operator, rule.not, rule.field, rule.value, context);
80
+ return evaluateOperator(
81
+ rule.operator,
82
+ rule.not,
83
+ rule.field,
84
+ rule.value,
85
+ context
86
+ );
68
87
  }
69
88
  function evaluateRules(rules, matchType, context) {
70
89
  if (rules.length === 0) return true;
@@ -82,6 +101,9 @@ function evaluateSegment(segment, context) {
82
101
  }
83
102
 
84
103
  // src/rollout.ts
104
+ function getNestedValue(input, path) {
105
+ return path.split(".").reduce((obj, key) => obj?.[key], input);
106
+ }
85
107
  function hashString(input) {
86
108
  let hash = 5381;
87
109
  for (let i = 0; i < input.length; i++) {
@@ -90,11 +112,12 @@ function hashString(input) {
90
112
  }
91
113
  return hash;
92
114
  }
93
- function evaluateRollout(strategyId, percentage, stickinessField, context) {
115
+ function evaluateRollout(flagKey, percentage, stickinessField, context) {
94
116
  if (percentage <= 0) return false;
95
117
  if (percentage >= 100) return true;
96
- const stickyValue = stickinessField !== null ? String(context[stickinessField] ?? "") : "";
97
- const seed = `${strategyId}:${stickyValue}`;
118
+ const stickinessValue = stickinessField ? getNestedValue(context, stickinessField) : context["userId"];
119
+ if (stickinessValue === void 0 || stickinessValue === null) return false;
120
+ const seed = `${flagKey}:${String(stickinessValue)}`;
98
121
  const hash = hashString(seed);
99
122
  const bucket = hash % 100;
100
123
  return bucket < percentage;
@@ -103,8 +126,8 @@ function evaluateRollout(strategyId, percentage, stickinessField, context) {
103
126
  // src/strategy.ts
104
127
  function isWithinTimeWindow(startsAt, endsAt) {
105
128
  const now = /* @__PURE__ */ new Date();
106
- if (startsAt !== null && now < startsAt) return false;
107
- if (endsAt !== null && now > endsAt) return false;
129
+ if (startsAt && new Date(startsAt) > now) return false;
130
+ if (endsAt && new Date(endsAt) < now) return false;
108
131
  return true;
109
132
  }
110
133
  function evaluateStrategy(strategy, context) {
@@ -113,11 +136,12 @@ function evaluateStrategy(strategy, context) {
113
136
  if (strategy.segment !== null) {
114
137
  if (!evaluateSegment(strategy.segment, context)) return false;
115
138
  } else if (strategy.rules.length > 0) {
116
- if (!evaluateRules(strategy.rules, strategy.matchType, context)) return false;
139
+ if (!evaluateRules(strategy.rules, strategy.matchType, context))
140
+ return false;
117
141
  }
118
142
  if (strategy.rolloutPercentage !== null) {
119
143
  return evaluateRollout(
120
- strategy.id,
144
+ strategy.flagKey,
121
145
  strategy.rolloutPercentage,
122
146
  strategy.rolloutStickinessField,
123
147
  context
@@ -130,7 +154,8 @@ function evaluateActionStrategy(strategy, context) {
130
154
  if (strategy.segment !== null) {
131
155
  if (!evaluateSegment(strategy.segment, context)) return null;
132
156
  } else if (strategy.rules.length > 0) {
133
- if (!evaluateRules(strategy.rules, strategy.matchType, context)) return null;
157
+ if (!evaluateRules(strategy.rules, strategy.matchType, context))
158
+ return null;
134
159
  }
135
160
  return strategy.effect;
136
161
  }
@@ -140,7 +165,12 @@ function evaluateFeatureFlag(flagEnv, context) {
140
165
  if (!flagEnv.enabled) {
141
166
  return { enabled: false, strategyId: null };
142
167
  }
143
- const sorted = [...flagEnv.strategies].sort((a, b) => b.priority - a.priority);
168
+ if (!flagEnv.strategies || flagEnv.strategies.length === 0) {
169
+ return { enabled: true, strategyId: null };
170
+ }
171
+ const sorted = [...flagEnv.strategies].sort(
172
+ (a, b) => b.priority - a.priority
173
+ );
144
174
  for (const strategy of sorted) {
145
175
  if (evaluateStrategy(strategy, context)) {
146
176
  return { enabled: true, strategyId: strategy.id };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@rolloutctrl/evaluator",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "RolloutCtrl feature flag & action evaluator",
5
+ "license": "MIT",
5
6
  "main": "./dist/index.js",
6
7
  "module": "./dist/index.mjs",
7
8
  "types": "./dist/index.d.ts",