Skip to content

Commit 9d14173

Browse files
adams85Adam Simonbeeme1mr
authored
fix(config-cat): Forward default value to underlying client (#1214)
Signed-off-by: Adam Simon <[email protected]> Co-authored-by: Adam Simon <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
1 parent 3dd1232 commit 9d14173

File tree

5 files changed

+122
-53
lines changed

5 files changed

+122
-53
lines changed

libs/providers/config-cat-web/src/lib/config-cat-web-provider.spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,9 @@ describe('ConfigCatWebProvider', () => {
172172
expect(() => provider.resolveObjectEvaluation('jsonInvalid', {}, { targetingKey })).toThrow(ParseError);
173173
});
174174

175-
it('should throw TypeMismatchError if string is only a JSON primitive', () => {
176-
expect(() => provider.resolveObjectEvaluation('jsonPrimitive', {}, { targetingKey })).toThrow(TypeMismatchError);
175+
it('should return right value if key exists and value is only a JSON primitive', () => {
176+
const value = provider.resolveObjectEvaluation('jsonPrimitive', {}, { targetingKey });
177+
expect(value).toHaveProperty('value', JSON.parse(values.jsonPrimitive));
177178
});
178179
});
179180
});

libs/providers/config-cat-web/src/lib/config-cat-web-provider.ts

+33-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
EvaluationContext,
3-
FlagNotFoundError,
43
JsonValue,
54
OpenFeatureEventEmitter,
65
Paradigm,
@@ -13,12 +12,20 @@ import {
1312
} from '@openfeature/web-sdk';
1413
import {
1514
isType,
15+
parseError,
1616
PrimitiveType,
1717
PrimitiveTypeName,
1818
toResolutionDetails,
1919
transformContext,
2020
} from '@openfeature/config-cat-core';
21-
import { getClient, IConfig, IConfigCatClient, OptionsForPollingMode, PollingMode } from 'configcat-js-ssr';
21+
import {
22+
getClient,
23+
IConfig,
24+
IConfigCatClient,
25+
OptionsForPollingMode,
26+
PollingMode,
27+
SettingValue,
28+
} from 'configcat-js-ssr';
2229

2330
export class ConfigCatWebProvider implements Provider {
2431
public readonly events = new OpenFeatureEventEmitter();
@@ -84,71 +91,81 @@ export class ConfigCatWebProvider implements Provider {
8491
defaultValue: boolean,
8592
context: EvaluationContext,
8693
): ResolutionDetails<boolean> {
87-
return this.evaluate(flagKey, 'boolean', context);
94+
return this.evaluate(flagKey, 'boolean', defaultValue, context);
8895
}
8996

9097
public resolveStringEvaluation(
9198
flagKey: string,
9299
defaultValue: string,
93100
context: EvaluationContext,
94101
): ResolutionDetails<string> {
95-
return this.evaluate(flagKey, 'string', context);
102+
return this.evaluate(flagKey, 'string', defaultValue, context);
96103
}
97104

98105
public resolveNumberEvaluation(
99106
flagKey: string,
100107
defaultValue: number,
101108
context: EvaluationContext,
102109
): ResolutionDetails<number> {
103-
return this.evaluate(flagKey, 'number', context);
110+
return this.evaluate(flagKey, 'number', defaultValue, context);
104111
}
105112

106113
public resolveObjectEvaluation<U extends JsonValue>(
107114
flagKey: string,
108115
defaultValue: U,
109116
context: EvaluationContext,
110117
): ResolutionDetails<U> {
111-
const objectValue = this.evaluate(flagKey, 'object', context);
118+
const objectValue = this.evaluate(flagKey, 'object', defaultValue, context);
112119
return objectValue as ResolutionDetails<U>;
113120
}
114121

115122
protected evaluate<T extends PrimitiveTypeName>(
116123
flagKey: string,
117124
flagType: T,
125+
defaultValue: PrimitiveType<T>,
118126
context: EvaluationContext,
119127
): ResolutionDetails<PrimitiveType<T>> {
120128
if (!this._client) {
121129
throw new ProviderNotReadyError('Provider is not initialized');
122130
}
123131

132+
// Make sure that the user-provided `defaultValue` is compatible with `flagType` as there is
133+
// no guarantee that it actually is. (User may bypass type checking or may not use TypeScript at all.)
134+
if (!isType(flagType, defaultValue)) {
135+
throw new TypeMismatchError();
136+
}
137+
138+
const configCatDefaultValue = flagType !== 'object' ? (defaultValue as SettingValue) : JSON.stringify(defaultValue);
139+
124140
const { value, ...evaluationData } = this._client
125141
.snapshot()
126-
.getValueDetails(flagKey, undefined, transformContext(context));
142+
.getValueDetails(flagKey, configCatDefaultValue, transformContext(context));
127143

128144
if (this._hasError && !evaluationData.errorMessage && !evaluationData.errorException) {
129145
this._hasError = false;
130146
this.events.emit(ProviderEvents.Ready);
131147
}
132148

133-
if (typeof value === 'undefined') {
134-
throw new FlagNotFoundError();
149+
if (evaluationData.isDefaultValue) {
150+
throw parseError(evaluationData.errorMessage);
135151
}
136152

137153
if (flagType !== 'object') {
138-
return toResolutionDetails(flagType, value, evaluationData);
139-
}
140-
141-
if (!isType('string', value)) {
142-
throw new TypeMismatchError();
154+
// When `flagType` (more precisely, `configCatDefaultValue`) is boolean, string or number,
155+
// ConfigCat SDK guarantees that the returned `value` is compatible with `PrimitiveType<T>`.
156+
// See also: https://configcat.com/docs/sdk-reference/js-ssr/#setting-type-mapping
157+
return toResolutionDetails(value as PrimitiveType<T>, evaluationData);
143158
}
144159

145160
let json: JsonValue;
146161
try {
147-
json = JSON.parse(value);
162+
// In this case we can be sure that `value` is string since `configCatDefaultValue` is string,
163+
// which means that ConfigCat SDK is guaranteed to return a string value.
164+
json = JSON.parse(value as string);
148165
} catch (e) {
149166
throw new ParseError(`Unable to parse "${value}" as JSON`);
150167
}
151168

152-
return toResolutionDetails(flagType, json, evaluationData);
169+
return toResolutionDetails(json as PrimitiveType<T>, evaluationData);
153170
}
154171
}

libs/providers/config-cat/src/lib/config-cat-provider.spec.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,9 @@ describe('ConfigCatProvider', () => {
183183
await expect(provider.resolveObjectEvaluation('jsonInvalid', {}, { targetingKey })).rejects.toThrow(ParseError);
184184
});
185185

186-
it('should throw TypeMismatchError if string is only a JSON primitive', async () => {
187-
await expect(provider.resolveObjectEvaluation('jsonPrimitive', {}, { targetingKey })).rejects.toThrow(
188-
TypeMismatchError,
189-
);
186+
it('should return right value if key exists and value is only a JSON primitive', async () => {
187+
const value = await provider.resolveObjectEvaluation('jsonPrimitive', {}, { targetingKey });
188+
expect(value).toHaveProperty('value', JSON.parse(values.jsonPrimitive));
190189
});
191190
});
192191
});

libs/providers/config-cat/src/lib/config-cat-provider.ts

+26-16
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import {
88
Paradigm,
99
ProviderNotReadyError,
1010
TypeMismatchError,
11-
FlagNotFoundError,
1211
ParseError,
1312
} from '@openfeature/server-sdk';
1413
import {
1514
isType,
15+
parseError,
1616
PrimitiveType,
1717
PrimitiveTypeName,
1818
toResolutionDetails,
1919
transformContext,
2020
} from '@openfeature/config-cat-core';
21-
import { PollingMode } from 'configcat-common';
21+
import { PollingMode, SettingValue } from 'configcat-common';
2222
import { IConfigCatClient, getClient, IConfig, OptionsForPollingMode } from 'configcat-node';
2323

2424
export class ConfigCatProvider implements Provider {
@@ -88,46 +88,55 @@ export class ConfigCatProvider implements Provider {
8888
defaultValue: boolean,
8989
context: EvaluationContext,
9090
): Promise<ResolutionDetails<boolean>> {
91-
return this.evaluate(flagKey, 'boolean', context);
91+
return this.evaluate(flagKey, 'boolean', defaultValue, context);
9292
}
9393

9494
public async resolveStringEvaluation(
9595
flagKey: string,
9696
defaultValue: string,
9797
context: EvaluationContext,
9898
): Promise<ResolutionDetails<string>> {
99-
return this.evaluate(flagKey, 'string', context);
99+
return this.evaluate(flagKey, 'string', defaultValue, context);
100100
}
101101

102102
public async resolveNumberEvaluation(
103103
flagKey: string,
104104
defaultValue: number,
105105
context: EvaluationContext,
106106
): Promise<ResolutionDetails<number>> {
107-
return this.evaluate(flagKey, 'number', context);
107+
return this.evaluate(flagKey, 'number', defaultValue, context);
108108
}
109109

110110
public async resolveObjectEvaluation<U extends JsonValue>(
111111
flagKey: string,
112112
defaultValue: U,
113113
context: EvaluationContext,
114114
): Promise<ResolutionDetails<U>> {
115-
const objectValue = await this.evaluate(flagKey, 'object', context);
115+
const objectValue = await this.evaluate(flagKey, 'object', defaultValue, context);
116116
return objectValue as ResolutionDetails<U>;
117117
}
118118

119119
protected async evaluate<T extends PrimitiveTypeName>(
120120
flagKey: string,
121121
flagType: T,
122+
defaultValue: PrimitiveType<T>,
122123
context: EvaluationContext,
123124
): Promise<ResolutionDetails<PrimitiveType<T>>> {
124125
if (!this._client) {
125126
throw new ProviderNotReadyError('Provider is not initialized');
126127
}
127128

129+
// Make sure that the user-provided `defaultValue` is compatible with `flagType` as there is
130+
// no guarantee that it actually is. (User may bypass type checking or may not use TypeScript at all.)
131+
if (!isType(flagType, defaultValue)) {
132+
throw new TypeMismatchError();
133+
}
134+
135+
const configCatDefaultValue = flagType !== 'object' ? (defaultValue as SettingValue) : JSON.stringify(defaultValue);
136+
128137
const { value, ...evaluationData } = await this._client.getValueDetailsAsync(
129138
flagKey,
130-
undefined,
139+
configCatDefaultValue,
131140
transformContext(context),
132141
);
133142

@@ -136,25 +145,26 @@ export class ConfigCatProvider implements Provider {
136145
this.events.emit(ProviderEvents.Ready);
137146
}
138147

139-
if (typeof value === 'undefined') {
140-
throw new FlagNotFoundError();
148+
if (evaluationData.isDefaultValue) {
149+
throw parseError(evaluationData.errorMessage);
141150
}
142151

143152
if (flagType !== 'object') {
144-
return toResolutionDetails(flagType, value, evaluationData);
145-
}
146-
147-
if (!isType('string', value)) {
148-
throw new TypeMismatchError();
153+
// When `flagType` (more precisely, `configCatDefaultValue`) is boolean, string or number,
154+
// ConfigCat SDK guarantees that the returned `value` is compatible with `PrimitiveType<T>`.
155+
// See also: https://configcat.com/docs/sdk-reference/node/#setting-type-mapping
156+
return toResolutionDetails(value as PrimitiveType<T>, evaluationData);
149157
}
150158

151159
let json: JsonValue;
152160
try {
153-
json = JSON.parse(value);
161+
// In this case we can be sure that `value` is string since `configCatDefaultValue` is string,
162+
// which means that ConfigCat SDK is guaranteed to return a string value.
163+
json = JSON.parse(value as string);
154164
} catch (e) {
155165
throw new ParseError(`Unable to parse "${value}" as JSON`);
156166
}
157167

158-
return toResolutionDetails(flagType, json, evaluationData);
168+
return toResolutionDetails(json as PrimitiveType<T>, evaluationData);
159169
}
160170
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import { IEvaluationDetails } from 'configcat-js-ssr';
2-
import { ResolutionDetails, ResolutionReason, TypeMismatchError, StandardResolutionReasons } from '@openfeature/core';
2+
import {
3+
ResolutionDetails,
4+
ResolutionReason,
5+
TypeMismatchError,
6+
StandardResolutionReasons,
7+
JsonValue,
8+
OpenFeatureError,
9+
ParseError,
10+
TargetingKeyMissingError,
11+
GeneralError,
12+
FlagNotFoundError,
13+
} from '@openfeature/core';
314

4-
export function toResolutionDetails<T extends PrimitiveTypeName>(
5-
type: T,
6-
value: unknown,
15+
export function toResolutionDetails<T>(
16+
value: T,
717
data: Omit<IEvaluationDetails, 'value'>,
818
reason?: ResolutionReason,
9-
): ResolutionDetails<PrimitiveType<T>> {
10-
if (!isType(type, value)) {
11-
throw new TypeMismatchError(`Requested ${type} flag but the actual value is ${typeof value}`);
12-
}
13-
19+
): ResolutionDetails<T> {
1420
const matchedTargeting = data.matchedTargetingRule;
1521
const matchedPercentage = data.matchedPercentageOption;
1622

@@ -25,19 +31,55 @@ export function toResolutionDetails<T extends PrimitiveTypeName>(
2531
};
2632
}
2733

28-
export type PrimitiveTypeName = 'string' | 'boolean' | 'number' | 'object' | 'undefined';
34+
export function parseError(errorMessage: string | undefined): OpenFeatureError {
35+
// Detecting the error type by checking the error message is awkward and fragile,
36+
// but ConfigCat SDK doesn't allow a better way at the moment.
37+
// However, there are plans to improve this situation, so let's revise this
38+
// as soon as ConfigCat SDK implements returning error codes.
39+
40+
if (errorMessage) {
41+
if (errorMessage.includes('Config JSON is not present')) {
42+
return new ParseError();
43+
}
44+
if (errorMessage.includes('the key was not found in config JSON')) {
45+
return new FlagNotFoundError();
46+
}
47+
if (
48+
errorMessage.includes('The type of a setting must match the type of the specified default value') ||
49+
/Setting value (?:is null|is undefined|'.*' is of an unsupported type)/.test(errorMessage)
50+
) {
51+
return new TypeMismatchError();
52+
}
53+
}
54+
return new GeneralError();
55+
}
56+
57+
export type PrimitiveTypeName = 'string' | 'boolean' | 'number' | 'object';
2958
export type PrimitiveType<T> = T extends 'string'
3059
? string
3160
: T extends 'boolean'
3261
? boolean
3362
: T extends 'number'
3463
? number
3564
: T extends 'object'
36-
? object
37-
: T extends 'undefined'
38-
? undefined
39-
: unknown;
65+
? JsonValue
66+
: never;
4067

4168
export function isType<T extends PrimitiveTypeName>(type: T, value: unknown): value is PrimitiveType<T> {
42-
return typeof value !== 'undefined' && typeof value === type;
69+
switch (type) {
70+
case 'string':
71+
case 'boolean':
72+
case 'number':
73+
return typeof value === type;
74+
case 'object':
75+
return (
76+
value === null ||
77+
typeof value === 'string' ||
78+
typeof value === 'boolean' ||
79+
typeof value === 'number' ||
80+
Array.isArray(value) ||
81+
typeof value === 'object'
82+
);
83+
}
84+
return false;
4385
}

0 commit comments

Comments
 (0)