@messagevisor/sdk 0.0.1 → 0.2.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.
@@ -0,0 +1,207 @@
1
+ import type { Condition, Context, GroupSegment, Segment } from "@messagevisor/types";
2
+
3
+ export interface EvaluateOptions {
4
+ context?: Context;
5
+ segments?: Record<string, Segment>;
6
+ resolveFlag?: (featureKey: string, context?: Context) => boolean;
7
+ resolveVariation?: (experimentKey: string, context?: Context) => string | null;
8
+ }
9
+
10
+ function getContextValue(context: Context | undefined, attribute: string) {
11
+ if (!context) {
12
+ return undefined;
13
+ }
14
+
15
+ return attribute
16
+ .split(".")
17
+ .reduce((value: any, part) => (value ? value[part] : undefined), context as any);
18
+ }
19
+
20
+ function compareDate(value: unknown, expected: unknown, operator: "before" | "after") {
21
+ const valueTime = new Date(value as any).getTime();
22
+ const expectedTime = new Date(expected as any).getTime();
23
+
24
+ if (isNaN(valueTime) || isNaN(expectedTime)) {
25
+ return false;
26
+ }
27
+
28
+ return operator === "before" ? valueTime < expectedTime : valueTime > expectedTime;
29
+ }
30
+
31
+ function stringContains(value: unknown, expected: unknown) {
32
+ return String(value).indexOf(String(expected)) !== -1;
33
+ }
34
+
35
+ function stringStartsWith(value: unknown, expected: unknown) {
36
+ const valueAsString = String(value);
37
+ const expectedAsString = String(expected);
38
+
39
+ return valueAsString.slice(0, expectedAsString.length) === expectedAsString;
40
+ }
41
+
42
+ function stringEndsWith(value: unknown, expected: unknown) {
43
+ const valueAsString = String(value);
44
+ const expectedAsString = String(expected);
45
+
46
+ return valueAsString.slice(valueAsString.length - expectedAsString.length) === expectedAsString;
47
+ }
48
+
49
+ function arrayContains(value: unknown[], expected: unknown) {
50
+ return value.indexOf(expected) !== -1;
51
+ }
52
+
53
+ function parseStructuredString(value: string): unknown {
54
+ if (!(value.startsWith("{") || value.startsWith("["))) {
55
+ return value;
56
+ }
57
+
58
+ try {
59
+ return JSON.parse(value);
60
+ } catch {
61
+ return value;
62
+ }
63
+ }
64
+
65
+ export function evaluateCondition(
66
+ condition: Condition | Condition[] | "*" | undefined,
67
+ options: EvaluateOptions = {},
68
+ ): boolean {
69
+ if (!condition || condition === "*") {
70
+ return true;
71
+ }
72
+
73
+ if (Array.isArray(condition)) {
74
+ return condition.every((item) => evaluateCondition(item, options));
75
+ }
76
+
77
+ if (typeof condition === "string") {
78
+ const parsedCondition = parseStructuredString(condition);
79
+
80
+ if (parsedCondition !== condition) {
81
+ return evaluateCondition(parsedCondition as Condition | Condition[], options);
82
+ }
83
+
84
+ return false;
85
+ }
86
+
87
+ if ("and" in condition) {
88
+ return condition.and.every((item) => evaluateCondition(item, options));
89
+ }
90
+
91
+ if ("or" in condition) {
92
+ return condition.or.some((item) => evaluateCondition(item, options));
93
+ }
94
+
95
+ if ("not" in condition) {
96
+ return !condition.not.every((item) => evaluateCondition(item, options));
97
+ }
98
+
99
+ if ("feature" in condition) {
100
+ const enabled = options.resolveFlag
101
+ ? options.resolveFlag(condition.feature, options.context)
102
+ : false;
103
+ return condition.operator === "isEnabled"
104
+ ? enabled
105
+ : condition.operator === "isDisabled"
106
+ ? !enabled
107
+ : false;
108
+ }
109
+
110
+ if ("experiment" in condition) {
111
+ const variation = options.resolveVariation
112
+ ? options.resolveVariation(condition.experiment, options.context)
113
+ : undefined;
114
+ return condition.operator === "hasVariation" ? variation === condition.value : false;
115
+ }
116
+
117
+ const value = getContextValue(options.context, condition.attribute);
118
+ const expected = condition.value;
119
+
120
+ switch (condition.operator) {
121
+ case "equals":
122
+ return value === expected;
123
+ case "notEquals":
124
+ return value !== expected;
125
+ case "exists":
126
+ return value !== undefined && value !== null;
127
+ case "notExists":
128
+ return value === undefined || value === null;
129
+ case "greaterThan":
130
+ return Number(value) > Number(expected);
131
+ case "greaterThanOrEquals":
132
+ return Number(value) >= Number(expected);
133
+ case "lessThan":
134
+ return Number(value) < Number(expected);
135
+ case "lessThanOrEquals":
136
+ return Number(value) <= Number(expected);
137
+ case "contains":
138
+ return stringContains(value, expected);
139
+ case "notContains":
140
+ return !stringContains(value, expected);
141
+ case "startsWith":
142
+ return stringStartsWith(value, expected);
143
+ case "endsWith":
144
+ return stringEndsWith(value, expected);
145
+ case "before":
146
+ return compareDate(value, expected, "before");
147
+ case "after":
148
+ return compareDate(value, expected, "after");
149
+ case "includes":
150
+ return Array.isArray(value) && arrayContains(value, expected);
151
+ case "notIncludes":
152
+ return !Array.isArray(value) || !arrayContains(value, expected);
153
+ case "in":
154
+ return Array.isArray(expected) && arrayContains(expected, value);
155
+ case "notIn":
156
+ return !Array.isArray(expected) || !arrayContains(expected, value);
157
+ default:
158
+ return false;
159
+ }
160
+ }
161
+
162
+ export function evaluateGroupSegment(
163
+ groupSegment: GroupSegment | GroupSegment[] | "*" | undefined,
164
+ options: EvaluateOptions = {},
165
+ ): boolean {
166
+ if (!groupSegment || groupSegment === "*") {
167
+ return true;
168
+ }
169
+
170
+ if (Array.isArray(groupSegment)) {
171
+ return groupSegment.every((item) => evaluateGroupSegment(item, options));
172
+ }
173
+
174
+ if (typeof groupSegment === "string") {
175
+ const parsedGroupSegment = parseStructuredString(groupSegment);
176
+
177
+ if (parsedGroupSegment !== groupSegment) {
178
+ return evaluateGroupSegment(parsedGroupSegment as GroupSegment | GroupSegment[], options);
179
+ }
180
+
181
+ return evaluateSegment(groupSegment, options);
182
+ }
183
+
184
+ if ("and" in groupSegment) {
185
+ return groupSegment.and.every((item) => evaluateGroupSegment(item, options));
186
+ }
187
+
188
+ if ("or" in groupSegment) {
189
+ return groupSegment.or.some((item) => evaluateGroupSegment(item, options));
190
+ }
191
+
192
+ if ("not" in groupSegment) {
193
+ return !groupSegment.not.every((item) => evaluateGroupSegment(item, options));
194
+ }
195
+
196
+ return false;
197
+ }
198
+
199
+ export function evaluateSegment(segmentKey: string, options: EvaluateOptions = {}) {
200
+ const segment = options.segments ? options.segments[segmentKey] : undefined;
201
+
202
+ if (!segment || segment.archived) {
203
+ return false;
204
+ }
205
+
206
+ return evaluateCondition(segment.conditions, options);
207
+ }