Skip to content

Commit 495bf69

Browse files
feat: add ConfigCat provider open-feature#327 (open-feature#334)
Signed-off-by: Lukas Reining <[email protected]>
1 parent 0797d4e commit 495bf69

19 files changed

+835
-1
lines changed

.release-please-manifest.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"libs/providers/flagd": "0.7.6",
55
"libs/providers/flagd-web": "0.3.4",
66
"libs/providers/env-var": "0.1.1",
7-
"libs/providers/in-memory": "0.1.1"
7+
"libs/providers/in-memory": "0.1.1",
8+
"libs/providers/config-cat": "0.1.0"
89
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
}

libs/providers/config-cat/README.md

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# ConfigCat Provider
2+
3+
This provider is an implementation for [ConfigCat](https://configcat.com) a managed feature flag service.
4+
5+
## Installation
6+
7+
```
8+
$ npm install @openfeature/config-cat-provider
9+
```
10+
11+
## Usage
12+
13+
The ConfigCat provider uses the [ConfigCat Javascript SDK](https://configcat.com/docs/sdk-reference/js/).
14+
15+
It can either be created by passing the ConfigCat SDK options to ```ConfigCatProvider.create``` or injecting a ConfigCat
16+
SDK client into ```ConfigCatProvider.createFromClient```.
17+
18+
The available options can be found in the [ConfigCat Javascript SDK docs](https://configcat.com/docs/sdk-reference/js/).
19+
20+
### Example using the default configuration
21+
22+
```javascript
23+
import { ConfigCatProvider } from '@openfeature/config-cat-provider';
24+
25+
const provider = OpenFeature.setProvider(ConfigCatProvider.create('<sdk_key>'));
26+
OpenFeature.setProvider(provider);
27+
```
28+
29+
### Example using different polling options and a setupHook
30+
31+
```javascript
32+
import { ConfigCatProvider } from '@openfeature/config-cat-provider';
33+
34+
const provider = ConfigCatProvider.create('<sdk_key>', PollingMode.LazyLoad, {
35+
setupHooks: (hooks) => hooks.on('clientReady', () => console.log('Client is ready!')),
36+
});
37+
38+
OpenFeature.setProvider(provider);
39+
```
40+
41+
### Example injecting a client
42+
43+
```javascript
44+
import { ConfigCatProvider } from '@openfeature/config-cat-provider';
45+
import * as configcat from 'configcat-js';
46+
47+
const configCatClient = configcat.getClient("<sdk_key>")
48+
const provider = ConfigCatProvider.createFromClient(configCatClient);
49+
50+
OpenFeature.setProvider(provider);
51+
```
52+
53+
## Evaluation Context
54+
55+
ConfigCat only supports string values in its "evaluation
56+
context", [there known as user](https://configcat.com/docs/advanced/user-object/).
57+
58+
This means that every value is converted to a string. This is trivial for numbers and booleans. Objects and arrays are
59+
converted to JSON strings that can be interpreted in ConfigCat.
60+
61+
ConfigCat has three known attributes, and allows for additional attributes.
62+
The following shows how the attributes are mapped:
63+
64+
| OpenFeature EvaluationContext Field | ConfigCat User Field | Required |
65+
|-------------------------------------|----------------------|----------|
66+
| targetingKey | identifier | yes |
67+
| email | email | no |
68+
| country | country | no |
69+
| _Any Other_ | custom | no |
70+
71+
The following example shows the conversion between an OpenFeature Evaluation Context and the corresponding ConfigCat
72+
User:
73+
74+
#### OpenFeature
75+
76+
```json
77+
{
78+
"targetingKey": "test",
79+
"email": "email",
80+
"country": "country",
81+
"customString": "customString",
82+
"customNumber": 1,
83+
"customBoolean": true,
84+
"customObject": {
85+
"prop1": "1",
86+
"prop2": 2
87+
},
88+
"customArray": [
89+
1,
90+
"2",
91+
false
92+
]
93+
}
94+
```
95+
96+
#### ConfigCat
97+
98+
```json
99+
{
100+
"identifier": "test",
101+
"email": "email",
102+
"country": "country",
103+
"custom": {
104+
"customString": "customString",
105+
"customBoolean": "true",
106+
"customNumber": "1",
107+
"customObject": "{\"prop1\":\"1\",\"prop2\":2}",
108+
"customArray": "[1,\"2\",false]"
109+
}
110+
}
111+
```
112+
113+
## Building
114+
115+
Run `nx package providers-config-cat` to build the library.
116+
117+
## Running unit tests
118+
119+
Run `nx test providers-config-cat` to execute the unit tests via [Jest](https://jestjs.io).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": [["minify", { "builtIns": false }]]
3+
}
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* eslint-disable */
2+
export default {
3+
displayName: 'providers-config-cat',
4+
preset: '../../../jest.preset.js',
5+
transform: {
6+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
7+
},
8+
moduleFileExtensions: ['ts', 'js', 'html'],
9+
coverageDirectory: '../../../coverage/libs/providers/config-cat',
10+
};
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "@openfeature/config-cat-provider",
3+
"version": "0.1.0",
4+
"type": "commonjs",
5+
"scripts": {
6+
"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",
7+
"current-version": "echo $npm_package_version"
8+
},
9+
"peerDependencies": {
10+
"@openfeature/js-sdk": "^1.0.0",
11+
"configcat-js": "^7.0.0"
12+
}
13+
}
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
{
2+
"name": "providers-config-cat",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "libs/providers/config-cat/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/providers/config-cat"
12+
},
13+
"dependsOn": [
14+
{
15+
"projects": "self",
16+
"target": "package"
17+
}
18+
]
19+
},
20+
"lint": {
21+
"executor": "@nrwl/linter:eslint",
22+
"outputs": ["{options.outputFile}"],
23+
"options": {
24+
"lintFilePatterns": ["libs/providers/config-cat/**/*.ts"]
25+
}
26+
},
27+
"test": {
28+
"executor": "@nrwl/jest:jest",
29+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
30+
"options": {
31+
"jestConfig": "libs/providers/config-cat/jest.config.ts",
32+
"passWithNoTests": true
33+
},
34+
"configurations": {
35+
"ci": {
36+
"ci": true,
37+
"codeCoverage": true
38+
}
39+
}
40+
},
41+
"package": {
42+
"executor": "@nrwl/rollup:rollup",
43+
"outputs": ["{options.outputPath}"],
44+
"options": {
45+
"project": "libs/providers/config-cat/package.json",
46+
"outputPath": "dist/libs/providers/config-cat",
47+
"entryFile": "libs/providers/config-cat/src/index.ts",
48+
"tsConfig": "libs/providers/config-cat/tsconfig.lib.json",
49+
"buildableProjectDepsInPackageJsonType": "dependencies",
50+
"compiler": "tsc",
51+
"generateExportsField": true,
52+
"umdName": "config-cat",
53+
"external": ["typescript"],
54+
"format": ["cjs", "esm"],
55+
"assets": [
56+
{
57+
"glob": "package.json",
58+
"input": "./assets",
59+
"output": "./src/"
60+
},
61+
{
62+
"glob": "LICENSE",
63+
"input": "./",
64+
"output": "./"
65+
},
66+
{
67+
"glob": "README.md",
68+
"input": "./libs/providers/config-cat",
69+
"output": "./"
70+
}
71+
]
72+
}
73+
}
74+
},
75+
"tags": []
76+
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib/config-cat-provider';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { ConfigCatProvider } from './config-cat-provider';
2+
import { ParseError, TypeMismatchError } from '@openfeature/js-sdk';
3+
import {
4+
IConfigCatClient,
5+
getClient,
6+
createFlagOverridesFromMap,
7+
OverrideBehaviour,
8+
createConsoleLogger,
9+
} from 'configcat-js';
10+
import { LogLevel } from 'configcat-common';
11+
12+
describe('ConfigCatProvider', () => {
13+
const targetingKey = "abc";
14+
15+
let client: IConfigCatClient;
16+
let provider: ConfigCatProvider;
17+
18+
const values = {
19+
booleanFalse: false,
20+
booleanTrue: true,
21+
number1: 1,
22+
number2: 2,
23+
stringTest: 'Test',
24+
jsonValid: JSON.stringify({ valid: true }),
25+
jsonInvalid: '{test:123',
26+
jsonPrimitive: JSON.stringify(123),
27+
};
28+
29+
beforeAll(() => {
30+
client = getClient('__key__', undefined, {
31+
logger: createConsoleLogger(LogLevel.Off),
32+
offline: true,
33+
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
34+
});
35+
provider = ConfigCatProvider.createFromClient(client);
36+
});
37+
38+
afterAll(() => {
39+
client.dispose();
40+
});
41+
42+
it('should be an instance of ConfigCatProvider', () => {
43+
expect(provider).toBeInstanceOf(ConfigCatProvider);
44+
});
45+
46+
describe('method resolveBooleanEvaluation', () => {
47+
it('should return default value for missing value', async () => {
48+
const value = await provider.resolveBooleanEvaluation('nonExistent', false, { targetingKey });
49+
expect(value).toHaveProperty('value', false);
50+
});
51+
52+
it('should return right value if key exists', async () => {
53+
const value = await provider.resolveBooleanEvaluation('booleanTrue', false, { targetingKey });
54+
expect(value).toHaveProperty('value', values.booleanTrue);
55+
});
56+
57+
it('should throw TypeMismatchError if type is different than expected', async () => {
58+
await expect(provider.resolveBooleanEvaluation('number1', false, { targetingKey })).rejects.toThrow(
59+
TypeMismatchError
60+
);
61+
});
62+
});
63+
64+
describe('method resolveStringEvaluation', () => {
65+
it('should return default value for missing value', async () => {
66+
const value = await provider.resolveStringEvaluation('nonExistent', 'default', { targetingKey });
67+
expect(value).toHaveProperty('value', 'default');
68+
});
69+
70+
it('should return right value if key exists', async () => {
71+
const value = await provider.resolveStringEvaluation('stringTest', 'default', { targetingKey });
72+
expect(value).toHaveProperty('value', values.stringTest);
73+
});
74+
75+
it('should throw TypeMismatchError if type is different than expected', async () => {
76+
await expect(provider.resolveStringEvaluation('number1', 'default', { targetingKey })).rejects.toThrow(
77+
TypeMismatchError
78+
);
79+
});
80+
});
81+
82+
describe('method resolveNumberEvaluation', () => {
83+
it('should return default value for missing value', async () => {
84+
const value = await provider.resolveNumberEvaluation('nonExistent', 0, { targetingKey });
85+
expect(value).toHaveProperty('value', 0);
86+
});
87+
88+
it('should return right value if key exists', async () => {
89+
const value = await provider.resolveNumberEvaluation('number1', 0, { targetingKey });
90+
expect(value).toHaveProperty('value', values.number1);
91+
});
92+
93+
it('should throw TypeMismatchError if type is different than expected', async () => {
94+
await expect(provider.resolveNumberEvaluation('stringTest', 0, { targetingKey })).rejects.toThrow(
95+
TypeMismatchError
96+
);
97+
});
98+
});
99+
100+
describe('method resolveObjectEvaluation', () => {
101+
it('should return default value for missing value', async () => {
102+
const value = await provider.resolveObjectEvaluation('nonExistent', {}, { targetingKey });
103+
expect(value).toHaveProperty('value', {});
104+
});
105+
106+
it('should return right value if key exists', async () => {
107+
const value = await provider.resolveObjectEvaluation('jsonValid', {}, { targetingKey });
108+
expect(value).toHaveProperty('value', JSON.parse(values.jsonValid));
109+
});
110+
111+
it('should throw ParseError if string is not valid JSON', async () => {
112+
await expect(provider.resolveObjectEvaluation('jsonInvalid', {}, { targetingKey })).rejects.toThrow(ParseError);
113+
});
114+
115+
it('should throw TypeMismatchError if string is only a JSON primitive', async () => {
116+
await expect(provider.resolveObjectEvaluation('jsonPrimitive', {}, { targetingKey })).rejects.toThrow(
117+
TypeMismatchError
118+
);
119+
});
120+
});
121+
});

0 commit comments

Comments
 (0)