Skip to content

Commit 2e1d2cb

Browse files
Adam Simonadams85
Adam Simon
authored andcommitted
Forward default value to underlying client (attempt to fix #1213)
Signed-off-by: Adam Simon <[email protected]>
1 parent 3e1295c commit 2e1d2cb

File tree

3 files changed

+101
-55
lines changed

3 files changed

+101
-55
lines changed

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

+28-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,13 @@ 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 { getClient, IConfig, IConfigCatClient, OptionsForPollingMode, PollingMode, SettingValue } from 'configcat-js-ssr';
2222

2323
export class ConfigCatWebProvider implements Provider {
2424
public readonly events = new OpenFeatureEventEmitter();
@@ -84,71 +84,83 @@ export class ConfigCatWebProvider implements Provider {
8484
defaultValue: boolean,
8585
context: EvaluationContext,
8686
): ResolutionDetails<boolean> {
87-
return this.evaluate(flagKey, 'boolean', context);
87+
return this.evaluate(flagKey, 'boolean', defaultValue, context);
8888
}
8989

9090
public resolveStringEvaluation(
9191
flagKey: string,
9292
defaultValue: string,
9393
context: EvaluationContext,
9494
): ResolutionDetails<string> {
95-
return this.evaluate(flagKey, 'string', context);
95+
return this.evaluate(flagKey, 'string', defaultValue, context);
9696
}
9797

9898
public resolveNumberEvaluation(
9999
flagKey: string,
100100
defaultValue: number,
101101
context: EvaluationContext,
102102
): ResolutionDetails<number> {
103-
return this.evaluate(flagKey, 'number', context);
103+
return this.evaluate(flagKey, 'number', defaultValue, context);
104104
}
105105

106106
public resolveObjectEvaluation<U extends JsonValue>(
107107
flagKey: string,
108108
defaultValue: U,
109109
context: EvaluationContext,
110110
): ResolutionDetails<U> {
111-
const objectValue = this.evaluate(flagKey, 'object', context);
111+
const objectValue = this.evaluate(flagKey, 'object', defaultValue, context);
112112
return objectValue as ResolutionDetails<U>;
113113
}
114114

115115
protected evaluate<T extends PrimitiveTypeName>(
116116
flagKey: string,
117117
flagType: T,
118+
defaultValue: PrimitiveType<T>,
118119
context: EvaluationContext,
119120
): ResolutionDetails<PrimitiveType<T>> {
120121
if (!this._client) {
121122
throw new ProviderNotReadyError('Provider is not initialized');
122123
}
123124

125+
// Make sure that the user-provided `defaultValue` is compatible with `flagType` as there is
126+
// no guarantee that it actually is. (User may bypass type checking or may not use TypeScript at all.)
127+
if (!isType(flagType, defaultValue)) {
128+
throw new TypeMismatchError();
129+
}
130+
131+
const configCatDefaultValue = typeof flagType !== 'object'
132+
? defaultValue as SettingValue
133+
: JSON.stringify(defaultValue);
134+
124135
const { value, ...evaluationData } = this._client
125136
.snapshot()
126-
.getValueDetails(flagKey, undefined, transformContext(context));
137+
.getValueDetails(flagKey, configCatDefaultValue, transformContext(context));
127138

128139
if (this._hasError && !evaluationData.errorMessage && !evaluationData.errorException) {
129140
this._hasError = false;
130141
this.events.emit(ProviderEvents.Ready);
131142
}
132143

133-
if (typeof value === 'undefined') {
134-
throw new FlagNotFoundError();
144+
if (evaluationData.isDefaultValue) {
145+
throw parseError(evaluationData.errorMessage);
135146
}
136147

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

145155
let json: JsonValue;
146156
try {
147-
json = JSON.parse(value);
157+
// In this case we can be sure that `value` is string since `configCatDefaultValue` is string,
158+
// which means that ConfigCat SDK is guaranteed to return a string value.
159+
json = JSON.parse(value as string);
148160
} catch (e) {
149161
throw new ParseError(`Unable to parse "${value}" as JSON`);
150162
}
151163

152-
return toResolutionDetails(flagType, json, evaluationData);
164+
return toResolutionDetails(json as PrimitiveType<T>, evaluationData);
153165
}
154166
}

Diff for: libs/providers/config-cat/src/lib/config-cat-provider.ts

+27-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,56 @@ 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 =
136+
typeof flagType !== 'object' ? (defaultValue as SettingValue) : JSON.stringify(defaultValue);
137+
128138
const { value, ...evaluationData } = await this._client.getValueDetailsAsync(
129139
flagKey,
130-
undefined,
140+
configCatDefaultValue,
131141
transformContext(context),
132142
);
133143

@@ -136,25 +146,26 @@ export class ConfigCatProvider implements Provider {
136146
this.events.emit(ProviderEvents.Ready);
137147
}
138148

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

143153
if (flagType !== 'object') {
144-
return toResolutionDetails(flagType, value, evaluationData);
145-
}
146-
147-
if (!isType('string', value)) {
148-
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/node/#setting-type-mapping
157+
return toResolutionDetails(value as PrimitiveType<T>, evaluationData);
149158
}
150159

151160
let json: JsonValue;
152161
try {
153-
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);
154165
} catch (e) {
155166
throw new ParseError(`Unable to parse "${value}" as JSON`);
156167
}
157168

158-
return toResolutionDetails(flagType, json, evaluationData);
169+
return toResolutionDetails(json as PrimitiveType<T>, evaluationData);
159170
}
160171
}
+46-23
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import { IEvaluationDetails } from 'configcat-js-ssr';
2-
import { ResolutionDetails, ResolutionReason, TypeMismatchError, StandardResolutionReasons } from '@openfeature/core';
2+
import { ResolutionDetails, ResolutionReason, TypeMismatchError, StandardResolutionReasons, JsonValue, OpenFeatureError, ParseError, TargetingKeyMissingError, GeneralError, FlagNotFoundError } from '@openfeature/core';
33

4-
export function toResolutionDetails<T extends PrimitiveTypeName>(
5-
type: T,
6-
value: unknown,
4+
export function toResolutionDetails<T>(
5+
value: T,
76
data: Omit<IEvaluationDetails, 'value'>,
87
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-
8+
): ResolutionDetails<T> {
149
const matchedTargeting = data.matchedTargetingRule;
1510
const matchedPercentage = data.matchedPercentageOption;
1611

@@ -25,19 +20,47 @@ export function toResolutionDetails<T extends PrimitiveTypeName>(
2520
};
2621
}
2722

28-
export type PrimitiveTypeName = 'string' | 'boolean' | 'number' | 'object' | 'undefined';
29-
export type PrimitiveType<T> = T extends 'string'
30-
? string
31-
: T extends 'boolean'
32-
? boolean
33-
: T extends 'number'
34-
? number
35-
: T extends 'object'
36-
? object
37-
: T extends 'undefined'
38-
? undefined
39-
: unknown;
23+
export function parseError(errorMessage: string | undefined): OpenFeatureError {
24+
// Detecting the error type by checking the error message is somewhat awkward
25+
// but ConfigCat SDK doesn't allow a better way at the moment.
26+
// However, there are plans to improve this situation, so let's revise this
27+
// as soon as ConfigCat SDK implements returning error codes.
4028

41-
export function isType<T extends PrimitiveTypeName>(type: T, value: unknown): value is PrimitiveType<T> {
42-
return typeof value !== 'undefined' && typeof value === type;
29+
if (errorMessage) {
30+
if (errorMessage.includes('Config JSON is not present')) {
31+
return new ParseError();
32+
}
33+
if (errorMessage.includes('the key was not found in config JSON')){
34+
return new FlagNotFoundError();
35+
}
36+
if (errorMessage.includes('The type of a setting must match the type of the specified default value')) {
37+
return new TypeMismatchError();
38+
}
39+
}
40+
return new GeneralError();
4341
}
42+
43+
export type PrimitiveTypeName = 'string' | 'boolean' | 'number' | 'object';
44+
export type PrimitiveType<T> =
45+
T extends 'string' ? string
46+
: T extends 'boolean' ? boolean
47+
: T extends 'number' ? number
48+
: T extends 'object' ? JsonValue
49+
: never;
50+
51+
export function isType<T extends PrimitiveTypeName>(type: T, value: unknown): value is PrimitiveType<T> {
52+
switch (type) {
53+
case 'string':
54+
case 'boolean':
55+
case 'number':
56+
return typeof value === type;
57+
case 'object':
58+
return value === null
59+
|| typeof value === 'string'
60+
|| typeof value === 'boolean'
61+
|| typeof value === 'number'
62+
|| Array.isArray(value)
63+
|| typeof value === 'object';
64+
}
65+
return false;
66+
}

0 commit comments

Comments
 (0)