Skip to content

Commit fd92a28

Browse files
committed
feat: pass-type-mismatch-error-in-response-instead-of-throwing
Signed-off-by: wadii <[email protected]>
1 parent e47f16c commit fd92a28

File tree

3 files changed

+96
-43
lines changed

3 files changed

+96
-43
lines changed

libs/providers/flagsmith/src/lib/flagsmith-provider.spec.ts

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
StandardResolutionReasons,
77
ProviderEvents,
88
ProviderStatus,
9+
ErrorCode,
910
} from '@openfeature/server-sdk';
1011
import { Flagsmith, Flags, BaseFlag } from 'flagsmith-nodejs';
1112
import { FlagsmithProviderError } from './exceptions';
@@ -157,11 +158,18 @@ describe('FlagsmithOpenFeatureProvider', () => {
157158
expect(result.reason).toBe(StandardResolutionReasons.DISABLED);
158159
});
159160

160-
it('should throw TypeMismatchError when flag value type does not match requested type', async () => {
161+
it('should return default value with error details when flag value type does not match requested type', async () => {
161162
mockFlags.getFlag.mockReturnValue(mockFlagData.jsonValidFlag);
162-
await expect(
163-
useBooleanConfigProvider.resolveBooleanEvaluation('disabled-flag', false, evaluationContext, loggerMock),
164-
).rejects.toThrow(TypeMismatchError);
163+
const result = await useBooleanConfigProvider.resolveBooleanEvaluation(
164+
'disabled-flag',
165+
false,
166+
evaluationContext,
167+
loggerMock,
168+
);
169+
expect(result.value).toBe(false);
170+
expect(result.reason).toBe(StandardResolutionReasons.ERROR);
171+
expect(result.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
172+
expect(result.errorMessage).toContain('is not of type boolean');
165173
});
166174

167175
describe('useFlagsmithDefaults', () => {
@@ -330,16 +338,23 @@ describe('FlagsmithOpenFeatureProvider', () => {
330338
).rejects.toThrow(FlagsmithProviderError);
331339
});
332340

333-
it('should throw TypeMismatchError when flag value type does not match requested type', async () => {
341+
it('should return default value with error details when flag value type does not match requested type', async () => {
334342
const booleanConfigProvider = new FlagsmithOpenFeatureProvider(mockFlagsmith, {
335343
returnValueForDisabledFlags: false,
336344
useFlagsmithDefaults: false,
337345
useBooleanConfigValue: true,
338346
});
339347
mockFlags.getFlag.mockReturnValue(mockFlagData.stringFlag);
340-
await expect(
341-
booleanConfigProvider.resolveBooleanEvaluation('test-flag', false, evaluationContext, loggerMock),
342-
).rejects.toThrow(TypeMismatchError);
348+
const result = await booleanConfigProvider.resolveBooleanEvaluation(
349+
'test-flag',
350+
false,
351+
evaluationContext,
352+
loggerMock,
353+
);
354+
expect(result.value).toBe(false);
355+
expect(result.reason).toBe(StandardResolutionReasons.ERROR);
356+
expect(result.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
357+
expect(result.errorMessage).toContain('is not of type boolean');
343358
});
344359

345360
it('should return false when flag value is numeric 0', async () => {
@@ -400,15 +415,22 @@ describe('FlagsmithOpenFeatureProvider', () => {
400415
).rejects.toThrow(FlagsmithProviderError);
401416
});
402417

403-
it('should throw TypeMismatchError when flag value is undefined', async () => {
418+
it('should return default value with error details when flag value is undefined', async () => {
404419
mockFlags.getFlag.mockReturnValue({
405420
enabled: true,
406421
value: undefined,
407422
isDefault: false,
408423
} as BaseFlag);
409-
await expect(
410-
defaultProvider.resolveStringEvaluation('test-flag', '', evaluationContext, loggerMock),
411-
).rejects.toThrow(TypeMismatchError);
424+
const result = await defaultProvider.resolveStringEvaluation(
425+
'test-flag',
426+
'default-string',
427+
evaluationContext,
428+
loggerMock,
429+
);
430+
expect(result.value).toBe('default-string');
431+
expect(result.reason).toBe(StandardResolutionReasons.ERROR);
432+
expect(result.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
433+
expect(result.errorMessage).toContain('is not of type string');
412434
});
413435

414436
it('should return a string when flag value is number', async () => {
@@ -434,22 +456,26 @@ describe('FlagsmithOpenFeatureProvider', () => {
434456
expect(result.reason).toBe(StandardResolutionReasons.TARGETING_MATCH);
435457
});
436458

437-
it('should return a number when flag value is a number as string', async () => {
459+
it('should return default value with error details when flag value is not a valid number', async () => {
438460
mockFlags.getFlag.mockReturnValue(mockFlagData.stringFlag);
439-
await expect(
440-
defaultProvider.resolveNumberEvaluation('test-flag', 0, evaluationContext, loggerMock),
441-
).rejects.toThrow(TypeMismatchError);
461+
const result = await defaultProvider.resolveNumberEvaluation('test-flag', 42, evaluationContext, loggerMock);
462+
expect(result.value).toBe(42);
463+
expect(result.reason).toBe(StandardResolutionReasons.ERROR);
464+
expect(result.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
465+
expect(result.errorMessage).toContain('is not of type number');
442466
});
443467

444-
it('should throw TypeMismatchError when flag value is undefined', async () => {
468+
it('should return default value with error details when flag value is not a valid number string', async () => {
445469
mockFlags.getFlag.mockReturnValue({
446470
enabled: true,
447471
value: 'not-a-number',
448472
isDefault: false,
449473
});
450-
await expect(
451-
defaultProvider.resolveNumberEvaluation('test-flag', 0, evaluationContext, loggerMock),
452-
).rejects.toThrow(TypeMismatchError);
474+
const result = await defaultProvider.resolveNumberEvaluation('test-flag', 99, evaluationContext, loggerMock);
475+
expect(result.value).toBe(99);
476+
expect(result.reason).toBe(StandardResolutionReasons.ERROR);
477+
expect(result.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
478+
expect(result.errorMessage).toContain('is not of type number');
453479
});
454480

455481
it('should return a number when flag value is a string with whitespace', async () => {
@@ -472,22 +498,38 @@ describe('FlagsmithOpenFeatureProvider', () => {
472498
expect(result.reason).toBe(StandardResolutionReasons.TARGETING_MATCH);
473499
});
474500

475-
it('should throw TypeMismatchError when flag value is invalid JSON', async () => {
501+
it('should return default value with error details when flag value is invalid JSON', async () => {
476502
mockFlags.getFlag.mockReturnValue(mockFlagData.jsonInvalidFlag);
477-
await expect(
478-
defaultProvider.resolveObjectEvaluation('test-flag', {}, evaluationContext, loggerMock),
479-
).rejects.toThrow(TypeMismatchError);
503+
const defaultObj = { default: true };
504+
const result = await defaultProvider.resolveObjectEvaluation(
505+
'test-flag',
506+
defaultObj,
507+
evaluationContext,
508+
loggerMock,
509+
);
510+
expect(result.value).toEqual(defaultObj);
511+
expect(result.reason).toBe(StandardResolutionReasons.ERROR);
512+
expect(result.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
513+
expect(result.errorMessage).toContain('is not of type object');
480514
});
481515

482-
it('should throw TypeMismatchError when flag value is undefined', async () => {
516+
it('should return default value with error details when flag value is undefined', async () => {
483517
mockFlags.getFlag.mockReturnValue({
484518
enabled: true,
485519
value: undefined,
486520
isDefault: false,
487521
} as BaseFlag);
488-
await expect(
489-
defaultProvider.resolveObjectEvaluation('test-flag', {}, evaluationContext, loggerMock),
490-
).rejects.toThrow(TypeMismatchError);
522+
const defaultObj = { fallback: 'value' };
523+
const result = await defaultProvider.resolveObjectEvaluation(
524+
'test-flag',
525+
defaultObj,
526+
evaluationContext,
527+
loggerMock,
528+
);
529+
expect(result.value).toEqual(defaultObj);
530+
expect(result.reason).toBe(StandardResolutionReasons.ERROR);
531+
expect(result.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
532+
expect(result.errorMessage).toContain('is not of type object');
491533
});
492534
});
493535

@@ -512,16 +554,18 @@ describe('FlagsmithOpenFeatureProvider', () => {
512554
);
513555
});
514556

515-
it('should throw TypeMismatchError when flag value type does not match requested type', async () => {
557+
it('should return default value with error details when flag value type does not match requested type', async () => {
516558
const provider = new FlagsmithOpenFeatureProvider(mockFlagsmith, {
517559
returnValueForDisabledFlags: false,
518560
useFlagsmithDefaults: false,
519561
useBooleanConfigValue: true,
520562
});
521563
mockFlags.getFlag.mockReturnValue(mockFlagData.stringFlag);
522-
await expect(
523-
provider.resolveBooleanEvaluation('test-flag', false, evaluationContext, loggerMock),
524-
).rejects.toThrow(TypeMismatchError);
564+
const result = await provider.resolveBooleanEvaluation('test-flag', false, evaluationContext, loggerMock);
565+
expect(result.value).toBe(false);
566+
expect(result.reason).toBe(StandardResolutionReasons.ERROR);
567+
expect(result.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
568+
expect(result.errorMessage).toContain('is not of type boolean');
525569
});
526570
});
527571
});

libs/providers/flagsmith/src/lib/flagsmith-provider.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
OpenFeatureEventEmitter,
1414
ProviderEvents,
1515
StandardResolutionReasons,
16+
FlagValue,
1617
} from '@openfeature/server-sdk';
1718
import { Flags, Flagsmith, BaseFlag, TraitConfig, FlagsmithValue } from 'flagsmith-nodejs';
1819
import { FlagsmithProviderError } from './exceptions';
@@ -103,6 +104,7 @@ export default class FlagsmithOpenFeatureProvider implements Provider {
103104
flagKey: string,
104105
flagType: FlagValueType,
105106
evaluationContext: EvaluationContext,
107+
defaultValue: FlagValue,
106108
): Promise<ResolutionDetails<any>> {
107109
let flag: BaseFlag;
108110
try {
@@ -130,7 +132,13 @@ export default class FlagsmithOpenFeatureProvider implements Provider {
130132

131133
const typedValue = typeFactory(flag.value, flagType);
132134
if (typedValue === undefined || typeof typedValue !== flagType) {
133-
throw new TypeMismatchError(`flag key ${flagKey} is not of type ${flagType}`);
135+
return {
136+
value: defaultValue,
137+
reason: StandardResolutionReasons.ERROR,
138+
errorCode: ErrorCode.TYPE_MISMATCH,
139+
errorMessage: `Flag value ${flag.value} is not of type ${flagType}`,
140+
flagMetadata: {},
141+
};
134142
}
135143

136144
if (flag?.isDefault) {
@@ -148,38 +156,38 @@ export default class FlagsmithOpenFeatureProvider implements Provider {
148156

149157
async resolveBooleanEvaluation(
150158
flagKey: string,
151-
_: boolean,
159+
defaultValue: boolean,
152160
context: EvaluationContext,
153161
__: Logger,
154162
): Promise<ResolutionDetails<boolean>> {
155-
return this.resolve(flagKey, 'boolean', context);
163+
return this.resolve(flagKey, 'boolean', context, defaultValue);
156164
}
157165

158166
async resolveStringEvaluation(
159167
flagKey: string,
160-
_: string,
168+
defaultValue: string,
161169
context: EvaluationContext,
162170
__: Logger,
163171
): Promise<ResolutionDetails<string>> {
164-
return this.resolve(flagKey, 'string', context);
172+
return this.resolve(flagKey, 'string', context, defaultValue);
165173
}
166174

167175
async resolveNumberEvaluation(
168176
flagKey: string,
169-
_: number,
177+
defaultValue: number,
170178
context: EvaluationContext,
171179
__: Logger,
172180
): Promise<ResolutionDetails<number>> {
173-
return this.resolve(flagKey, 'number', context);
181+
return this.resolve(flagKey, 'number', context, defaultValue);
174182
}
175183

176184
async resolveObjectEvaluation<T extends JsonValue>(
177185
flagKey: string,
178-
_: T,
186+
defaultValue: T,
179187
context: EvaluationContext,
180188
__: Logger,
181189
): Promise<ResolutionDetails<T>> {
182-
return this.resolve(flagKey, 'object', context) as Promise<ResolutionDetails<T>>;
190+
return this.resolve(flagKey, 'object', context, defaultValue) as Promise<ResolutionDetails<T>>;
183191
}
184192

185193
async initialize(context?: EvaluationContext): Promise<void> {

libs/providers/flagsmith/src/lib/type-factory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ const toNumber = (value: unknown): number | undefined => {
1010

1111
const toBoolean = (value: unknown): boolean | undefined => {
1212
if (typeof value === 'boolean') return value;
13-
// I added this one in case true/false is passed as a number: wdyt ?
1413
if (typeof value === 'number') {
15-
return value !== 0;
14+
if (value === 0) return false;
15+
if (value === 1) return true;
16+
return undefined;
1617
}
1718
if (typeof value === 'string') {
1819
const lower = value.toLowerCase();

0 commit comments

Comments
 (0)