Implement new typed interface
This commit is contained in:
parent
df0904d3eb
commit
be8a5de12e
|
|
@ -5,10 +5,10 @@
|
|||
###############################################
|
||||
|
||||
# Options: true, false
|
||||
new-welcome-message=false
|
||||
NEW_WELCOME_MESSAGE=false
|
||||
|
||||
# Options: recursive, memo, loop, binet, default
|
||||
fib-algo=default
|
||||
FIB_ALGO=default
|
||||
|
||||
###############################################
|
||||
##
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -17,6 +17,7 @@
|
|||
"@opentelemetry/exporter-zipkin": "~1.0.0",
|
||||
"@opentelemetry/sdk-node": "^0.27.0",
|
||||
"@opentelemetry/sdk-trace-node": "~1.0.0",
|
||||
"change-case": "^4.1.2",
|
||||
"express": "4.17.2",
|
||||
"express-validator": "^6.14.0",
|
||||
"launchdarkly-node-server-sdk": "^6.4.0",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const app = express();
|
|||
const oFeatClient = openfeature.getClient('api');
|
||||
|
||||
app.get('/api', async (req, res) => {
|
||||
const message = (await oFeatClient.getVariation('new-welcome-message')).boolValue
|
||||
const message = await oFeatClient.getBooleanValue('new-welcome-message', false)
|
||||
? 'Welcome to the next gen api!'
|
||||
: 'Welcome to the api!';
|
||||
res.send({ message });
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@
|
|||
"sourceRoot": "packages/fibonacci/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/js:tsc",
|
||||
"executor": "@nrwl/node:webpack",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/fibonacci",
|
||||
"main": "packages/fibonacci/src/index.ts",
|
||||
"tsConfig": "packages/fibonacci/tsconfig.lib.json",
|
||||
"assets": ["packages/fibonacci/*.md"]
|
||||
"tsConfig": "packages/fibonacci/tsconfig.lib.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"extractLicenses": true,
|
||||
"inspect": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { openfeature } from '@openfeature/openfeature-js';
|
|||
const oFeatClient = openfeature.getClient('fibonacci');
|
||||
|
||||
export async function fibonacci(num: number): Promise<number> {
|
||||
const variation = await oFeatClient.getVariation('fib-algo');
|
||||
const value = await oFeatClient.getStringValue('fib-algo', 'recursive');
|
||||
/**
|
||||
* TODO: See if variations should return OTel methods that allow developers to
|
||||
* define logs, metrics, and events related to the flag. It could be useful
|
||||
* here to determine the impact the algorithm has on performance.
|
||||
*/
|
||||
switch (variation.stringValue) {
|
||||
switch (value) {
|
||||
case 'recursive':
|
||||
console.log('Running the recursive fibonacci function');
|
||||
return getNthFibRecursive(num);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": []
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@
|
|||
"sourceRoot": "packages/js-cloudbees-provider/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/js:tsc",
|
||||
"executor": "@nrwl/node:webpack",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/js-cloudbees-provider",
|
||||
"main": "packages/js-cloudbees-provider/src/index.ts",
|
||||
"tsConfig": "packages/js-cloudbees-provider/tsconfig.lib.json",
|
||||
"assets": ["packages/js-cloudbees-provider/*.md"]
|
||||
"tsConfig": "packages/js-cloudbees-provider/tsconfig.lib.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"extractLicenses": true,
|
||||
"inspect": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
|
|
|||
|
|
@ -1,47 +1,58 @@
|
|||
import {
|
||||
FeatureProvider,
|
||||
FlagEvaluationRequest,
|
||||
FlagEvaluationVariationResponse,
|
||||
Context,
|
||||
FeatureProvider, parseValidJsonObject
|
||||
} from '@openfeature/openfeature-js';
|
||||
|
||||
import * as Rox from 'rox-node';
|
||||
|
||||
export class CloudbeesProvider implements FeatureProvider {
|
||||
name = 'cloudbees';
|
||||
private initialized: Promise<void>;
|
||||
|
||||
constructor(private readonly appKey: string) {
|
||||
Rox.setup(appKey, {}).then(() => {
|
||||
console.log(`CloudBees Provider initialised: appKey ${appKey}`)
|
||||
constructor(readonly appKey: string) {
|
||||
// we don't expose any init events at the moment (we might later) so for now, lets create a private
|
||||
// promise to await into before we evaluate any flags.
|
||||
this.initialized = new Promise((resolve) => {
|
||||
Rox.setup(appKey, {}).then(() => {
|
||||
console.log(`CloudBees Provider initialized: appKey ${appKey}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async evaluateFlag(
|
||||
request: FlagEvaluationRequest
|
||||
): Promise<FlagEvaluationVariationResponse> {
|
||||
/**
|
||||
* CloudBees Feature management uses different methods to distinguish between different types of flag.
|
||||
* See https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-flags/dynamic-api:
|
||||
* * Boolean flag value: Rox.dynamicApi.isEnabled
|
||||
* * String flag value: Rox.dynamicApi.value
|
||||
* * Number flag value: Rox.dynamicApi.getNumber
|
||||
**/
|
||||
|
||||
/**
|
||||
* CloudBees Feature Management also defines a default value for a flag in code.
|
||||
* This default value is returned by the SDK if the flag is not enabled ('targeting' is off)
|
||||
* See https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-flags/flag-default-values
|
||||
**/
|
||||
// This assumes a boolean flag. Should we include the flag type in the request?
|
||||
const value = Rox.dynamicApi.isEnabled(request.flagId, false, request.context)
|
||||
|
||||
console.log(`${this.name} flag '${request.flagId}' has a value of '${value}'`);
|
||||
|
||||
return {
|
||||
enabled: true, // Cloudbees will return default values if the flag is disabled, so from a caller's perspective it is always enabled
|
||||
// Callers should not care if a flag is enabled or not
|
||||
boolValue: value,
|
||||
// stringValue: value.toString(),
|
||||
// numberValue: value,
|
||||
}
|
||||
|
||||
/**
|
||||
* CloudBees Feature Management also defines a default value for a flag in code.
|
||||
* This default value is returned by the SDK if the flag is not enabled ('targeting' is off)
|
||||
* See https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-flags/flag-default-values
|
||||
**/
|
||||
async isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
// for CloudBees Feature Management, isEnabled is functionally equal to getBooleanValue.
|
||||
return this.getBooleanValue(flagId, defaultValue, context);
|
||||
}
|
||||
}
|
||||
|
||||
async getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
await this.initialized;
|
||||
return Rox.dynamicApi.isEnabled(flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
async getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
|
||||
await this.initialized;
|
||||
return Rox.dynamicApi.value(flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
async getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
|
||||
await this.initialized;
|
||||
return Rox.dynamicApi.getNumber(flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
async getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
|
||||
await this.initialized;
|
||||
|
||||
/**
|
||||
* NOTE: objects are not supported in Cloudbees Feature Management, for demo purposes, we use the string API,
|
||||
* and stringify the default.
|
||||
* This may not be performant, and other, more elegant solutions should be considered.
|
||||
*/
|
||||
const value = Rox.dynamicApi.value(flagId, JSON.stringify(defaultValue), context);
|
||||
return parseValidJsonObject(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": []
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@
|
|||
"sourceRoot": "packages/js-env-provider/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/js:tsc",
|
||||
"executor": "@nrwl/node:webpack",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/js-env-provider",
|
||||
"main": "packages/js-env-provider/src/index.ts",
|
||||
"tsConfig": "packages/js-env-provider/tsconfig.lib.json",
|
||||
"assets": ["packages/js-env-provider/*.md"]
|
||||
"tsConfig": "packages/js-env-provider/tsconfig.lib.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"extractLicenses": true,
|
||||
"inspect": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
|
|
|||
|
|
@ -1,23 +1,64 @@
|
|||
import {
|
||||
FeatureProvider,
|
||||
FlagEvaluationRequest,
|
||||
FlagEvaluationVariationResponse,
|
||||
Context,
|
||||
FeatureProvider, FlagTypeError,
|
||||
FlagValueParseError,
|
||||
parseValidBoolean,
|
||||
parseValidNumber
|
||||
} from '@openfeature/openfeature-js';
|
||||
import { constantCase } from 'change-case';
|
||||
|
||||
export class OpenFeatureEnvProvider implements FeatureProvider {
|
||||
name = 'environment variable';
|
||||
|
||||
async evaluateFlag(
|
||||
request: FlagEvaluationRequest
|
||||
): Promise<FlagEvaluationVariationResponse> {
|
||||
console.log(`${this.name}: evaluation flag`);
|
||||
const flagValue = process.env[request.flagId];
|
||||
name =' environment variable';
|
||||
|
||||
isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
return this.getBooleanValue(flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
console.log(`Flag '${request.flagId}' has a value of '${flagValue}'`);
|
||||
return {
|
||||
enabled: !!flagValue,
|
||||
boolValue:
|
||||
typeof flagValue === 'string' && flagValue.toLowerCase() === 'true',
|
||||
};
|
||||
getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
const stringValue = this.getVarValue(flagId);
|
||||
if (stringValue) {
|
||||
return Promise.resolve(parseValidBoolean(stringValue));
|
||||
} else {
|
||||
throw new FlagTypeError(`Error resolving ${flagId} from environment`);
|
||||
}
|
||||
}
|
||||
|
||||
getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
|
||||
const stringValue = this.getVarValue(flagId);
|
||||
if (stringValue) {
|
||||
return Promise.resolve(stringValue);
|
||||
} else {
|
||||
throw new FlagTypeError(`Error resolving ${flagId} from environment`);
|
||||
}
|
||||
}
|
||||
|
||||
getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
|
||||
const stringValue = this.getVarValue(flagId);
|
||||
if (stringValue) {
|
||||
return Promise.resolve(parseValidNumber(stringValue));
|
||||
} else {
|
||||
throw new FlagTypeError(`Error resolving ${flagId} from environment`);
|
||||
}
|
||||
}
|
||||
|
||||
getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
|
||||
const stringValue = this.getVarValue(flagId);
|
||||
if (stringValue) {
|
||||
try {
|
||||
const parsed = JSON.parse(stringValue);
|
||||
return Promise.resolve(parsed);
|
||||
} catch (err) {
|
||||
throw new FlagValueParseError(`Error parsing ${flagId}`);
|
||||
}
|
||||
} else {
|
||||
throw new FlagTypeError(`Error resolving ${flagId} from environment`);
|
||||
}
|
||||
}
|
||||
|
||||
getVarValue(key: string): string | undefined {
|
||||
// convert key to ENV_VAR style casing
|
||||
const envVarCaseKey = constantCase(key);
|
||||
return process.env[envVarCaseKey];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@
|
|||
"sourceRoot": "packages/js-launchdarkly-provider/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/js:tsc",
|
||||
"executor": "@nrwl/node:webpack",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/js-launchdarkly-provider",
|
||||
"main": "packages/js-launchdarkly-provider/src/index.ts",
|
||||
"tsConfig": "packages/js-launchdarkly-provider/tsconfig.lib.json",
|
||||
"assets": ["packages/js-launchdarkly-provider/*.md"]
|
||||
"tsConfig": "packages/js-launchdarkly-provider/tsconfig.lib.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"extractLicenses": true,
|
||||
"inspect": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import {
|
||||
Context,
|
||||
FeatureProvider,
|
||||
FlagEvaluationRequest,
|
||||
FlagEvaluationVariationResponse,
|
||||
FlagTypeError,
|
||||
FlagValueParseError
|
||||
} from '@openfeature/openfeature-js';
|
||||
import { LDClient, init } from 'launchdarkly-node-server-sdk';
|
||||
import { init, LDClient } from 'launchdarkly-node-server-sdk';
|
||||
|
||||
/**
|
||||
* A comically primitive LaunchDarkly provider demo
|
||||
|
|
@ -11,7 +12,7 @@ import { LDClient, init } from 'launchdarkly-node-server-sdk';
|
|||
export class OpenFeatureLaunchDarklyProvider implements FeatureProvider {
|
||||
name = 'LaunchDarkly';
|
||||
private client: LDClient;
|
||||
private initialized: Promise<boolean>;
|
||||
private initialized: Promise<void>;
|
||||
|
||||
constructor(sdkKey: string) {
|
||||
|
||||
|
|
@ -22,28 +23,76 @@ export class OpenFeatureLaunchDarklyProvider implements FeatureProvider {
|
|||
this.initialized = new Promise((resolve) => {
|
||||
this.client.once('ready', () => {
|
||||
console.log(`${this.name}: initialization complete.`);
|
||||
resolve(true);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
return this.getBooleanValue(flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
async evaluateFlag(
|
||||
request: FlagEvaluationRequest
|
||||
): Promise<FlagEvaluationVariationResponse> {
|
||||
console.log(`${this.name}: evaluation flag`);
|
||||
async getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
const value = await this.evaluateFlag(flagId, defaultValue, context);
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
} else {
|
||||
throw new FlagTypeError(this.getFlagTypeErrorMessage(flagId, value, 'boolean'));
|
||||
}
|
||||
}
|
||||
|
||||
async getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
|
||||
const value = await this.evaluateFlag(flagId, defaultValue, context);
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
} else {
|
||||
throw new FlagTypeError(this.getFlagTypeErrorMessage(flagId, value, 'string'));
|
||||
}
|
||||
}
|
||||
|
||||
async getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
|
||||
const value = await this.evaluateFlag(flagId, defaultValue, context);
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
} else {
|
||||
throw new FlagTypeError(this.getFlagTypeErrorMessage(flagId, value, 'number'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: objects are not supported in Launch Darkly, for demo purposes, we use the string API,
|
||||
* and stringify the default.
|
||||
* This may not be performant, and other, more elegant solutions should be considered.
|
||||
*/
|
||||
async getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context, ): Promise<T> {
|
||||
const value = await this.evaluateFlag(flagId, JSON.stringify(defaultValue), context);
|
||||
if (typeof value === 'string') {
|
||||
// we may want to allow the parsing to be customized.
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (err) {
|
||||
throw new FlagValueParseError(`Error parsing flag value for ${flagId}`);
|
||||
}
|
||||
} else {
|
||||
throw new FlagTypeError(this.getFlagTypeErrorMessage(flagId, value, 'object'));
|
||||
}
|
||||
}
|
||||
|
||||
private getFlagTypeErrorMessage(flagId: string, value: unknown, expectedType: string) {
|
||||
return `Flag value ${flagId} had unexpected type ${typeof value}, expected ${expectedType}.`;
|
||||
}
|
||||
|
||||
// LD values can be boolean, number, or string: https://docs.launchdarkly.com/sdk/client-side/node-js#getting-started
|
||||
private async evaluateFlag(flagId: string, defaultValue: boolean | string | number | object, context?: Context): Promise<boolean | number | string> {
|
||||
// await the initialization before actually calling for a flag.
|
||||
await this.initialized;
|
||||
|
||||
const userKey = request.context.userId ?? 'anonymous';
|
||||
const flagValue = await this.client.variation(request.flagId, { key: userKey}, false);
|
||||
// eventually we'll want a well-defined SDK context object, whose properties will be mapped appropriately to each provider.
|
||||
const userKey = context?.userId ?? 'anonymous';
|
||||
const flagValue = await this.client.variation(flagId, { key: userKey}, defaultValue);
|
||||
|
||||
console.log(`Flag '${request.flagId}' has a value of '${flagValue}'`);
|
||||
return {
|
||||
enabled: !!flagValue,
|
||||
boolValue: !!flagValue,
|
||||
stringValue: flagValue.toString()
|
||||
};
|
||||
console.log(`Flag '${flagId}' has a value of '${flagValue}'`);
|
||||
return flagValue;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@
|
|||
"sourceRoot": "packages/js-split-provider/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/js:tsc",
|
||||
"executor": "@nrwl/node:webpack",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/js-split-provider",
|
||||
"main": "packages/js-split-provider/src/index.ts",
|
||||
"tsConfig": "packages/js-split-provider/tsconfig.lib.json",
|
||||
"assets": ["packages/js-split-provider/*.md"]
|
||||
"tsConfig": "packages/js-split-provider/tsconfig.lib.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"extractLicenses": true,
|
||||
"inspect": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
|
|
|||
|
|
@ -1,37 +1,71 @@
|
|||
import {
|
||||
FeatureProvider,
|
||||
FlagEvaluationRequest,
|
||||
FlagEvaluationVariationResponse,
|
||||
Context,
|
||||
FeatureProvider, FlagTypeError,
|
||||
parseValidJsonObject,
|
||||
parseValidNumber
|
||||
} from '@openfeature/openfeature-js';
|
||||
import type { IClient } from '@splitsoftware/splitio/types/splitio';
|
||||
|
||||
export class OpenFeatureSplitProvider implements FeatureProvider {
|
||||
name = 'split';
|
||||
private initialized: Promise<void>;
|
||||
|
||||
constructor(private readonly client: IClient) {}
|
||||
constructor(private readonly client: IClient) {
|
||||
// we don't expose any init events at the moment (we might later) so for now, lets create a private
|
||||
// promise to await into before we evaluate any flags.
|
||||
this.initialized = new Promise((resolve) => {
|
||||
client.on(client.Event.SDK_READY, () => {
|
||||
console.log(`Split Provider initialized`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
return this.getBooleanValue(flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
async evaluateFlag(
|
||||
request: FlagEvaluationRequest
|
||||
): Promise<FlagEvaluationVariationResponse> {
|
||||
console.log(`${this.name}: evaluation flag`);
|
||||
/**
|
||||
* Split doesn't directly handle booleans as treatment values.
|
||||
* It will be up to the provider author and it's users to come up with conventions for converting strings to booleans.
|
||||
*/
|
||||
async getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
await this.initialized;
|
||||
const stringValue = this.client.getTreatment(context?.userId ?? 'anonymous', flagId);
|
||||
const asUnknown = stringValue as unknown;
|
||||
|
||||
const flagValue = this.client.getTreatment(
|
||||
request.context.userId ?? 'anonymous',
|
||||
request.flagId
|
||||
);
|
||||
switch (asUnknown) {
|
||||
case 'on':
|
||||
return true;
|
||||
case 'off':
|
||||
return false;
|
||||
case 'true':
|
||||
return true;
|
||||
case 'false':
|
||||
return false;
|
||||
case true:
|
||||
return true;
|
||||
case false:
|
||||
return false;
|
||||
default:
|
||||
throw new FlagTypeError(`Invalid boolean value for ${asUnknown}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Flag '${request.flagId}' has a value of '${flagValue}'`);
|
||||
/**
|
||||
* Split uses strings for treatment values. On and off are default but the
|
||||
* values can be changed. "control" is a reserved treatment value and means
|
||||
* something went wrong.
|
||||
*/
|
||||
return {
|
||||
enabled: !!flagValue,
|
||||
boolValue:
|
||||
typeof flagValue === 'string' &&
|
||||
!['off', 'control'].includes(flagValue.toLowerCase()),
|
||||
stringValue: flagValue,
|
||||
};
|
||||
async getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
|
||||
await this.initialized;
|
||||
return this.client.getTreatment(context?.userId ?? 'anonymous', flagId);
|
||||
}
|
||||
|
||||
async getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
|
||||
await this.initialized;
|
||||
const value = this.client.getTreatment(context?.userId ?? 'anonymous', flagId);
|
||||
return parseValidNumber(value);
|
||||
}
|
||||
|
||||
async getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
|
||||
await this.initialized;
|
||||
const value = this.client.getTreatment(context?.userId ?? 'anonymous', flagId);
|
||||
return parseValidJsonObject(value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": []
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
|
||||
|
|
|
|||
|
|
@ -3,13 +3,19 @@
|
|||
"sourceRoot": "packages/openfeature-js/src",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/js:tsc",
|
||||
"executor": "@nrwl/node:webpack",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/openfeature-js",
|
||||
"main": "packages/openfeature-js/src/index.ts",
|
||||
"tsConfig": "packages/openfeature-js/tsconfig.lib.json",
|
||||
"assets": ["packages/openfeature-js/*.md"]
|
||||
"tsConfig": "packages/openfeature-js/tsconfig.lib.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"extractLicenses": true,
|
||||
"inspect": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { OpenFeatureAPI } from './lib/api';
|
||||
export type { OpenFeatureAPI } from './lib/api';
|
||||
export * from "./lib/types";
|
||||
export * from './lib/types';
|
||||
export * from './lib/errors';
|
||||
export * from './lib/utils';
|
||||
|
||||
export const openfeature = OpenFeatureAPI.getInstance();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { OpenFeatureClient } from './client';
|
||||
import { getGlobal, registerGlobal } from './global';
|
||||
import { Feature, FeatureProvider } from './types';
|
||||
import { Features, FeatureProvider } from './types';
|
||||
|
||||
export class OpenFeatureAPI {
|
||||
private provider?: FeatureProvider;
|
||||
|
|
@ -16,7 +16,7 @@ export class OpenFeatureAPI {
|
|||
return instance;
|
||||
}
|
||||
|
||||
public getClient(name: string, version?: string): Feature {
|
||||
public getClient(name?: string, version?: string): Features {
|
||||
return new OpenFeatureClient(this, { name, version });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
import { OpenFeatureAPI } from './api';
|
||||
import {
|
||||
FeatureProvider,
|
||||
Context,
|
||||
FlagEvaluationVariationResponse,
|
||||
Feature,
|
||||
} from './types';
|
||||
import { NOOP_FEATURE_PROVIDER } from './noop-provider';
|
||||
import { Span, trace, Tracer } from '@opentelemetry/api';
|
||||
import { OpenFeatureAPI } from './api';
|
||||
import { NOOP_FEATURE_PROVIDER } from './noop-provider';
|
||||
import { SpanProperties } from './span-properties';
|
||||
import {
|
||||
Context, FeatureProvider, Features, FlagType
|
||||
} from './types';
|
||||
|
||||
type OpenFeatureClientOptions = {
|
||||
name: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export class OpenFeatureClient implements Feature {
|
||||
private _name: string;
|
||||
export class OpenFeatureClient implements Features {
|
||||
private _name: string | undefined;
|
||||
private _version?: string;
|
||||
|
||||
private _trace: Tracer;
|
||||
|
|
@ -29,49 +26,64 @@ export class OpenFeatureClient implements Feature {
|
|||
this._trace = trace.getTracer(OpenFeatureClient.name);
|
||||
}
|
||||
|
||||
// TODO: see if a default callback makes sense here
|
||||
async isEnabled(id: string, context?: Context): Promise<boolean> {
|
||||
return (await this.evaluateFlag('is_enabled', id, context)).enabled;
|
||||
isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
return this.getBooleanValue(flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
async getVariation(
|
||||
id: string,
|
||||
context?: Context
|
||||
): Promise<FlagEvaluationVariationResponse> {
|
||||
return this.evaluateFlag('variation', id, context);
|
||||
getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
return this.evaluateFlag('boolean', flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
private async evaluateFlag(
|
||||
type: 'is_enabled' | 'variation',
|
||||
getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
|
||||
return this.evaluateFlag('string', flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
|
||||
return this.evaluateFlag('number', flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
|
||||
return this.evaluateFlag('json', flagId, defaultValue, context);
|
||||
}
|
||||
|
||||
private async evaluateFlag<T extends boolean | string | number | object>(
|
||||
type: FlagType,
|
||||
id: string,
|
||||
defaultValue: T,
|
||||
context?: Context
|
||||
) {
|
||||
): Promise<T> {
|
||||
const span = this.startSpan(`feature flag - ${type}`);
|
||||
try {
|
||||
span.setAttribute(SpanProperties.FEATURE_FLAG_ID, id);
|
||||
|
||||
const response = await this.getProvider().evaluateFlag({
|
||||
flagId: id,
|
||||
context: context ?? {},
|
||||
clientName: this._name,
|
||||
clientVersion: this._version,
|
||||
});
|
||||
|
||||
span.setAttribute(SpanProperties.FEATURE_FLAG_ENABLED, response.enabled);
|
||||
|
||||
if (response.stringValue) {
|
||||
span.setAttribute(
|
||||
SpanProperties.FEATURE_FLAG_VARIATION_STRING,
|
||||
response.stringValue
|
||||
);
|
||||
const provider = this.getProvider();
|
||||
let valuePromise: boolean | string | number | object;
|
||||
switch(type) {
|
||||
case 'boolean': {
|
||||
valuePromise = provider.getBooleanValue(id, defaultValue as boolean, context);
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
valuePromise = provider.getStringValue(id, defaultValue as string, context);
|
||||
break;
|
||||
}
|
||||
case 'number': {
|
||||
valuePromise = provider.getNumberValue(id, defaultValue as number, context);
|
||||
break;
|
||||
}
|
||||
case 'json': {
|
||||
valuePromise = provider.getObjectValue(id, defaultValue as object, context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
const value = await valuePromise;
|
||||
span.setAttribute(SpanProperties.FEATURE_FLAG_VALUE, value.toString());
|
||||
return value as T;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const enabled = false;
|
||||
span.setAttribute(SpanProperties.FEATURE_FLAG_ENABLED, enabled);
|
||||
return { enabled };
|
||||
span.setAttribute(SpanProperties.FEATURE_FLAG_VALUE, defaultValue.toString());
|
||||
return defaultValue;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
export enum ErrorCodes {
|
||||
GeneralError = 'GENERAL_ERROR',
|
||||
FlagTypeError = 'FLAG_TYPE_ERROR',
|
||||
FlagValueParseError = 'FLAG_VALUE_PARSE_ERROR'
|
||||
};;
|
||||
|
||||
export abstract class OpenFeatureError extends Error {
|
||||
abstract code: ErrorCodes;
|
||||
}
|
||||
|
||||
export class FlagTypeError extends OpenFeatureError {
|
||||
code: ErrorCodes;
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, FlagTypeError.prototype);
|
||||
this.code = ErrorCodes.FlagTypeError;
|
||||
}
|
||||
}
|
||||
|
||||
export class FlagValueParseError extends OpenFeatureError {
|
||||
code: ErrorCodes;
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, FlagTypeError.prototype);
|
||||
this.code = ErrorCodes.FlagValueParseError;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,27 @@
|
|||
import { FeatureProvider, FlagEvaluationResponse } from './types';
|
||||
import { Context, FeatureProvider } from './types';
|
||||
|
||||
class NoopFeatureProvider implements FeatureProvider {
|
||||
name = 'No-op Provider';
|
||||
|
||||
async evaluateFlag(): Promise<FlagEvaluationResponse> {
|
||||
return { enabled: false };
|
||||
constructor() {
|
||||
console.warn(`No provider configured. Falling back to ${this.name}.`);
|
||||
}
|
||||
|
||||
readonly name = 'No-op Provider';
|
||||
|
||||
isEnabled(id: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
return Promise.resolve(defaultValue);
|
||||
}
|
||||
getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
|
||||
return Promise.resolve(defaultValue);
|
||||
}
|
||||
getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
|
||||
return Promise.resolve(defaultValue);
|
||||
}
|
||||
getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
|
||||
return Promise.resolve(defaultValue);
|
||||
}
|
||||
getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
|
||||
return Promise.resolve(defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,5 @@ export const SpanProperties = {
|
|||
FEATURE_FLAG_CLIENT_VERSION: 'feature_flag_client_version',
|
||||
FEATURE_FLAG_SERVICE: 'feature_flag_service',
|
||||
FEATURE_FLAG_ID: 'feature_flag_id',
|
||||
FEATURE_FLAG_ENABLED: 'feature_flag_enabled',
|
||||
FEATURE_FLAG_VARIATION_STRING: 'feature_flag_variation_string',
|
||||
FEATURE_FLAG_VALUE: 'feature_flag_variation_string',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,34 +2,52 @@ export type Context = {
|
|||
userId?: string;
|
||||
};
|
||||
|
||||
export interface Feature {
|
||||
isEnabled(id: string, context?: Context): Promise<boolean>;
|
||||
getVariation(
|
||||
id: string,
|
||||
context?: Context
|
||||
): Promise<FlagEvaluationVariationResponse>;
|
||||
export type FlagType = 'boolean' | 'string' | 'number' | 'json';
|
||||
|
||||
/**
|
||||
* This interface is common to both Providers and the SDK presented to the Application Author.
|
||||
* This is incidental, and may not continue to be the case, especially as additional configuration is provided.
|
||||
*/
|
||||
export interface Features {
|
||||
|
||||
/**
|
||||
* Get a boolean flag value.
|
||||
*
|
||||
* NOTE: In some providers this has distinct behavior from getBooleanValue
|
||||
*/
|
||||
isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get a boolean flag value.
|
||||
*/
|
||||
getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get a string flag value.
|
||||
*/
|
||||
getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string>;
|
||||
|
||||
/**
|
||||
* Get a number flag value.
|
||||
*/
|
||||
getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number>;
|
||||
|
||||
/**
|
||||
* Get a object (JSON) flag value.
|
||||
*/
|
||||
getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T>;
|
||||
}
|
||||
|
||||
export interface FeatureProvider extends Features {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Client extends Features {}
|
||||
|
||||
export type FlagEvaluationRequest = {
|
||||
clientName: string;
|
||||
clientVersion?: string;
|
||||
flagId: string;
|
||||
context: Context;
|
||||
};
|
||||
|
||||
export type FlagEvaluationResponse = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type FlagEvaluationVariationResponse = FlagEvaluationResponse & {
|
||||
stringValue?: string;
|
||||
boolValue?: boolean;
|
||||
numberValue?: number;
|
||||
};
|
||||
|
||||
export interface FeatureProvider {
|
||||
name: string;
|
||||
evaluateFlag(
|
||||
request: FlagEvaluationRequest
|
||||
): Promise<FlagEvaluationVariationResponse>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { FlagTypeError, FlagValueParseError } from './errors';
|
||||
|
||||
export const parseValidNumber = (stringValue: string) => {
|
||||
const result = Number.parseFloat(stringValue);
|
||||
if (Number.isNaN(result)) {
|
||||
throw new FlagTypeError(`Invalid numeric value ${stringValue}`);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const parseValidBoolean = (stringValue: string) => {
|
||||
const asUnknown = stringValue as unknown;
|
||||
|
||||
switch (asUnknown) {
|
||||
case 'true':
|
||||
return true;
|
||||
case 'false':
|
||||
return false;
|
||||
case true:
|
||||
return true;
|
||||
case false:
|
||||
return false;
|
||||
default:
|
||||
throw new FlagTypeError(`Invalid boolean value for ${asUnknown}`)
|
||||
}
|
||||
};
|
||||
|
||||
export const parseValidJsonObject = <T extends object>(stringValue: string): T => {
|
||||
// we may want to allow the parsing to be customized.
|
||||
try {
|
||||
const value = JSON.parse(stringValue);
|
||||
if (typeof value === 'object') {
|
||||
throw new FlagTypeError(`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`);
|
||||
}
|
||||
return value;
|
||||
} catch (err) {
|
||||
throw new FlagValueParseError(`Error parsing ${stringValue} as JSON`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const { openfeature } = require('../dist/packages/openfeature-js/src');
|
||||
const { CloudbeesProvider } = require("../dist/packages/js-cloudbees-provider/src");
|
||||
const { openfeature } = require('../dist/packages/openfeature-js/main');
|
||||
const { CloudbeesProvider } = require("../dist/packages/js-cloudbees-provider/main");
|
||||
|
||||
/**
|
||||
* Registers the environment variable provider to the globally scoped
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const { openfeature } = require('../dist/packages/openfeature-js/src');
|
||||
const { openfeature } = require('../dist/packages/openfeature-js/main');
|
||||
const {
|
||||
OpenFeatureEnvProvider,
|
||||
} = require('../dist/packages/js-env-provider/src');
|
||||
} = require('../dist/packages/js-env-provider/main');
|
||||
|
||||
/**
|
||||
* Registers the environment variable provider to the globally scoped
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const { openfeature } = require('../dist/packages/openfeature-js/src');
|
||||
const { openfeature } = require('../dist/packages/openfeature-js/main');
|
||||
const {
|
||||
OpenFeatureLaunchDarklyProvider,
|
||||
} = require('../dist/packages/js-launchdarkly-provider/src');
|
||||
} = require('../dist/packages/js-launchdarkly-provider/main');
|
||||
|
||||
/**
|
||||
* Registers the LaunchDarkly provider to the globally scoped
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
const { openfeature } = require('../dist/packages/openfeature-js/src');
|
||||
const { openfeature } = require('../dist/packages/openfeature-js/main');
|
||||
const {
|
||||
OpenFeatureSplitProvider,
|
||||
} = require('../dist/packages/js-split-provider/src');
|
||||
} = require('../dist/packages/js-split-provider/main');
|
||||
const { SplitFactory } = require('@splitsoftware/splitio');
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"outDir": "lib",
|
||||
"paths": {
|
||||
"@openfeature/fibonacci": ["packages/fibonacci/src/index.ts"],
|
||||
"@openfeature/js-cloudbees-provider": ["packages/js-cloudbees-provider/src/index.ts"],
|
||||
|
|
@ -22,7 +23,8 @@
|
|||
"@openfeature/js-split-provider": ["packages/js-split-provider/src/index.ts"],
|
||||
"@openfeature/js-launchdarkly-provider": ["packages/js-launchdarkly-provider/src/index.ts"],
|
||||
"@openfeature/openfeature-js": ["packages/openfeature-js/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["node_modules", "tmp"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue