Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions libs/providers/ofrep-web/src/lib/model/in-memory-cache.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { FlagMetadata, FlagValue, ResolutionDetails } from '@openfeature/web-sdk';
import type { ResolutionError } from './resolution-error';
import type { FlagMetadata } from '@openfeature/web-sdk';
import type { EvaluationResponse } from '@openfeature/ofrep-core';

/**
* Cache of flag values from bulk evaluation.
*/
export type FlagCache = { [key: string]: ResolutionDetails<FlagValue> | ResolutionError };
export type FlagCache = { [key: string]: EvaluationResponse };

/**
* Cache of metadata from bulk evaluation.
Expand Down
22 changes: 20 additions & 2 deletions libs/providers/ofrep-web/src/lib/ofrep-web-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { OFREPWebProvider } from './ofrep-web-provider';
import TestLogger from '../../test/test-logger';
import { ClientProviderEvents, ClientProviderStatus, OpenFeature } from '@openfeature/web-sdk';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { server } from '../../../../shared/ofrep-core/src/test/mock-service-worker';
import { ClientProviderEvents, ClientProviderStatus, OpenFeature } from '@openfeature/web-sdk';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { TEST_FLAG_METADATA, TEST_FLAG_SET_METADATA } from '../../../../shared/ofrep-core/src/test/test-constants';

Expand Down Expand Up @@ -159,6 +159,24 @@ describe('OFREPWebProvider', () => {
expect(flag.flagMetadata).toEqual(TEST_FLAG_SET_METADATA);
});

it('should return default value if API does not return a value', async () => {
const flagKey = 'flag-without-value';
const providerName = expect.getState().currentTestName || 'test-provider';
const provider = new OFREPWebProvider({ baseUrl: endpointBaseURL }, new TestLogger());
await OpenFeature.setContext(defaultContext);
await OpenFeature.setProviderAndWait(providerName, provider);
const client = OpenFeature.getClient(providerName);

const flag = client.getNumberDetails(flagKey, 42);
expect(flag).toEqual({
flagKey,
value: 42,
variant: 'emptyVariant',
flagMetadata: TEST_FLAG_METADATA,
reason: 'DISABLED',
});
});

it('should return EvaluationDetails if the flag exists', async () => {
const flagKey = 'bool-flag';
const providerName = expect.getState().currentTestName || 'test-provider';
Expand Down Expand Up @@ -190,7 +208,7 @@ describe('OFREPWebProvider', () => {
flagKey,
value: false,
errorCode: 'PARSE_ERROR',
errorMessage: 'Flag or flag configuration could not be parsed',
errorMessage: 'custom error details',
reason: 'ERROR',
flagMetadata: {},
});
Expand Down
108 changes: 37 additions & 71 deletions libs/providers/ofrep-web/src/lib/ofrep-web-provider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { EvaluationRequest, EvaluationResponse } from '@openfeature/ofrep-core';
import { ErrorMessageMap } from '@openfeature/ofrep-core';
import {
type EvaluationFlagValue,
handleEvaluationError,
isEvaluationFailureResponse,
OFREPApi,
OFREPApiFetchError,
OFREPApiTooManyRequestsError,
OFREPApiUnauthorizedError,
OFREPForbiddenError,
isEvaluationFailureResponse,
isEvaluationSuccessResponse,
toFlagMetadata,
toResolutionDetails,
} from '@openfeature/ofrep-core';
import type {
EvaluationContext,
Expand All @@ -30,18 +34,6 @@ import type { EvaluateFlagsResponse } from './model/evaluate-flags-response';
import { BulkEvaluationStatus } from './model/evaluate-flags-response';
import type { FlagCache, MetadataCache } from './model/in-memory-cache';
import type { OFREPWebProviderOptions } from './model/ofrep-web-provider-options';
import { isResolutionError } from './model/resolution-error';

const ErrorMessageMap: { [key in ErrorCode]: string } = {
[ErrorCode.FLAG_NOT_FOUND]: 'Flag was not found',
[ErrorCode.GENERAL]: 'General error',
[ErrorCode.INVALID_CONTEXT]: 'Context is invalid or could be parsed',
[ErrorCode.PARSE_ERROR]: 'Flag or flag configuration could not be parsed',
[ErrorCode.PROVIDER_FATAL]: 'Provider is in a fatal error state',
[ErrorCode.PROVIDER_NOT_READY]: 'Provider is not yet ready',
[ErrorCode.TARGETING_KEY_MISSING]: 'Targeting key is missing',
[ErrorCode.TYPE_MISMATCH]: 'Flag is not of expected type',
};

export class OFREPWebProvider implements Provider {
DEFAULT_POLL_INTERVAL = 30000;
Expand All @@ -62,7 +54,7 @@ export class OFREPWebProvider implements Provider {
private _pollingInterval: number;
private _retryPollingAfter: Date | undefined;
private _flagCache: FlagCache = {};
private _flagSetMetadataCache: MetadataCache = {};
private _flagSetMetadataCache?: MetadataCache = {};
private _context: EvaluationContext | undefined;
private _pollingIntervalId?: number;

Expand Down Expand Up @@ -109,28 +101,28 @@ export class OFREPWebProvider implements Provider {
defaultValue: boolean,
context: EvaluationContext,
): ResolutionDetails<boolean> {
return this._resolve(flagKey, 'boolean', defaultValue);
return this._resolve(flagKey, defaultValue);
}
resolveStringEvaluation(
flagKey: string,
defaultValue: string,
context: EvaluationContext,
): ResolutionDetails<string> {
return this._resolve(flagKey, 'string', defaultValue);
return this._resolve(flagKey, defaultValue);
}
resolveNumberEvaluation(
flagKey: string,
defaultValue: number,
context: EvaluationContext,
): ResolutionDetails<number> {
return this._resolve(flagKey, 'number', defaultValue);
return this._resolve(flagKey, defaultValue);
}
resolveObjectEvaluation<T extends JsonValue>(
flagKey: string,
defaultValue: T,
context: EvaluationContext,
): ResolutionDetails<T> {
return this._resolve(flagKey, 'object', defaultValue);
return this._resolve(flagKey, defaultValue);
}

/**
Expand Down Expand Up @@ -204,36 +196,24 @@ export class OFREPWebProvider implements Provider {
}

const bulkSuccessResp = response.value;
const newCache: FlagCache = {};

if ('flags' in bulkSuccessResp && Array.isArray(bulkSuccessResp.flags)) {
bulkSuccessResp.flags.forEach((evalResp: EvaluationResponse) => {
if (isEvaluationFailureResponse(evalResp)) {
newCache[evalResp.key] = {
reason: StandardResolutionReasons.ERROR,
flagMetadata: evalResp.metadata,
errorCode: evalResp.errorCode,
errorDetails: evalResp.errorDetails,
};
}

if (isEvaluationSuccessResponse(evalResp) && evalResp.key) {
newCache[evalResp.key] = {
value: evalResp.value,
variant: evalResp.variant,
reason: evalResp.reason,
flagMetadata: evalResp.metadata,
};
}
});
const listUpdatedFlags = this._getListUpdatedFlags(this._flagCache, newCache);
this._flagCache = newCache;
this._etag = response.httpResponse?.headers.get('etag');
this._flagSetMetadataCache = typeof bulkSuccessResp.metadata === 'object' ? bulkSuccessResp.metadata : {};
return { status: BulkEvaluationStatus.SUCCESS_WITH_CHANGES, flags: listUpdatedFlags };
} else {
if (!('flags' in bulkSuccessResp) || !Array.isArray(bulkSuccessResp.flags)) {
throw new Error('No flags in OFREP bulk evaluation response');
}

const newCache = bulkSuccessResp.flags.reduce<FlagCache>((currentCache, currentResponse) => {
if (currentResponse.key) {
currentCache[currentResponse.key] = currentResponse;
}
return currentCache;
}, {});

const listUpdatedFlags = this._getListUpdatedFlags(this._flagCache, newCache);
this._flagCache = newCache;
this._etag = response.httpResponse?.headers.get('etag');
this._flagSetMetadataCache = toFlagMetadata(
typeof bulkSuccessResp.metadata === 'object' ? bulkSuccessResp.metadata : {},
);
return { status: BulkEvaluationStatus.SUCCESS_WITH_CHANGES, flags: listUpdatedFlags };
} catch (error) {
if (error instanceof OFREPApiTooManyRequestsError && error.retryAfterDate !== null) {
this._retryPollingAfter = error.retryAfterDate;
Expand Down Expand Up @@ -278,7 +258,7 @@ export class OFREPWebProvider implements Provider {
* @param defaultValue - default value
* @private
*/
private _resolve<T extends FlagValue>(flagKey: string, type: string, defaultValue: T): ResolutionDetails<T> {
private _resolve<T extends FlagValue>(flagKey: string, defaultValue: T): ResolutionDetails<T> {
const resolved = this._flagCache[flagKey];

if (!resolved) {
Expand All @@ -291,32 +271,18 @@ export class OFREPWebProvider implements Provider {
};
}

if (isResolutionError(resolved)) {
return {
...resolved,
value: defaultValue,
errorMessage: ErrorMessageMap[resolved.errorCode],
};
}
return this.responseToResolutionDetails(resolved, defaultValue);
}

if (typeof resolved.value !== type) {
return {
value: defaultValue,
flagMetadata: resolved.flagMetadata,
reason: StandardResolutionReasons.ERROR,
errorCode: ErrorCode.TYPE_MISMATCH,
errorMessage: ErrorMessageMap[ErrorCode.TYPE_MISMATCH],
};
private responseToResolutionDetails<T extends EvaluationFlagValue>(
response: EvaluationResponse,
defaultValue: T,
): ResolutionDetails<T> {
if (isEvaluationFailureResponse(response)) {
return handleEvaluationError(response, defaultValue);
}

return {
variant: resolved.variant,
value: resolved.value as T,
flagMetadata: resolved.flagMetadata,
errorCode: resolved.errorCode,
errorMessage: resolved.errorMessage,
reason: resolved.reason,
};
return toResolutionDetails(response, defaultValue);
}

/**
Expand Down
14 changes: 13 additions & 1 deletion libs/providers/ofrep/src/lib/ofrep-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
OFREPApiUnexpectedResponseError,
OFREPForbiddenError,
} from '@openfeature/ofrep-core';
import { ErrorCode, GeneralError, TypeMismatchError } from '@openfeature/server-sdk';
import { ErrorCode, GeneralError } from '@openfeature/server-sdk';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { TEST_FLAG_METADATA } from '../../../../shared/ofrep-core/src/test/test-constants';
// eslint-disable-next-line @nx/enforce-module-boundaries
Expand Down Expand Up @@ -166,6 +166,18 @@ describe('OFREPProvider should', () => {
expect(flag.value).toEqual(true);
});

it('should return default value if API does not return a value', async () => {
const flag = await provider.resolveNumberEvaluation('flag-without-value', 42, {
errors: { disabled: true },
});
expect(flag).toEqual({
value: 42,
variant: 'emptyVariant',
flagMetadata: TEST_FLAG_METADATA,
reason: 'DISABLED',
});
});

it('run successful evaluation of basic boolean flag', async () => {
const flag = await provider.resolveBooleanEvaluation('my-flag', false, {});
expect(flag.value).toEqual(true);
Expand Down
17 changes: 6 additions & 11 deletions libs/providers/ofrep/src/lib/ofrep-provider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { EvaluationFlagValue, OFREPApiEvaluationResult, OFREPProviderBaseOptions } from '@openfeature/ofrep-core';
import { isEvaluationFailureResponse } from '@openfeature/ofrep-core';
import {
OFREPApi,
OFREPApiTooManyRequestsError,
handleEvaluationError,
toResolutionDetails,
} from '@openfeature/ofrep-core';
import type { EvaluationContext, JsonValue, Provider, ResolutionDetails } from '@openfeature/server-sdk';
import { ErrorCode, GeneralError, StandardResolutionReasons } from '@openfeature/server-sdk';
import { GeneralError } from '@openfeature/server-sdk';

export type OFREPProviderOptions = OFREPProviderBaseOptions;

Expand Down Expand Up @@ -91,19 +92,13 @@ export class OFREPProvider implements Provider {
defaultValue: T,
): ResolutionDetails<T> {
if (result.httpStatus !== 200) {
return handleEvaluationError(result, defaultValue);
return handleEvaluationError(result.value, defaultValue);
}

if (typeof result.value.value !== typeof defaultValue) {
return {
value: defaultValue,
reason: StandardResolutionReasons.ERROR,
flagMetadata: result.value.metadata,
errorCode: ErrorCode.TYPE_MISMATCH,
errorMessage: 'Flag is not of expected type',
};
if (isEvaluationFailureResponse(result)) {
return handleEvaluationError(result, defaultValue);
}

return toResolutionDetails(result.value);
return toResolutionDetails(result.value, defaultValue);
}
}
20 changes: 20 additions & 0 deletions libs/shared/ofrep-core/src/lib/api/ofrep-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,16 @@ describe('OFREPApi', () => {
},
},
},
{
key: 'flag-without-value',
metadata: {
booleanKey: true,
numberKey: 1,
stringKey: 'string',
},
reason: 'DISABLED',
variant: 'emptyVariant',
},
],
} satisfies BulkEvaluationSuccessResponse);
});
Expand Down Expand Up @@ -367,6 +377,16 @@ describe('OFREPApi', () => {
},
},
},
{
key: 'flag-without-value',
metadata: {
booleanKey: true,
numberKey: 1,
stringKey: 'string',
},
reason: 'DISABLED',
variant: 'emptyVariant',
},
],
});
});
Expand Down
Loading