Skip to content

Commit 8fac8cb

Browse files
feat: Initial version of the flagd js core (#620)
Signed-off-by: Kavindu Dodanduwa <[email protected]> Signed-off-by: Kavindu Dodanduwa <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent ee41479 commit 8fac8cb

21 files changed

+743
-15
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@
1010
[submodule "libs/providers/flagd-web/spec"]
1111
path = libs/providers/flagd-web/spec
1212
url = https://github.com/open-feature/spec.git
13+
[submodule "libs/shared/flagd-core/flagd-schemas"]
14+
path = libs/shared/flagd-core/flagd-schemas
15+
url = [email protected]:open-feature/flagd-schemas.git

libs/shared/flagd-core/.eslintrc.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"extends": ["../../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
7+
"rules": {}
8+
},
9+
{
10+
"files": ["*.ts", "*.tsx"],
11+
"rules": {}
12+
},
13+
{
14+
"files": ["*.js", "*.jsx"],
15+
"rules": {}
16+
},
17+
{
18+
"files": ["*.json"],
19+
"parser": "jsonc-eslint-parser",
20+
"rules": {
21+
"@nx/dependency-checks": "error"
22+
}
23+
}
24+
]
25+
}

libs/shared/flagd-core/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# flagd-core
2+
3+
flagd-core contain the core logic of flagd [in-process evaluation](https://flagd.dev/architecture/#in-process-evaluation) provider.
4+
This package is intended to be used by concrete implementations of flagd in-process providers.
5+
6+
## Usage
7+
8+
flagd-core wraps a simple flagd feature flag storage and flag evaluation logic.
9+
10+
To use this implementation, instantiate a `FlagdCore` and provide valid flagd flag configurations.
11+
12+
```typescript
13+
const core = new FlagdCore();
14+
core.setConfigurations(FLAG_CONFIGURATION_STRING);
15+
```
16+
17+
Once initialization is complete, use matching flag resolving call.
18+
19+
```typescript
20+
const resolution = core.resolveBooleanEvaluation('myBoolFlag', false, {});
21+
```

libs/shared/flagd-core/flagd-schemas

Submodule flagd-schemas added at 3e46814

libs/shared/flagd-core/jest.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* eslint-disable */
2+
export default {
3+
displayName: 'flagd-core',
4+
preset: '../../../jest.preset.js',
5+
testEnvironment: 'node',
6+
transform: {
7+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
8+
},
9+
moduleFileExtensions: ['ts', 'js', 'html'],
10+
coverageDirectory: '../../../coverage/libs/shared/flagd-core',
11+
};

libs/shared/flagd-core/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "@openfeature/flagd-core",
3+
"version": "0.0.1-alpha",
4+
"scripts": {
5+
"publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi",
6+
"current-version": "echo $npm_package_version"
7+
},
8+
"dependencies": {
9+
"@openfeature/server-sdk": ">=1.6.0",
10+
"ajv": "^8.12.0",
11+
"tslib": "^2.3.0"
12+
}
13+
}

libs/shared/flagd-core/project.json

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"name": "flagd-core",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "libs/shared/flagd-core/src",
5+
"projectType": "library",
6+
"targets": {
7+
"publish": {
8+
"executor": "nx:run-commands",
9+
"options": {
10+
"command": "npm run publish-if-not-exists",
11+
"cwd": "dist/libs/shared/flagd-core"
12+
},
13+
"dependsOn": [
14+
"build"
15+
]
16+
},
17+
"lint": {
18+
"executor": "@nx/linter:eslint",
19+
"outputs": [
20+
"{options.outputFile}"
21+
],
22+
"options": {
23+
"lintFilePatterns": [
24+
"libs/shared/flagd-core/**/*.ts"
25+
]
26+
}
27+
},
28+
"test": {
29+
"executor": "@nx/jest:jest",
30+
"outputs": [
31+
"{workspaceRoot}/coverage/{projectRoot}"
32+
],
33+
"options": {
34+
"jestConfig": "libs/shared/flagd-core/jest.config.ts",
35+
"passWithNoTests": true
36+
},
37+
"configurations": {
38+
"ci": {
39+
"ci": true,
40+
"codeCoverage": true
41+
}
42+
}
43+
},
44+
"package": {
45+
"executor": "@nx/rollup:rollup",
46+
"outputs": [
47+
"{options.outputPath}"
48+
],
49+
"options": {
50+
"project": "libs/shared/flagd-core/package.json",
51+
"outputPath": "dist/libs/shared/flagd-core",
52+
"entryFile": "libs/shared/flagd-core/src/index.ts",
53+
"tsConfig": "libs/shared/flagd-core/tsconfig.lib.json",
54+
"compiler": "tsc",
55+
"generateExportsField": true,
56+
"buildableProjectDepsInPackageJsonType": "dependencies",
57+
"umdName": "flagd-core",
58+
"external": "all",
59+
"format": [
60+
"cjs",
61+
"esm"
62+
],
63+
"assets": [
64+
{
65+
"glob": "package.json",
66+
"input": "./assets",
67+
"output": "./src/"
68+
},
69+
{
70+
"glob": "LICENSE",
71+
"input": "./",
72+
"output": "./"
73+
},
74+
{
75+
"glob": "README.md",
76+
"input": "./libs/shared/flagd-core",
77+
"output": "./"
78+
}
79+
],
80+
"updateBuildableProjectDepsInPackageJson": true
81+
}
82+
}
83+
},
84+
"tags": []
85+
}

libs/shared/flagd-core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './lib/flagd-core';
2+
export * from './lib/feature-flag';
3+
export * from './lib/storage';
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {FeatureFlag, Flag} from './feature-flag';
2+
3+
describe('Flagd flag structure', () => {
4+
it('should be constructed with valid input - boolean', () => {
5+
const input: Flag = {
6+
state: 'ENABLED',
7+
defaultVariant: 'off',
8+
variants: {
9+
on: true,
10+
off: false,
11+
},
12+
targeting: '',
13+
};
14+
15+
const ff = new FeatureFlag(input);
16+
17+
expect(ff).toBeTruthy();
18+
expect(ff.state).toBe('ENABLED');
19+
expect(ff.defaultVariant).toBe('off');
20+
expect(ff.targetingString).toBe('""');
21+
expect(ff.variants.get('on')).toBeTruthy();
22+
expect(ff.variants.get('off')).toBeFalsy();
23+
});
24+
25+
it('should be constructed with valid input - number', () => {
26+
const input = {
27+
state: 'ENABLED',
28+
defaultVariant: 'one',
29+
variants: {
30+
one: 1.0,
31+
two: 2.0,
32+
},
33+
targeting: '',
34+
};
35+
36+
const ff = new FeatureFlag(input);
37+
38+
expect(ff).toBeTruthy();
39+
expect(ff.state).toBe('ENABLED');
40+
expect(ff.defaultVariant).toBe('one');
41+
expect(ff.targetingString).toBe('""');
42+
expect(ff.variants.get('one')).toBe(1.0);
43+
expect(ff.variants.get('two')).toBe(2.0);
44+
});
45+
46+
it('should be constructed with valid input - object', () => {
47+
const input = {
48+
state: 'ENABLED',
49+
defaultVariant: 'pi2',
50+
variants: {
51+
pi2: {
52+
value: 3.14,
53+
accuracy: 2,
54+
},
55+
pi5: {
56+
value: 3.14159,
57+
accuracy: 5,
58+
},
59+
},
60+
targeting: '',
61+
};
62+
63+
const ff = new FeatureFlag(input);
64+
65+
expect(ff).toBeTruthy();
66+
expect(ff.state).toBe('ENABLED');
67+
expect(ff.defaultVariant).toBe('pi2');
68+
expect(ff.targetingString).toBe('""');
69+
expect(ff.variants.get('pi2')).toStrictEqual({ value: 3.14, accuracy: 2 });
70+
expect(ff.variants.get('pi5')).toStrictEqual({ value: 3.14159, accuracy: 5 });
71+
});
72+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {FlagValue} from '@openfeature/server-sdk';
2+
3+
/**
4+
* Flagd flag configuration structure mapping to schema definition.
5+
*/
6+
export interface Flag {
7+
state: string,
8+
defaultVariant: string,
9+
variants: {[key: string]: FlagValue},
10+
targeting: string
11+
}
12+
13+
/**
14+
* Flagd flag configuration structure for internal reference.
15+
*/
16+
export class FeatureFlag {
17+
private readonly _state: string;
18+
private readonly _defaultVariant: string;
19+
private readonly _variants: Map<string, FlagValue>;
20+
private readonly _targetingString: string;
21+
22+
constructor(flag: Flag) {
23+
this._state = flag['state'];
24+
this._defaultVariant = flag['defaultVariant'];
25+
this._variants = new Map<string, FlagValue>(Object.entries(flag['variants']));
26+
this._targetingString = JSON.stringify(flag['targeting']);
27+
}
28+
29+
get state(): string {
30+
return this._state;
31+
}
32+
33+
get defaultVariant(): string {
34+
return this._defaultVariant;
35+
}
36+
37+
get targetingString(): string {
38+
return this._targetingString;
39+
}
40+
41+
get variants(): Map<string, FlagValue> {
42+
return this._variants
43+
}
44+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {FlagdCore} from './flagd-core';
2+
import {StandardResolutionReasons, TypeMismatchError} from '@openfeature/server-sdk';
3+
4+
const flagCfg = `{"flags":{"myBoolFlag":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"myStringFlag":{"state":"ENABLED","variants":{"key1":"val1","key2":"val2"},"defaultVariant":"key1"},"myFloatFlag":{"state":"ENABLED","variants":{"one":1.23,"two":2.34},"defaultVariant":"one"},"myIntFlag":{"state":"ENABLED","variants":{"one":1,"two":2},"defaultVariant":"one"},"myObjectFlag":{"state":"ENABLED","variants":{"object1":{"key":"val"},"object2":{"key":true}},"defaultVariant":"object1"},"fibAlgo":{"variants":{"recursive":"recursive","memo":"memo","loop":"loop","binet":"binet"},"defaultVariant":"recursive","state":"ENABLED","targeting":{"if":[{"$ref":"emailWithFaas"},"binet",null]}},"targetedFlag":{"variants":{"first":"AAA","second":"BBB","third":"CCC"},"defaultVariant":"first","state":"ENABLED","targeting":{"if":[{"in":["@openfeature.dev",{"var":"email"}]},"second",{"in":["Chrome",{"var":"userAgent"}]},"third",null]}}},"$evaluators":{"emailWithFaas":{"in":["@faas.com",{"var":["email"]}]}}}`;
5+
6+
describe('flagdJsCore resolving', () => {
7+
let core: FlagdCore;
8+
9+
beforeAll(() => {
10+
core = new FlagdCore();
11+
core.setConfigurations(flagCfg);
12+
});
13+
14+
it('should resolve boolean flag', () => {
15+
const resolved = core.resolveBooleanEvaluation('myBoolFlag', false, {});
16+
expect(resolved.value).toBeTruthy();
17+
expect(resolved.reason).toBe(StandardResolutionReasons.STATIC);
18+
expect(resolved.variant).toBe("on")
19+
});
20+
21+
it('should resolve string flag', () => {
22+
const resolved = core.resolveStringEvaluation('myStringFlag', 'key2', {});
23+
expect(resolved.value).toBe('val1');
24+
expect(resolved.reason).toBe(StandardResolutionReasons.STATIC);
25+
expect(resolved.variant).toBe("key1")
26+
});
27+
28+
it('should resolve number flag', () => {
29+
const resolved = core.resolveNumberEvaluation('myFloatFlag', 2.34, {});
30+
expect(resolved.value).toBe(1.23);
31+
expect(resolved.reason).toBe(StandardResolutionReasons.STATIC);
32+
expect(resolved.variant).toBe("one")
33+
});
34+
35+
it('should resolve object flag', () => {
36+
const resolved = core.resolveObjectEvaluation('myObjectFlag', {key: true}, {});
37+
expect(resolved.value).toStrictEqual({key: 'val'});
38+
expect(resolved.reason).toBe(StandardResolutionReasons.STATIC);
39+
expect(resolved.variant).toBe("object1")
40+
});
41+
42+
});
43+
44+
describe('flagdJsCore validations', () => {
45+
// flags of disabled, invalid variants and missing variant
46+
const mixFlags =
47+
'{"flags":{"myBoolFlag":{"state":"DISABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"myStringFlag":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"myIntFlag":{"state":"ENABLED","variants":{"two":2},"defaultVariant":"one"}}}';
48+
let core: FlagdCore;
49+
50+
beforeAll(() => {
51+
core = new FlagdCore();
52+
core.setConfigurations(mixFlags);
53+
});
54+
55+
it('should validate flag type - eval int as boolean', () => {
56+
expect(() => core.resolveBooleanEvaluation('myIntFlag', true, {}))
57+
.toThrow(TypeMismatchError)
58+
});
59+
60+
it('should validate flag status', () => {
61+
const evaluation = core.resolveBooleanEvaluation('myBoolFlag', false, {});
62+
63+
expect(evaluation).toBeTruthy()
64+
expect(evaluation.value).toBe(false)
65+
expect(evaluation.reason).toBe(StandardResolutionReasons.DISABLED)
66+
});
67+
68+
it('should validate variant', () => {
69+
expect(() => core.resolveStringEvaluation('myStringFlag', 'hello', {}))
70+
.toThrow(TypeMismatchError)
71+
});
72+
73+
it('should validate variant existence', () => {
74+
expect(() => core.resolveNumberEvaluation('myIntFlag', 100, {}))
75+
.toThrow(TypeMismatchError)
76+
});
77+
});

0 commit comments

Comments
 (0)