@splitsoftware/openfeature-js-split-provider 1.0.7 → 1.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.
- package/CHANGES.txt +29 -13
- package/LICENSE +1 -1
- package/README.md +74 -12
- package/es/index.js +1 -0
- package/es/lib/js-split-provider.js +155 -0
- package/lib/index.js +4 -0
- package/lib/lib/js-split-provider.js +159 -0
- package/package.json +37 -29
- package/src/__tests__/mocks/redis-commands.txt +11 -0
- package/src/__tests__/nodeSuites/client.spec.js +181 -139
- package/src/__tests__/nodeSuites/client_redis.spec.js +115 -0
- package/src/__tests__/nodeSuites/provider.spec.js +241 -109
- package/src/__tests__/testUtils/eventSourceMock.js +1 -2
- package/src/__tests__/testUtils/index.js +43 -0
- package/src/lib/js-split-provider.ts +143 -73
- package/types/index.d.ts +1 -0
- package/types/lib/js-split-provider.d.ts +27 -0
- package/src/__tests__/node.spec.js +0 -7
|
@@ -1,44 +1,67 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EvaluationContext,
|
|
3
|
-
Provider,
|
|
4
|
-
ResolutionDetails,
|
|
5
|
-
ParseError,
|
|
6
3
|
FlagNotFoundError,
|
|
7
4
|
JsonValue,
|
|
8
|
-
|
|
5
|
+
OpenFeatureEventEmitter,
|
|
6
|
+
ParseError,
|
|
7
|
+
Provider,
|
|
8
|
+
ProviderEvents,
|
|
9
|
+
ResolutionDetails,
|
|
9
10
|
StandardResolutionReasons,
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
TargetingKeyMissingError,
|
|
12
|
+
TrackingEventDetails
|
|
13
|
+
} from '@openfeature/server-sdk';
|
|
14
|
+
import { SplitFactory } from '@splitsoftware/splitio';
|
|
15
|
+
import type SplitIO from '@splitsoftware/splitio/types/splitio';
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
splitClient: SplitIO.IClient;
|
|
17
|
+
type SplitProviderOptions = {
|
|
18
|
+
splitClient: SplitIO.IClient | SplitIO.IAsyncClient;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
type Consumer = {
|
|
18
|
-
|
|
22
|
+
targetingKey: string | undefined;
|
|
23
|
+
trafficType: string;
|
|
19
24
|
attributes: SplitIO.Attributes;
|
|
20
25
|
};
|
|
21
26
|
|
|
22
|
-
const CONTROL_VALUE_ERROR_MESSAGE =
|
|
27
|
+
const CONTROL_VALUE_ERROR_MESSAGE = 'Received the "control" value from Split.';
|
|
28
|
+
const CONTROL_TREATMENT = 'control';
|
|
23
29
|
|
|
24
30
|
export class OpenFeatureSplitProvider implements Provider {
|
|
25
31
|
metadata = {
|
|
26
|
-
name:
|
|
32
|
+
name: 'split',
|
|
27
33
|
};
|
|
28
|
-
|
|
29
|
-
private client: SplitIO.IClient;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
|
|
35
|
+
private client: SplitIO.IClient | SplitIO.IAsyncClient;
|
|
36
|
+
private trafficType: string;
|
|
37
|
+
public readonly events = new OpenFeatureEventEmitter();
|
|
38
|
+
|
|
39
|
+
private getSplitClient(options: SplitProviderOptions | string | SplitIO.ISDK | SplitIO.IAsyncSDK) {
|
|
40
|
+
if (typeof(options) === 'string') {
|
|
41
|
+
const splitFactory = SplitFactory({core: { authorizationKey: options } });
|
|
42
|
+
return splitFactory.client();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let splitClient;
|
|
46
|
+
try {
|
|
47
|
+
splitClient = (options as SplitIO.ISDK | SplitIO.IAsyncSDK).client();
|
|
48
|
+
} catch {
|
|
49
|
+
splitClient = (options as SplitProviderOptions).splitClient
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return splitClient;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
constructor(options: SplitProviderOptions | string | SplitIO.ISDK | SplitIO.IAsyncSDK) {
|
|
56
|
+
// Asume 'user' as default traffic type'
|
|
57
|
+
this.trafficType = 'user';
|
|
58
|
+
this.client = this.getSplitClient(options);
|
|
59
|
+
this.client.on(this.client.Event.SDK_UPDATE, () => {
|
|
60
|
+
this.events.emit(ProviderEvents.ConfigurationChanged);
|
|
61
|
+
});
|
|
39
62
|
}
|
|
40
63
|
|
|
41
|
-
async resolveBooleanEvaluation(
|
|
64
|
+
public async resolveBooleanEvaluation(
|
|
42
65
|
flagKey: string,
|
|
43
66
|
_: boolean,
|
|
44
67
|
context: EvaluationContext
|
|
@@ -47,36 +70,20 @@ export class OpenFeatureSplitProvider implements Provider {
|
|
|
47
70
|
flagKey,
|
|
48
71
|
this.transformContext(context)
|
|
49
72
|
);
|
|
73
|
+
const treatment = details.value.toLowerCase();
|
|
74
|
+
|
|
75
|
+
if ( treatment === 'on' || treatment === 'true' ) {
|
|
76
|
+
return { ...details, value: true };
|
|
77
|
+
}
|
|
50
78
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
case "on":
|
|
54
|
-
value = true;
|
|
55
|
-
break;
|
|
56
|
-
case "off":
|
|
57
|
-
value = false;
|
|
58
|
-
break;
|
|
59
|
-
case "true":
|
|
60
|
-
value = true;
|
|
61
|
-
break;
|
|
62
|
-
case "false":
|
|
63
|
-
value = false;
|
|
64
|
-
break;
|
|
65
|
-
case true:
|
|
66
|
-
value = true;
|
|
67
|
-
break;
|
|
68
|
-
case false:
|
|
69
|
-
value = false;
|
|
70
|
-
break;
|
|
71
|
-
case "control":
|
|
72
|
-
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
|
|
73
|
-
default:
|
|
74
|
-
throw new ParseError(`Invalid boolean value for ${details.value}`);
|
|
79
|
+
if ( treatment === 'off' || treatment === 'false' ) {
|
|
80
|
+
return { ...details, value: false };
|
|
75
81
|
}
|
|
76
|
-
|
|
82
|
+
|
|
83
|
+
throw new ParseError(`Invalid boolean value for ${treatment}`);
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
async resolveStringEvaluation(
|
|
86
|
+
public async resolveStringEvaluation(
|
|
80
87
|
flagKey: string,
|
|
81
88
|
_: string,
|
|
82
89
|
context: EvaluationContext
|
|
@@ -85,13 +92,10 @@ export class OpenFeatureSplitProvider implements Provider {
|
|
|
85
92
|
flagKey,
|
|
86
93
|
this.transformContext(context)
|
|
87
94
|
);
|
|
88
|
-
if (details.value === "control") {
|
|
89
|
-
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
|
|
90
|
-
}
|
|
91
95
|
return details;
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
async resolveNumberEvaluation(
|
|
98
|
+
public async resolveNumberEvaluation(
|
|
95
99
|
flagKey: string,
|
|
96
100
|
_: number,
|
|
97
101
|
context: EvaluationContext
|
|
@@ -103,7 +107,7 @@ export class OpenFeatureSplitProvider implements Provider {
|
|
|
103
107
|
return { ...details, value: this.parseValidNumber(details.value) };
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
async resolveObjectEvaluation<U extends JsonValue>(
|
|
110
|
+
public async resolveObjectEvaluation<U extends JsonValue>(
|
|
107
111
|
flagKey: string,
|
|
108
112
|
_: U,
|
|
109
113
|
context: EvaluationContext
|
|
@@ -119,31 +123,82 @@ export class OpenFeatureSplitProvider implements Provider {
|
|
|
119
123
|
flagKey: string,
|
|
120
124
|
consumer: Consumer
|
|
121
125
|
): Promise<ResolutionDetails<string>> {
|
|
122
|
-
if (!consumer.
|
|
126
|
+
if (!consumer.targetingKey) {
|
|
123
127
|
throw new TargetingKeyMissingError(
|
|
124
|
-
|
|
128
|
+
'The Split provider requires a targeting key.'
|
|
125
129
|
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
flagKey,
|
|
131
|
-
consumer.attributes
|
|
130
|
+
}
|
|
131
|
+
if (flagKey == null || flagKey === '') {
|
|
132
|
+
throw new FlagNotFoundError(
|
|
133
|
+
'flagKey must be a non-empty string'
|
|
132
134
|
);
|
|
133
|
-
const details: ResolutionDetails<string> = {
|
|
134
|
-
value: value,
|
|
135
|
-
variant: value,
|
|
136
|
-
reason: StandardResolutionReasons.TARGETING_MATCH,
|
|
137
|
-
};
|
|
138
|
-
return details;
|
|
139
135
|
}
|
|
136
|
+
|
|
137
|
+
await new Promise((resolve, reject) => {
|
|
138
|
+
this.readinessHandler(resolve, reject);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const { treatment: value, config }: SplitIO.TreatmentWithConfig = await this.client.getTreatmentWithConfig(
|
|
142
|
+
consumer.targetingKey,
|
|
143
|
+
flagKey,
|
|
144
|
+
consumer.attributes
|
|
145
|
+
);
|
|
146
|
+
if (value === CONTROL_TREATMENT) {
|
|
147
|
+
throw new FlagNotFoundError(CONTROL_VALUE_ERROR_MESSAGE);
|
|
148
|
+
}
|
|
149
|
+
const flagMetadata = { config: config ? config : '' };
|
|
150
|
+
const details: ResolutionDetails<string> = {
|
|
151
|
+
value: value,
|
|
152
|
+
variant: value,
|
|
153
|
+
flagMetadata: flagMetadata,
|
|
154
|
+
reason: StandardResolutionReasons.TARGETING_MATCH,
|
|
155
|
+
};
|
|
156
|
+
return details;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async track(
|
|
160
|
+
trackingEventName: string,
|
|
161
|
+
context: EvaluationContext,
|
|
162
|
+
details: TrackingEventDetails
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
|
|
165
|
+
// eventName is always required
|
|
166
|
+
if (trackingEventName == null || trackingEventName === '')
|
|
167
|
+
throw new ParseError('Missing eventName, required to track');
|
|
168
|
+
|
|
169
|
+
// targetingKey is always required
|
|
170
|
+
const { targetingKey, trafficType } = this.transformContext(context);
|
|
171
|
+
if (targetingKey == null || targetingKey === '')
|
|
172
|
+
throw new TargetingKeyMissingError('Missing targetingKey, required to track');
|
|
173
|
+
|
|
174
|
+
let value;
|
|
175
|
+
let properties: SplitIO.Properties = {};
|
|
176
|
+
if (details != null) {
|
|
177
|
+
if (details.value != null) {
|
|
178
|
+
value = details.value;
|
|
179
|
+
}
|
|
180
|
+
if (details.properties != null) {
|
|
181
|
+
properties = details.properties as SplitIO.Properties;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.client.track(targetingKey, trafficType, trackingEventName, value, properties);
|
|
140
186
|
}
|
|
141
187
|
|
|
142
|
-
|
|
188
|
+
public async onClose?(): Promise<void> {
|
|
189
|
+
return this.client.destroy();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
//Transform the context into an object useful for the Split API, an key string with arbitrary Split 'Attributes'.
|
|
143
193
|
private transformContext(context: EvaluationContext): Consumer {
|
|
144
|
-
const { targetingKey, ...attributes } = context;
|
|
194
|
+
const { targetingKey, trafficType: ttVal, ...attributes } = context;
|
|
195
|
+
const trafficType =
|
|
196
|
+
ttVal != null && typeof ttVal === 'string' && ttVal.trim() !== ''
|
|
197
|
+
? ttVal
|
|
198
|
+
: this.trafficType;
|
|
145
199
|
return {
|
|
146
|
-
|
|
200
|
+
targetingKey,
|
|
201
|
+
trafficType,
|
|
147
202
|
// Stringify context objects include date.
|
|
148
203
|
attributes: JSON.parse(JSON.stringify(attributes)),
|
|
149
204
|
};
|
|
@@ -169,7 +224,7 @@ export class OpenFeatureSplitProvider implements Provider {
|
|
|
169
224
|
// we may want to allow the parsing to be customized.
|
|
170
225
|
try {
|
|
171
226
|
const value = JSON.parse(stringValue);
|
|
172
|
-
if (typeof value !==
|
|
227
|
+
if (typeof value !== 'object') {
|
|
173
228
|
throw new ParseError(
|
|
174
229
|
`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`
|
|
175
230
|
);
|
|
@@ -179,4 +234,19 @@ export class OpenFeatureSplitProvider implements Provider {
|
|
|
179
234
|
throw new ParseError(`Error parsing ${stringValue} as JSON, ${err}`);
|
|
180
235
|
}
|
|
181
236
|
}
|
|
182
|
-
|
|
237
|
+
|
|
238
|
+
private async readinessHandler(onSdkReady: (params?: unknown) => void, onSdkTimedOut: () => void): Promise<void> {
|
|
239
|
+
|
|
240
|
+
const clientStatus = this.client.getStatus();
|
|
241
|
+
if (clientStatus.isReady) {
|
|
242
|
+
onSdkReady();
|
|
243
|
+
} else {
|
|
244
|
+
if (clientStatus.hasTimedout) {
|
|
245
|
+
onSdkTimedOut();
|
|
246
|
+
} else {
|
|
247
|
+
this.client.on(this.client.Event.SDK_READY_TIMED_OUT, onSdkTimedOut);
|
|
248
|
+
}
|
|
249
|
+
this.client.on(this.client.Event.SDK_READY, onSdkReady);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/js-split-provider';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EvaluationContext, JsonValue, OpenFeatureEventEmitter, Provider, ResolutionDetails, TrackingEventDetails } from '@openfeature/server-sdk';
|
|
2
|
+
import type SplitIO from '@splitsoftware/splitio/types/splitio';
|
|
3
|
+
type SplitProviderOptions = {
|
|
4
|
+
splitClient: SplitIO.IClient | SplitIO.IAsyncClient;
|
|
5
|
+
};
|
|
6
|
+
export declare class OpenFeatureSplitProvider implements Provider {
|
|
7
|
+
metadata: {
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
private client;
|
|
11
|
+
private trafficType;
|
|
12
|
+
readonly events: OpenFeatureEventEmitter;
|
|
13
|
+
private getSplitClient;
|
|
14
|
+
constructor(options: SplitProviderOptions | string | SplitIO.ISDK | SplitIO.IAsyncSDK);
|
|
15
|
+
resolveBooleanEvaluation(flagKey: string, _: boolean, context: EvaluationContext): Promise<ResolutionDetails<boolean>>;
|
|
16
|
+
resolveStringEvaluation(flagKey: string, _: string, context: EvaluationContext): Promise<ResolutionDetails<string>>;
|
|
17
|
+
resolveNumberEvaluation(flagKey: string, _: number, context: EvaluationContext): Promise<ResolutionDetails<number>>;
|
|
18
|
+
resolveObjectEvaluation<U extends JsonValue>(flagKey: string, _: U, context: EvaluationContext): Promise<ResolutionDetails<U>>;
|
|
19
|
+
private evaluateTreatment;
|
|
20
|
+
track(trackingEventName: string, context: EvaluationContext, details: TrackingEventDetails): Promise<void>;
|
|
21
|
+
onClose?(): Promise<void>;
|
|
22
|
+
private transformContext;
|
|
23
|
+
private parseValidNumber;
|
|
24
|
+
private parseValidJsonObject;
|
|
25
|
+
private readinessHandler;
|
|
26
|
+
}
|
|
27
|
+
export {};
|