Skip to content

Commit f4b1afb

Browse files
committed
feat: adds rocketflag provider
Signed-off-by: JK Gunnink <[email protected]>
1 parent c3c3d01 commit f4b1afb

16 files changed

+493
-2
lines changed

.release-please-manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@
2121
"libs/shared/config-cat-core": "0.1.1",
2222
"libs/providers/unleash-web": "0.1.1",
2323
"libs/providers/growthbook": "0.1.2",
24-
"libs/providers/aws-ssm": "0.1.3"
24+
"libs/providers/aws-ssm": "0.1.3",
25+
"libs/providers/rocketflag": "0.1.0"
2526
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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": [
22+
"error",
23+
{
24+
"ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"]
25+
}
26+
]
27+
}
28+
}
29+
]
30+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# RocketFlag Provider
2+
3+
## Installation
4+
5+
```
6+
$ npm install @openfeature/rocketflag-provider
7+
```
8+
9+
## Building
10+
11+
Run `nx package providers-rocketflag` to build the library.
12+
13+
## Running unit tests
14+
15+
Run `nx test providers-rocketflag` to execute the unit tests via [Jest](https://jestjs.io).
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": [["minify", { "builtIns": false }]]
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default {
2+
displayName: 'RocketFlag',
3+
preset: '../../../jest.preset.js',
4+
transform: {
5+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
6+
},
7+
moduleFileExtensions: ['ts', 'js', 'html'],
8+
coverageDirectory: '../../../coverage/libs/providers/rocketflag',
9+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@openfeature/rocketflag-provider",
3+
"version": "0.0.1",
4+
"dependencies": {
5+
"tslib": "^2.3.0"
6+
},
7+
"main": "./src/index.js",
8+
"typings": "./src/index.d.ts",
9+
"scripts": {
10+
"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",
11+
"current-version": "echo $npm_package_version"
12+
},
13+
"license": "Apache-2.0",
14+
"peerDependencies": {
15+
"@openfeature/web-sdk": "^1.6.0"
16+
}
17+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{
2+
"name": "RocketFlag",
3+
"$schema": "../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "providers/src",
5+
"projectType": "library",
6+
"release": {
7+
"version": {
8+
"generatorOptions": {
9+
"packageRoot": "dist/{projectRoot}",
10+
"currentVersionResolver": "git-tag"
11+
}
12+
}
13+
},
14+
"tags": [],
15+
"targets": {
16+
"nx-release-publish": {
17+
"options": {
18+
"packageRoot": "dist/{projectRoot}"
19+
}
20+
},
21+
"lint": {
22+
"executor": "@nx/eslint:lint"
23+
},
24+
"test": {
25+
"executor": "@nx/jest:jest",
26+
"outputs": [
27+
"{workspaceRoot}/coverage/{projectRoot}"
28+
],
29+
"options": {
30+
"jestConfig": "{projectRoot}/jest.config.ts"
31+
}
32+
},
33+
"package": {
34+
"executor": "@nx/rollup:rollup",
35+
"outputs": [
36+
"{options.outputPath}"
37+
],
38+
"options": {
39+
"project": "libs/providers/rocketflag/package.json",
40+
"outputPath": "dist/libs/providers/rocketflag",
41+
"entryFile": "libs/providers/rocketflag/src/index.ts",
42+
"tsConfig": "libs/providers/rocketflag/tsconfig.lib.json",
43+
"compiler": "tsc",
44+
"generateExportsField": true,
45+
"umdName": "RocketFlag",
46+
"external": "all",
47+
"format": [
48+
"cjs",
49+
"esm"
50+
],
51+
"assets": [
52+
{
53+
"glob": "package.json",
54+
"input": "./assets",
55+
"output": "./src/"
56+
},
57+
{
58+
"glob": "LICENSE",
59+
"input": "./",
60+
"output": "./"
61+
},
62+
{
63+
"glob": "README.md",
64+
"input": "./libs/providers/rocketflag",
65+
"output": "./"
66+
}
67+
]
68+
}
69+
},
70+
"publish": {
71+
"executor": "nx:run-commands",
72+
"options": {
73+
"command": "npm run publish-if-not-exists",
74+
"cwd": "dist/libs/providers/rocketflag"
75+
},
76+
"dependsOn": [
77+
{
78+
"projects": "self",
79+
"target": "package"
80+
}
81+
]
82+
}
83+
}
84+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib/rocketflag-provider';
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { EvaluationContext } from '@openfeature/web-sdk';
2+
import { OpenFeature, StandardResolutionReasons, ErrorCode } from '@openfeature/web-sdk';
3+
import type { FlagStatus, UserContext } from './rocketflag-provider';
4+
import { RocketFlagProvider } from './rocketflag-provider';
5+
6+
// Create a mock RocketFlag client for testing
7+
const mockClient = {
8+
getFlag: jest.fn<Promise<FlagStatus>, [string, UserContext]>(),
9+
};
10+
11+
// Mock Logger
12+
const mockLogger = {
13+
info: jest.fn(),
14+
warn: jest.fn(),
15+
error: jest.fn(),
16+
debug: jest.fn(),
17+
};
18+
19+
describe('RocketFlagProvider', () => {
20+
beforeEach(() => jest.clearAllMocks());
21+
22+
it('should have the correct metadata name', () => {
23+
const provider = new RocketFlagProvider(mockClient);
24+
expect(provider.metadata.name).toBe('RocketFlagProvider');
25+
});
26+
27+
describe('resolveBooleanEvaluation', () => {
28+
it('should return STALE initially, then resolve to the correct value with TARGETING_MATCH', async () => {
29+
const provider = new RocketFlagProvider(mockClient);
30+
const flagKey = 'test-flag-targeting';
31+
const targetingContext: EvaluationContext = { targetingKey: '[email protected]' };
32+
33+
mockClient.getFlag.mockResolvedValue({ enabled: true });
34+
35+
const initialDetails = provider.resolveBooleanEvaluation(flagKey, false, targetingContext, mockLogger);
36+
expect(initialDetails.reason).toBe(StandardResolutionReasons.STALE);
37+
expect(initialDetails.value).toBe(false);
38+
39+
await new Promise((resolve) => setTimeout(resolve, 0));
40+
41+
const finalDetails = provider.resolveBooleanEvaluation(flagKey, false, targetingContext, mockLogger);
42+
43+
expect(finalDetails.value).toBe(true);
44+
expect(finalDetails.reason).toBe(StandardResolutionReasons.TARGETING_MATCH);
45+
expect(mockClient.getFlag).toHaveBeenCalledWith(flagKey, { cohort: '[email protected]' });
46+
expect(mockClient.getFlag).toHaveBeenCalledTimes(2);
47+
});
48+
49+
it('should return STALE initially, then resolve with DEFAULT reason when no targetingKey is provided', async () => {
50+
const provider = new RocketFlagProvider(mockClient);
51+
const flagKey = 'test-flag-default';
52+
53+
mockClient.getFlag.mockResolvedValue({ enabled: true });
54+
55+
const initialDetails = provider.resolveBooleanEvaluation(flagKey, false, {}, mockLogger);
56+
expect(initialDetails.reason).toBe(StandardResolutionReasons.STALE);
57+
58+
await new Promise((resolve) => setTimeout(resolve, 0));
59+
60+
const finalDetails = provider.resolveBooleanEvaluation(flagKey, false, {}, mockLogger);
61+
62+
expect(finalDetails.value).toBe(true);
63+
expect(finalDetails.reason).toBe(StandardResolutionReasons.DEFAULT);
64+
expect(mockClient.getFlag).toHaveBeenCalledWith(flagKey, {});
65+
});
66+
67+
it('should return STALE initially, then resolve with an ERROR if the client rejects', async () => {
68+
const provider = new RocketFlagProvider(mockClient);
69+
OpenFeature.setProvider(provider);
70+
const client = OpenFeature.getClient();
71+
const flagKey = 'test-flag-error';
72+
const errorMessage = 'Network error';
73+
74+
mockClient.getFlag.mockRejectedValue(new Error(errorMessage));
75+
76+
const initialDetails = provider.resolveBooleanEvaluation(flagKey, false, {}, mockLogger);
77+
expect(initialDetails.reason).toBe(StandardResolutionReasons.STALE);
78+
79+
await new Promise((resolve) => setTimeout(resolve, 0));
80+
81+
const finalDetails = client.getBooleanDetails(flagKey, false);
82+
83+
expect(finalDetails.value).toBe(false); // Default value
84+
expect(finalDetails.reason).toBe(StandardResolutionReasons.ERROR);
85+
expect(finalDetails.errorCode).toBe(ErrorCode.GENERAL);
86+
expect(finalDetails.errorMessage).toBe(errorMessage);
87+
});
88+
89+
it('should return from cache on subsequent calls for the same context', () => {
90+
const provider = new RocketFlagProvider(mockClient);
91+
const flagKey = 'cached-flag';
92+
const targetingContext: EvaluationContext = { targetingKey: 'cached-user' };
93+
const cacheKey = JSON.stringify({ flagKey, context: targetingContext });
94+
const cachedDetails = {
95+
value: true,
96+
reason: StandardResolutionReasons.TARGETING_MATCH,
97+
};
98+
99+
// @ts-expect-error - setting private property for test purposes
100+
provider.cache.set(cacheKey, cachedDetails);
101+
102+
const result = provider.resolveBooleanEvaluation(flagKey, false, targetingContext, mockLogger);
103+
104+
expect(result).toEqual(cachedDetails);
105+
expect(mockClient.getFlag).toHaveBeenCalledTimes(1);
106+
});
107+
});
108+
109+
// Tests for other evaluation types to ensure they return TYPE_MISMATCH
110+
describe('Unsupported Evaluations', () => {
111+
const provider = new RocketFlagProvider(mockClient);
112+
113+
it('resolveStringEvaluation should return TYPE_MISMATCH error', () => {
114+
const details = provider.resolveStringEvaluation('flag', 'default');
115+
expect(details.reason).toBe(StandardResolutionReasons.ERROR);
116+
expect(details.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
117+
expect(details.value).toBe('default');
118+
});
119+
120+
it('resolveNumberEvaluation should return TYPE_MISMATCH error', () => {
121+
const details = provider.resolveNumberEvaluation('flag', 123);
122+
expect(details.reason).toBe(StandardResolutionReasons.ERROR);
123+
expect(details.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
124+
expect(details.value).toBe(123);
125+
});
126+
127+
it('resolveObjectEvaluation should return TYPE_MISMATCH error', () => {
128+
const defaultValue = { key: 'value' };
129+
const details = provider.resolveObjectEvaluation('flag', defaultValue);
130+
expect(details.reason).toBe(StandardResolutionReasons.ERROR);
131+
expect(details.errorCode).toBe(ErrorCode.TYPE_MISMATCH);
132+
expect(details.value).toEqual(defaultValue);
133+
});
134+
});
135+
});

0 commit comments

Comments
 (0)