@featbit/openfeature-provider-node-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "openfeature-provider-node-server-demo",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "type": "commonjs",
6
+ "scripts": {
7
+ "esm": "tsx src/esm.ts",
8
+ "cjs": "tsx src/commonjs.cjs"
9
+ },
10
+ "author": "",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "@featbit/node-server-sdk": "file:../../../featbit-node-server-sdk",
14
+ "@featbit/openfeature-provider-node-server": "file:../../",
15
+ "@openfeature/server-sdk": "^1.7.5"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.10.1",
19
+ "tsx": "^4.6.2"
20
+ }
21
+ }
@@ -0,0 +1,30 @@
1
+ const fb = require('@featbit/node-server-sdk');
2
+ const { FbProvider } = require("@featbit/openfeature-provider-node-server");
3
+ const { OpenFeature, ProviderEvents } = require("@openfeature/server-sdk");
4
+
5
+ const provider = new FbProvider({ sdkKey: 'j-2pVwU1e0uNg8LR_u27KAL1n1amy42U2P2kDf5acCMA', streamingUri: 'ws://localhost:5100', eventsUri: 'http://localhost:5100' });
6
+ OpenFeature.setProvider(provider);
7
+
8
+ // If you need access to the FbClient, then you can use provider.getClient()
9
+
10
+ // Evaluations before the provider indicates it is ready may get default values with a
11
+ // CLIENT_NOT_READY reason.
12
+ OpenFeature.addHandler(ProviderEvents.Ready, (eventDetails) => {
13
+ console.log(`Changed ${eventDetails.flagsChanged}`);
14
+ });
15
+
16
+
17
+ // The FeatBit provider supports the ProviderEvents.ConfigurationChanged event.
18
+ // The provider will emit this event for any flag key that may have changed (each event will contain
19
+ // a single key in the `flagsChanged` field).
20
+ OpenFeature.addHandler(ProviderEvents.ConfigurationChanged, async (eventDetails) => {
21
+ const client = OpenFeature.getClient();
22
+ const value = await client.getBooleanValue('ff1', false, {targetingKey: 'my-key'});
23
+ console.log({...eventDetails, value});
24
+ });
25
+
26
+ // (async () => {
27
+ // // When the FeatBit provider is closed it will flush the events on the FbClient instance.
28
+ // // This can be useful for short lived processes.
29
+ // await OpenFeature.close();
30
+ // })();
@@ -0,0 +1,34 @@
1
+ import { OpenFeature, ProviderEvents } from '@openfeature/server-sdk';
2
+ import { FbProvider } from '@featbit/openfeature-provider-node-server';
3
+
4
+ const provider = new FbProvider({
5
+ sdkKey: 'j-2pVwU1e0uNg8LR_u27KAL1n1amy42U2P2kDf5acCMA',
6
+ streamingUri: 'ws://localhost:5100',
7
+ eventsUri: 'http://localhost:5100'
8
+ });
9
+
10
+ OpenFeature.setProvider(provider);
11
+
12
+ // If you need access to the FbClient, then you can use provider.getClient()
13
+
14
+ // Evaluations before the provider indicates it is ready may get default values with a
15
+ // CLIENT_NOT_READY reason.
16
+ OpenFeature.addHandler(ProviderEvents.Ready, (eventDetails) => {
17
+ console.log(`Changed ${eventDetails.flagsChanged}`);
18
+ });
19
+
20
+
21
+ // The FeatBit provider supports the ProviderEvents.ConfigurationChanged event.
22
+ // The provider will emit this event for any flag key that may have changed (each event will contain
23
+ // a single key in the `flagsChanged` field).
24
+ OpenFeature.addHandler(ProviderEvents.ConfigurationChanged, async (eventDetails) => {
25
+ const client = OpenFeature.getClient();
26
+ const value = await client.getBooleanValue('ff1', false, {targetingKey: 'my-key'});
27
+ console.log({...eventDetails, value});
28
+ });
29
+
30
+ // (async () => {
31
+ // // When the FeatBit provider is closed it will flush the events on the FbClient instance.
32
+ // // This can be useful for short lived processes.
33
+ // await OpenFeature.close();
34
+ // })();
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ transform: { '^.+\\.ts?$': 'ts-jest' },
3
+ testMatch: ['**/*.test.ts?(x)'],
4
+ testEnvironment: 'node',
5
+ moduleFileExtensions: ['ts', 'js'],
6
+ collectCoverageFrom: ['src/**/*.ts']
7
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@featbit/openfeature-provider-node-server",
3
+ "version": "1.0.0",
4
+ "description": "A OpenFeature provider implementation for the FeatBit node SDK",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "rimraf dist && tsc --build",
9
+ "test": "npx jest --ci",
10
+ "prepublishOnly": "npm run build"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/featbit/openfeature-provider-node-server.git"
15
+ },
16
+ "keywords": [],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "bugs": {
20
+ "url": "https://github.com/featbit/openfeature-provider-node-server/issues"
21
+ },
22
+ "homepage": "https://github.com/featbit/openfeature-provider-node-server#readme",
23
+ "peerDependencies": {
24
+ "@featbit/node-server-sdk": "^1.0.0",
25
+ "@openfeature/server-sdk": "^1.9.0"
26
+ },
27
+ "devDependencies": {
28
+ "@featbit/node-server-sdk": "^1.0.0",
29
+ "@openfeature/server-sdk": "^1.9.0",
30
+ "@types/jest": "^29.5.11",
31
+ "jest": "^29.7.0",
32
+ "rimraf": "^5.0.5",
33
+ "ts-jest": "^29.1.1",
34
+ "typescript": "^5.3.3"
35
+ }
36
+ }
@@ -0,0 +1,203 @@
1
+ import {
2
+ EvaluationContext,
3
+ Hook,
4
+ JsonValue,
5
+ OpenFeatureEventEmitter,
6
+ Provider,
7
+ ProviderEvents,
8
+ ProviderStatus,
9
+ ResolutionDetails,
10
+ } from "@openfeature/server-sdk";
11
+ import {
12
+ BasicLogger,
13
+ FbClientBuilder,
14
+ ILogger,
15
+ IFbClient,
16
+ IOptions
17
+ } from "@featbit/node-server-sdk";
18
+ import SafeLogger from "./SafeLogger";
19
+ import { translateContext } from "./translateContext";
20
+ import { translateResult } from "./translateResult";
21
+
22
+
23
+ /**
24
+ * An OpenFeature provider for the FeatBit SDK for node.
25
+ */
26
+ export class FbProvider implements Provider {
27
+ metadata = {
28
+ name: 'featbit-node-provider',
29
+ };
30
+
31
+ private readonly logger: ILogger;
32
+ private readonly client: IFbClient;
33
+ private readonly clientConstructionError: any;
34
+ private innerStatus: ProviderStatus = ProviderStatus.NOT_READY;
35
+
36
+ public readonly events = new OpenFeatureEventEmitter();
37
+
38
+ /**
39
+ * Construct a {@link FbProvider}.
40
+ * @param options The {@link IOptions} to initialize the FeatBit client instance.
41
+ */
42
+ constructor(options: IOptions) {
43
+ if (options.logger) {
44
+ this.logger = new SafeLogger(options.logger, new BasicLogger({level: 'info'}));
45
+ } else {
46
+ this.logger = new BasicLogger({level: 'info'});
47
+ }
48
+
49
+ try {
50
+ this.client = new FbClientBuilder(options).build();
51
+ this.client.on('update', ({key}: {
52
+ key: string
53
+ }) => this.events.emit(ProviderEvents.ConfigurationChanged, {flagsChanged: [key]}))
54
+ } catch (err) {
55
+ this.clientConstructionError = err;
56
+ this.logger.error(`Encountered unrecoverable initialization error, ${ err }`);
57
+ this.innerStatus = ProviderStatus.ERROR;
58
+ }
59
+ }
60
+
61
+ async initialize(context?: EvaluationContext): Promise<void> {
62
+ if (!this.client) {
63
+ // The client could not be constructed.
64
+ if (this.clientConstructionError) {
65
+ throw this.clientConstructionError;
66
+ }
67
+ throw new Error('Unknown problem encountered during initialization');
68
+ }
69
+ try {
70
+ await this.client.waitForInitialization();
71
+ this.innerStatus = ProviderStatus.READY;
72
+ } catch (e) {
73
+ this.innerStatus = ProviderStatus.ERROR;
74
+ throw e;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Determines the boolean variation of a feature flag for a context, along with information about
80
+ * how it was calculated.
81
+ *
82
+ * If the flag does not evaluate to a boolean value, then the defaultValue will be returned.
83
+ *
84
+ * @param flagKey The unique key of the feature flag.
85
+ * @param defaultValue The default value of the flag, to be used if the value is not available
86
+ * from FeatBit.
87
+ * @param context The context requesting the flag.
88
+ * @returns A promise which will resolve to a ResolutionDetails.
89
+ */
90
+ async resolveBooleanEvaluation(
91
+ flagKey: string,
92
+ defaultValue: boolean,
93
+ context: EvaluationContext
94
+ ): Promise<ResolutionDetails<boolean>> {
95
+ const res = await this.client.boolVariationDetail(
96
+ flagKey,
97
+ translateContext(this.logger, context),
98
+ defaultValue,
99
+ );
100
+
101
+ return Promise.resolve(translateResult(res));
102
+ }
103
+
104
+ /**
105
+ * Determines the string variation of a feature flag for a context, along with information about
106
+ * how it was calculated.
107
+ *
108
+ * If the flag does not evaluate to a string value, then the defaultValue will be returned.
109
+ *
110
+ * @param flagKey The unique key of the feature flag.
111
+ * @param defaultValue The default value of the flag, to be used if the value is not available
112
+ * from FeatBit.
113
+ * @param context The context requesting the flag.
114
+ * @returns A promise which will resolve to a ResolutionDetails.
115
+ */
116
+ async resolveStringEvaluation(
117
+ flagKey: string,
118
+ defaultValue: string,
119
+ context: EvaluationContext
120
+ ): Promise<ResolutionDetails<string>> {
121
+ const res = await this.client.stringVariationDetail(
122
+ flagKey,
123
+ translateContext(this.logger, context),
124
+ defaultValue,
125
+ );
126
+
127
+ return Promise.resolve(translateResult(res));
128
+ }
129
+
130
+ /**
131
+ * Determines the numeric variation of a feature flag for a context, along with information about
132
+ * how it was calculated.
133
+ *
134
+ * If the flag does not evaluate to a numeric value, then the defaultValue will be returned.
135
+ *
136
+ * @param flagKey The unique key of the feature flag.
137
+ * @param defaultValue The default value of the flag, to be used if the value is not available
138
+ * from FeatBit.
139
+ * @param context The context requesting the flag.
140
+ * @returns A promise which will resolve to a ResolutionDetails.
141
+ */
142
+ async resolveNumberEvaluation(
143
+ flagKey: string,
144
+ defaultValue: number,
145
+ context: EvaluationContext
146
+ ): Promise<ResolutionDetails<number>> {
147
+ const res = await this.client.numberVariationDetail(
148
+ flagKey,
149
+ translateContext(this.logger, context),
150
+ defaultValue,
151
+ );
152
+
153
+ return Promise.resolve(translateResult(res));
154
+ }
155
+
156
+ /**
157
+ * Determines the object variation of a feature flag for a context, along with information about
158
+ * how it was calculated.
159
+ *
160
+ * @param flagKey The unique key of the feature flag.
161
+ * @param defaultValue The default value of the flag, to be used if the value is not available
162
+ * from FeatBit.
163
+ * @param context The context requesting the flag.
164
+ * @returns A promise which will resolve to a ResolutionDetails.
165
+ */
166
+ async resolveObjectEvaluation<T extends JsonValue>(
167
+ flagKey: string,
168
+ defaultValue: T,
169
+ context: EvaluationContext
170
+ ): Promise<ResolutionDetails<T>> {
171
+ const res = await this.client.jsonVariationDetail(
172
+ flagKey,
173
+ translateContext(this.logger, context),
174
+ defaultValue,
175
+ );
176
+
177
+ return Promise.resolve(translateResult<T>(res));
178
+ }
179
+
180
+ /**
181
+ * Get the status of the FeatBit provider.
182
+ */
183
+ public get status() {
184
+ return this.innerStatus;
185
+ }
186
+
187
+ get hooks(): Hook[] {
188
+ return [];
189
+ }
190
+
191
+ /**
192
+ * Get the IFbClientWithEvents instance used by this provider.
193
+ *
194
+ * @returns The client for this provider.
195
+ */
196
+ public getClient() {
197
+ return this.client;
198
+ }
199
+
200
+ async onClose(): Promise<void> {
201
+ await this.client.close();
202
+ }
203
+ }
@@ -0,0 +1,67 @@
1
+ import { ILogger } from "@featbit/node-server-sdk";
2
+
3
+ /**
4
+ * Logging levels. Each should correspond to a method on the logger.
5
+ */
6
+ const LEVELS = ['error', 'warn', 'info', 'debug'];
7
+
8
+ /**
9
+ * The safeLogger logic exists because we allow the application to pass in a custom logger, but
10
+ * there is no guarantee that the logger works correctly and if it ever throws exceptions there
11
+ * could be serious consequences (e.g. an uncaught exception within an error event handler, due
12
+ * to the provider trying to log the error, can terminate the application). An exception could
13
+ * result from faulty logic in the logger implementation, or it could be that this is not a logger
14
+ * at all but some other kind of object; the former is handled by a catch block that logs an error
15
+ * message to the provider's default logger, and we can at least partly guard against the latter by
16
+ * checking for the presence of required methods at configuration time.
17
+ */
18
+ export default class SafeLogger implements ILogger {
19
+ private logger: ILogger;
20
+
21
+ private fallback: ILogger;
22
+
23
+ /**
24
+ * Construct a safe logger with the specified logger.
25
+ * @param logger The logger to use.
26
+ * @param fallback A fallback logger to use in case an issue is encountered using
27
+ * the provided logger.
28
+ */
29
+ constructor(logger: ILogger, fallback: ILogger) {
30
+ LEVELS.forEach((level) => {
31
+ if (!logger[level] || typeof logger[level] !== 'function') {
32
+ throw new Error(`Provided logger instance must support logger.${level}(...) method`);
33
+ // Note that the provider normally does not throw exceptions to the application, but that
34
+ // rule does not apply to the constructor which will throw an exception if the parameters
35
+ // are so invalid that we cannot proceed with creating the client. An invalid logger meets
36
+ // those criteria since the SDK calls the logger during nearly all of its operations.
37
+ }
38
+ });
39
+ this.logger = logger;
40
+ this.fallback = fallback;
41
+ }
42
+
43
+ private log(level: 'error' | 'warn' | 'info' | 'debug', args: any[]) {
44
+ try {
45
+ this.logger[level](...args);
46
+ } catch {
47
+ // If all else fails do not break.
48
+ this.fallback[level](...args);
49
+ }
50
+ }
51
+
52
+ error(...args: any[]): void {
53
+ this.log('error', args);
54
+ }
55
+
56
+ warn(...args: any[]): void {
57
+ this.log('warn', args);
58
+ }
59
+
60
+ info(...args: any[]): void {
61
+ this.log('info', args);
62
+ }
63
+
64
+ debug(...args: any[]): void {
65
+ this.log('debug', args);
66
+ }
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { FbProvider } from "./FbProvider";
2
+
3
+
4
+ export {
5
+ FbProvider
6
+ }
@@ -0,0 +1,40 @@
1
+ import { IUser, UserBuilder, IContextProperty, ILogger } from "@featbit/node-server-sdk";
2
+ import { EvaluationContext } from "@openfeature/server-sdk";
3
+
4
+ const builtInKeys = ['key', 'name', 'custom', 'targetingKey'];
5
+
6
+ export function translateContext(logger: ILogger, evalContext: EvaluationContext): IUser {
7
+ const custom: IContextProperty[] = (evalContext.custom || []) as unknown as IContextProperty[];
8
+ const name: string = evalContext.name as string || '';
9
+ const key: string = evalContext.targetingKey || evalContext.key as string || '';
10
+
11
+ if (key === '') {
12
+ logger.error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the "
13
+ + 'type must be a string.');
14
+ }
15
+
16
+ const builder = new UserBuilder(key)
17
+ .name(name);
18
+
19
+ if (custom) {
20
+ if (Array.isArray(custom)) {
21
+ custom.forEach(({ name, value }) => {
22
+ builder.custom(name, value);
23
+ });
24
+ } else if (typeof custom === 'object') {
25
+ Object.entries(custom).forEach(([key, value]) => {
26
+ builder.custom(key, value as any as string);
27
+ });
28
+ }
29
+ }
30
+
31
+ Object.entries(evalContext).forEach(([key, value]) => {
32
+ if (builtInKeys.includes(key)) {
33
+ return;
34
+ }
35
+
36
+ builder.custom(key, value as string);
37
+ });
38
+
39
+ return builder.build();
40
+ }
@@ -0,0 +1,42 @@
1
+ import { ErrorCode, ResolutionDetails, StandardResolutionReasons } from "@openfeature/server-sdk";
2
+ import { ReasonKinds, IEvalDetail } from "@featbit/node-server-sdk";
3
+
4
+ /**
5
+ * Create a ResolutionDetails for an evaluation that produced a type different
6
+ * than the expected type.
7
+ * @param value The default value to populate the ResolutionDetails with.
8
+ * @returns A ResolutionDetails with the default value.
9
+ */
10
+ function errorResult<T>(value: T, errorCode: ErrorCode, errorMessage?: string): ResolutionDetails<T> {
11
+ return {
12
+ value,
13
+ reason: StandardResolutionReasons.ERROR,
14
+ errorCode,
15
+ errorMessage
16
+ };
17
+ }
18
+
19
+ export function translateResult<T>(result: IEvalDetail<T>): ResolutionDetails<T> {
20
+ if (result.kind === ReasonKinds.WrongType) {
21
+ return errorResult(result.value, ErrorCode.TYPE_MISMATCH, result.reason);
22
+ }
23
+
24
+ if (result.kind === ReasonKinds.FlagNotFound) {
25
+ return errorResult(result.value, ErrorCode.FLAG_NOT_FOUND, result.reason);
26
+ }
27
+
28
+ if (result.kind === ReasonKinds.Error) {
29
+ return errorResult(result.value, ErrorCode.GENERAL, result.reason);
30
+ }
31
+
32
+ if (result.kind === ReasonKinds.ClientNotReady) {
33
+ return errorResult(result.value, ErrorCode.PROVIDER_NOT_READY, result.reason);
34
+ }
35
+
36
+ const resolution: ResolutionDetails<T> = {
37
+ value: result.value,
38
+ reason: result.kind,
39
+ };
40
+
41
+ return resolution;
42
+ }