fix: removes exports of OpenFeatureClient class and makes event props readonly (#918)

<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR
<!-- add the description of the PR here -->

Removes OpenFeatureClient class from exports and makes event details
readonly as described here:
https://github.com/open-feature/js-sdk/issues/799

Signed-off-by: Lukas Reining <lukas.reining@codecentric.de>
This commit is contained in:
Lukas Reining 2024-05-02 16:36:32 +02:00 committed by GitHub
parent 6dd558ee61
commit e9a25c21cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 63 additions and 50 deletions

View File

@ -1,2 +1 @@
export * from './client'; export * from './client';
export * from './open-feature-client';

View File

@ -8,10 +8,11 @@ import {
objectOrUndefined, objectOrUndefined,
stringOrUndefined, stringOrUndefined,
} from '@openfeature/core'; } from '@openfeature/core';
import { Client, OpenFeatureClient } from './client'; import { Client } from './client';
import { OpenFeatureEventEmitter, ProviderEvents } from './events'; import { OpenFeatureEventEmitter, ProviderEvents } from './events';
import { Hook } from './hooks'; import { Hook } from './hooks';
import { NOOP_PROVIDER, Provider, ProviderStatus } from './provider'; import { NOOP_PROVIDER, Provider, ProviderStatus } from './provider';
import { OpenFeatureClient } from './client/open-feature-client';
// use a symbol as a key for the global singleton // use a symbol as a key for the global singleton
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api'); const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
@ -26,10 +27,17 @@ type DomainRecord = {
const _globalThis = globalThis as OpenFeatureGlobal; const _globalThis = globalThis as OpenFeatureGlobal;
export class OpenFeatureAPI extends OpenFeatureCommonAPI<ClientProviderStatus, Provider, Hook> implements ManageContext<Promise<void>> { export class OpenFeatureAPI
extends OpenFeatureCommonAPI<ClientProviderStatus, Provider, Hook>
implements ManageContext<Promise<void>>
{
protected _statusEnumType: typeof ProviderStatus = ProviderStatus; protected _statusEnumType: typeof ProviderStatus = ProviderStatus;
protected _apiEmitter: GenericEventEmitter<ProviderEvents> = new OpenFeatureEventEmitter(); protected _apiEmitter: GenericEventEmitter<ProviderEvents> = new OpenFeatureEventEmitter();
protected _defaultProvider: ProviderWrapper<Provider, ClientProviderStatus> = new ProviderWrapper(NOOP_PROVIDER, ProviderStatus.NOT_READY, this._statusEnumType); protected _defaultProvider: ProviderWrapper<Provider, ClientProviderStatus> = new ProviderWrapper(
NOOP_PROVIDER,
ProviderStatus.NOT_READY,
this._statusEnumType,
);
protected _domainScopedProviders: Map<string, ProviderWrapper<Provider, ClientProviderStatus>> = new Map(); protected _domainScopedProviders: Map<string, ProviderWrapper<Provider, ClientProviderStatus>> = new Map();
protected _createEventEmitter = () => new OpenFeatureEventEmitter(); protected _createEventEmitter = () => new OpenFeatureEventEmitter();
@ -111,9 +119,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<ClientProviderStatus, P
...unboundProviders, ...unboundProviders,
]; ];
await Promise.all( await Promise.all(
allDomainRecords.map((dm) => allDomainRecords.map((dm) => this.runProviderContextChangeHandler(dm.domain, dm.wrapper, oldContext, context)),
this.runProviderContextChangeHandler(dm.domain, dm.wrapper, oldContext, context),
),
); );
} }
} }
@ -222,7 +228,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<ClientProviderStatus, P
): Promise<void> { ): Promise<void> {
// this should always be set according to the typings, but let's be defensive considering JS // this should always be set according to the typings, but let's be defensive considering JS
const providerName = wrapper.provider?.metadata?.name || 'unnamed-provider'; const providerName = wrapper.provider?.metadata?.name || 'unnamed-provider';
try { try {
if (typeof wrapper.provider.onContextChange === 'function') { if (typeof wrapper.provider.onContextChange === 'function') {
wrapper.incrementPendingContextChanges(); wrapper.incrementPendingContextChanges();

View File

@ -8,13 +8,13 @@ import {
JsonObject, JsonObject,
JsonValue, JsonValue,
OpenFeature, OpenFeature,
OpenFeatureClient,
Provider, Provider,
ProviderFatalError, ProviderFatalError,
ProviderStatus, ProviderStatus,
ResolutionDetails, ResolutionDetails,
StandardResolutionReasons, StandardResolutionReasons,
} from '../src'; } from '../src';
import { OpenFeatureClient } from '../src/client/open-feature-client';
const BOOLEAN_VALUE = true; const BOOLEAN_VALUE = true;
const STRING_VALUE = 'val'; const STRING_VALUE = 'val';
@ -382,7 +382,7 @@ describe('OpenFeatureClient', () => {
// No generic information exists at runtime, but this test has some value in ensuring the generic args still exist in the typings. // No generic information exists at runtime, but this test has some value in ensuring the generic args still exist in the typings.
const client = OpenFeature.getClient(); const client = OpenFeature.getClient();
const details: ResolutionDetails<JsonValue> = client.getObjectDetails('flag', { key: 'value' }); const details: ResolutionDetails<JsonValue> = client.getObjectDetails('flag', { key: 'value' });
expect(details).toBeDefined(); expect(details).toBeDefined();
}); });
}); });
@ -464,45 +464,45 @@ describe('OpenFeatureClient', () => {
expect(details.errorCode).toEqual(ErrorCode.PROVIDER_NOT_READY); expect(details.errorCode).toEqual(ErrorCode.PROVIDER_NOT_READY);
}); });
}); });
describe('Evaluation details structure', () => { describe('Evaluation details structure', () => {
const flagKey = 'number-details'; const flagKey = 'number-details';
const defaultValue = 1970; const defaultValue = 1970;
let details: EvaluationDetails<number>; let details: EvaluationDetails<number>;
describe('Normal execution', () => { describe('Normal execution', () => {
beforeEach(() => { beforeEach(() => {
const client = OpenFeature.getClient(); const client = OpenFeature.getClient();
details = client.getNumberDetails(flagKey, defaultValue); details = client.getNumberDetails(flagKey, defaultValue);
expect(details).toBeDefined(); expect(details).toBeDefined();
}); });
describe('Requirement 1.4.2, 1.4.3', () => { describe('Requirement 1.4.2, 1.4.3', () => {
it('should contain flag value', () => { it('should contain flag value', () => {
expect(details.value).toEqual(NUMBER_VALUE); expect(details.value).toEqual(NUMBER_VALUE);
}); });
}); });
describe('Requirement 1.4.4', () => { describe('Requirement 1.4.4', () => {
it('should contain flag key', () => { it('should contain flag key', () => {
expect(details.flagKey).toEqual(flagKey); expect(details.flagKey).toEqual(flagKey);
}); });
}); });
describe('Requirement 1.4.5', () => { describe('Requirement 1.4.5', () => {
it('should contain flag variant', () => { it('should contain flag variant', () => {
expect(details.variant).toEqual(NUMBER_VARIANT); expect(details.variant).toEqual(NUMBER_VARIANT);
}); });
}); });
describe('Requirement 1.4.6', () => { describe('Requirement 1.4.6', () => {
it('should contain reason', () => { it('should contain reason', () => {
expect(details.reason).toEqual(REASON); expect(details.reason).toEqual(REASON);
}); });
}); });
}); });
describe('Abnormal execution', () => { describe('Abnormal execution', () => {
const NON_OPEN_FEATURE_ERROR_MESSAGE = 'A null dereference or something, I dunno.'; const NON_OPEN_FEATURE_ERROR_MESSAGE = 'A null dereference or something, I dunno.';
const OPEN_FEATURE_ERROR_MESSAGE = "This ain't the flag you're looking for."; const OPEN_FEATURE_ERROR_MESSAGE = "This ain't the flag you're looking for.";
@ -522,14 +522,14 @@ describe('OpenFeatureClient', () => {
} as unknown as Provider; } as unknown as Provider;
const defaultNumberValue = 123; const defaultNumberValue = 123;
const defaultStringValue = 'hey!'; const defaultStringValue = 'hey!';
beforeEach(() => { beforeEach(() => {
OpenFeature.setProvider(errorProvider); OpenFeature.setProvider(errorProvider);
client = OpenFeature.getClient(); client = OpenFeature.getClient();
nonOpenFeatureErrorDetails = client.getNumberDetails('some-flag', defaultNumberValue); nonOpenFeatureErrorDetails = client.getNumberDetails('some-flag', defaultNumberValue);
openFeatureErrorDetails = client.getStringDetails('some-flag', defaultStringValue); openFeatureErrorDetails = client.getStringDetails('some-flag', defaultStringValue);
}); });
describe('Requirement 1.4.7', () => { describe('Requirement 1.4.7', () => {
describe('OpenFeatureError', () => { describe('OpenFeatureError', () => {
it('should contain error code', () => { it('should contain error code', () => {
@ -537,7 +537,7 @@ describe('OpenFeatureClient', () => {
expect(openFeatureErrorDetails.errorCode).toEqual(ErrorCode.FLAG_NOT_FOUND); // should get code from thrown OpenFeatureError expect(openFeatureErrorDetails.errorCode).toEqual(ErrorCode.FLAG_NOT_FOUND); // should get code from thrown OpenFeatureError
}); });
}); });
describe('Non-OpenFeatureError', () => { describe('Non-OpenFeatureError', () => {
it('should contain error code', () => { it('should contain error code', () => {
expect(nonOpenFeatureErrorDetails.errorCode).toBeTruthy(); expect(nonOpenFeatureErrorDetails.errorCode).toBeTruthy();
@ -545,30 +545,30 @@ describe('OpenFeatureClient', () => {
}); });
}); });
}); });
describe('Requirement 1.4.8', () => { describe('Requirement 1.4.8', () => {
it('should contain error reason', () => { it('should contain error reason', () => {
expect(nonOpenFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR); expect(nonOpenFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR);
expect(openFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR); expect(openFeatureErrorDetails.reason).toEqual(StandardResolutionReasons.ERROR);
}); });
}); });
describe('Requirement 1.4.9', () => { describe('Requirement 1.4.9', () => {
it('must not throw, must return default', () => { it('must not throw, must return default', () => {
nonOpenFeatureErrorDetails = client.getNumberDetails('some-flag', defaultNumberValue); nonOpenFeatureErrorDetails = client.getNumberDetails('some-flag', defaultNumberValue);
expect(nonOpenFeatureErrorDetails).toBeTruthy(); expect(nonOpenFeatureErrorDetails).toBeTruthy();
expect(nonOpenFeatureErrorDetails.value).toEqual(defaultNumberValue); expect(nonOpenFeatureErrorDetails.value).toEqual(defaultNumberValue);
}); });
}); });
describe('Requirement 1.4.12', () => { describe('Requirement 1.4.12', () => {
describe('OpenFeatureError', () => { describe('OpenFeatureError', () => {
it('should contain "error" message', () => { it('should contain "error" message', () => {
expect(openFeatureErrorDetails.errorMessage).toEqual(OPEN_FEATURE_ERROR_MESSAGE); expect(openFeatureErrorDetails.errorMessage).toEqual(OPEN_FEATURE_ERROR_MESSAGE);
}); });
}); });
describe('Non-OpenFeatureError', () => { describe('Non-OpenFeatureError', () => {
it('should contain "error" message', () => { it('should contain "error" message', () => {
expect(nonOpenFeatureErrorDetails.errorMessage).toEqual(NON_OPEN_FEATURE_ERROR_MESSAGE); expect(nonOpenFeatureErrorDetails.errorMessage).toEqual(NON_OPEN_FEATURE_ERROR_MESSAGE);
@ -576,14 +576,14 @@ describe('OpenFeatureClient', () => {
}); });
}); });
}); });
describe('Requirement 1.4.13, Requirement 1.4.14', () => { describe('Requirement 1.4.13, Requirement 1.4.14', () => {
it('should return immutable `flag metadata` as defined by the provider', () => { it('should return immutable `flag metadata` as defined by the provider', () => {
const flagMetadata = { const flagMetadata = {
url: 'https://test.dev', url: 'https://test.dev',
version: '1', version: '1',
}; };
const flagMetadataProvider = { const flagMetadataProvider = {
metadata: { metadata: {
name: 'flag-metadata', name: 'flag-metadata',
@ -595,14 +595,14 @@ describe('OpenFeatureClient', () => {
}; };
}), }),
} as unknown as Provider; } as unknown as Provider;
OpenFeature.setProvider(flagMetadataProvider); OpenFeature.setProvider(flagMetadataProvider);
const client = OpenFeature.getClient(); const client = OpenFeature.getClient();
const response = client.getBooleanDetails('some-flag', false); const response = client.getBooleanDetails('some-flag', false);
expect(response.flagMetadata).toBe(flagMetadata); expect(response.flagMetadata).toBe(flagMetadata);
expect(Object.isFrozen(response.flagMetadata)).toBeTruthy(); expect(Object.isFrozen(response.flagMetadata)).toBeTruthy();
}); });
it('should return empty `flag metadata` because it was not set by the provider', () => { it('should return empty `flag metadata` because it was not set by the provider', () => {
// The mock provider doesn't contain flag metadata // The mock provider doesn't contain flag metadata
OpenFeature.setProvider(MOCK_PROVIDER); OpenFeature.setProvider(MOCK_PROVIDER);

View File

@ -1,5 +1,6 @@
import { Paradigm } from '@openfeature/core'; import { Paradigm } from '@openfeature/core';
import { OpenFeature, OpenFeatureAPI, OpenFeatureClient, Provider, ProviderStatus } from '../src'; import { OpenFeature, OpenFeatureAPI, Provider, ProviderStatus } from '../src';
import { OpenFeatureClient } from '../src/client/open-feature-client';
const mockProvider = (config?: { initialStatus?: ProviderStatus; runsOn?: Paradigm }) => { const mockProvider = (config?: { initialStatus?: ProviderStatus; runsOn?: Paradigm }) => {
return { return {

View File

@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { getOpenFeatureClientToken, OpenFeatureModule, ServerProviderEvents } from '../src'; import { getOpenFeatureClientToken, OpenFeatureModule, ServerProviderEvents } from '../src';
import { OpenFeature, OpenFeatureClient } from '@openfeature/server-sdk'; import { Client, OpenFeature } from '@openfeature/server-sdk';
import { getOpenFeatureDefaultTestModule } from './fixtures'; import { getOpenFeatureDefaultTestModule } from './fixtures';
describe('OpenFeatureModule', () => { describe('OpenFeatureModule', () => {
@ -31,19 +31,19 @@ describe('OpenFeatureModule', () => {
it('should return the SDKs default provider and not throw', async () => { it('should return the SDKs default provider and not throw', async () => {
expect(() => { expect(() => {
moduleWithoutProvidersRef.get<OpenFeatureClient>(getOpenFeatureClientToken()); moduleWithoutProvidersRef.get<Client>(getOpenFeatureClientToken());
}).not.toThrow(); }).not.toThrow();
}); });
}); });
it('should return the default provider', async () => { it('should return the default provider', async () => {
const client = moduleRef.get<OpenFeatureClient>(getOpenFeatureClientToken()); const client = moduleRef.get<Client>(getOpenFeatureClientToken());
expect(client).toBeDefined(); expect(client).toBeDefined();
expect(await client.getStringValue('testStringFlag', '')).toEqual('expected-string-value-default'); expect(await client.getStringValue('testStringFlag', '')).toEqual('expected-string-value-default');
}); });
it('should inject the client with the given scope', async () => { it('should inject the client with the given scope', async () => {
const client = moduleRef.get<OpenFeatureClient>(getOpenFeatureClientToken('domainScopedClient')); const client = moduleRef.get<Client>(getOpenFeatureClientToken('domainScopedClient'));
expect(client).toBeDefined(); expect(client).toBeDefined();
expect(await client.getStringValue('testStringFlag', '')).toEqual('expected-string-value-scoped'); expect(await client.getStringValue('testStringFlag', '')).toEqual('expected-string-value-scoped');
}); });

View File

@ -1,14 +1,14 @@
import { Controller, Get, Injectable, UseInterceptors } from '@nestjs/common'; import { Controller, Get, Injectable, UseInterceptors } from '@nestjs/common';
import { Observable, map } from 'rxjs'; import { Observable, map } from 'rxjs';
import { BooleanFeatureFlag, ObjectFeatureFlag, NumberFeatureFlag, FeatureClient, StringFeatureFlag } from '../src'; import { BooleanFeatureFlag, ObjectFeatureFlag, NumberFeatureFlag, FeatureClient, StringFeatureFlag } from '../src';
import { OpenFeatureClient, EvaluationDetails, FlagValue } from '@openfeature/server-sdk'; import { Client, EvaluationDetails, FlagValue } from '@openfeature/server-sdk';
import { EvaluationContextInterceptor } from '../src'; import { EvaluationContextInterceptor } from '../src';
@Injectable() @Injectable()
export class OpenFeatureTestService { export class OpenFeatureTestService {
constructor( constructor(
@FeatureClient() public defaultClient: OpenFeatureClient, @FeatureClient() public defaultClient: Client,
@FeatureClient({ domain: 'domainScopedClient' }) public domainScopedClient: OpenFeatureClient, @FeatureClient({ domain: 'domainScopedClient' }) public domainScopedClient: Client,
) {} ) {}
public async serviceMethod(flag: EvaluationDetails<FlagValue>) { public async serviceMethod(flag: EvaluationDetails<FlagValue>) {

View File

@ -1,2 +1 @@
export * from './client'; export * from './client';
export * from './open-feature-client';

View File

@ -6,7 +6,7 @@ import {
objectOrUndefined, objectOrUndefined,
stringOrUndefined, stringOrUndefined,
} from '@openfeature/core'; } from '@openfeature/core';
import { Client, OpenFeatureClient } from './client'; import { Client } from './client';
import { OpenFeatureEventEmitter } from './events'; import { OpenFeatureEventEmitter } from './events';
import { Hook } from './hooks'; import { Hook } from './hooks';
import { NOOP_PROVIDER, Provider, ProviderStatus } from './provider'; import { NOOP_PROVIDER, Provider, ProviderStatus } from './provider';
@ -17,6 +17,7 @@ import {
TransactionContextPropagator, TransactionContextPropagator,
} from './transaction-context'; } from './transaction-context';
import { ServerProviderStatus } from '@openfeature/core'; import { ServerProviderStatus } from '@openfeature/core';
import { OpenFeatureClient } from './client/open-feature-client';
// use a symbol as a key for the global singleton // use a symbol as a key for the global singleton
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js-sdk/api'); const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/js-sdk/api');
@ -28,11 +29,17 @@ const _globalThis = globalThis as OpenFeatureGlobal;
export class OpenFeatureAPI export class OpenFeatureAPI
extends OpenFeatureCommonAPI<ServerProviderStatus, Provider, Hook> extends OpenFeatureCommonAPI<ServerProviderStatus, Provider, Hook>
implements ManageContext<OpenFeatureAPI>, ManageTransactionContextPropagator<OpenFeatureCommonAPI<ServerProviderStatus, Provider>> implements
ManageContext<OpenFeatureAPI>,
ManageTransactionContextPropagator<OpenFeatureCommonAPI<ServerProviderStatus, Provider>>
{ {
protected _statusEnumType: typeof ProviderStatus = ProviderStatus; protected _statusEnumType: typeof ProviderStatus = ProviderStatus;
protected _apiEmitter = new OpenFeatureEventEmitter(); protected _apiEmitter = new OpenFeatureEventEmitter();
protected _defaultProvider: ProviderWrapper<Provider, ServerProviderStatus> = new ProviderWrapper(NOOP_PROVIDER, ProviderStatus.NOT_READY, this._statusEnumType); protected _defaultProvider: ProviderWrapper<Provider, ServerProviderStatus> = new ProviderWrapper(
NOOP_PROVIDER,
ProviderStatus.NOT_READY,
this._statusEnumType,
);
protected _domainScopedProviders: Map<string, ProviderWrapper<Provider, ServerProviderStatus>> = new Map(); protected _domainScopedProviders: Map<string, ProviderWrapper<Provider, ServerProviderStatus>> = new Map();
protected _createEventEmitter = () => new OpenFeatureEventEmitter(); protected _createEventEmitter = () => new OpenFeatureEventEmitter();

View File

@ -9,7 +9,6 @@ import {
JsonObject, JsonObject,
JsonValue, JsonValue,
OpenFeature, OpenFeature,
OpenFeatureClient,
Provider, Provider,
ProviderFatalError, ProviderFatalError,
ProviderStatus, ProviderStatus,
@ -18,6 +17,7 @@ import {
TransactionContext, TransactionContext,
TransactionContextPropagator, TransactionContextPropagator,
} from '../src'; } from '../src';
import { OpenFeatureClient } from '../src/client/open-feature-client';
const BOOLEAN_VALUE = true; const BOOLEAN_VALUE = true;
const STRING_VALUE = 'val'; const STRING_VALUE = 'val';

View File

@ -1,5 +1,6 @@
import { Paradigm } from '@openfeature/core'; import { Paradigm } from '@openfeature/core';
import { OpenFeature, OpenFeatureAPI, OpenFeatureClient, Provider, ProviderStatus } from '../src'; import { OpenFeature, OpenFeatureAPI, Provider, ProviderStatus } from '../src';
import { OpenFeatureClient } from '../src/client/open-feature-client';
const mockProvider = (config?: { initialStatus?: ProviderStatus; runsOn?: Paradigm }) => { const mockProvider = (config?: { initialStatus?: ProviderStatus; runsOn?: Paradigm }) => {
return { return {

View File

@ -6,24 +6,24 @@ export type EventMetadata = {
}; };
export type CommonEventDetails = { export type CommonEventDetails = {
providerName: string; readonly providerName: string;
/** /**
* @deprecated alias of "domain", use domain instead * @deprecated alias of "domain", use domain instead
*/ */
clientName?: string; readonly clientName?: string;
readonly domain?: string; readonly domain?: string;
}; };
type CommonEventProps = { type CommonEventProps = {
message?: string; readonly message?: string;
metadata?: EventMetadata; readonly metadata?: EventMetadata;
}; };
export type ReadyEvent = CommonEventProps; export type ReadyEvent = CommonEventProps;
export type ErrorEvent = CommonEventProps; export type ErrorEvent = CommonEventProps;
export type StaleEvent = CommonEventProps; export type StaleEvent = CommonEventProps;
export type ReconcilingEvent = CommonEventProps & { errorCode: ErrorCode }; export type ReconcilingEvent = CommonEventProps & { readonly errorCode: ErrorCode };
export type ConfigChangeEvent = CommonEventProps & { flagsChanged?: string[] }; export type ConfigChangeEvent = CommonEventProps & { readonly flagsChanged?: string[] };
type ServerEventMap = { type ServerEventMap = {
[ServerProviderEvents.Ready]: ReadyEvent; [ServerProviderEvents.Ready]: ReadyEvent;