Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dependent features #520

Merged
merged 11 commits into from
Sep 21, 2023
35 changes: 32 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,37 @@ export default class UnleashClient extends EventEmitter {
}
}

isParentDependencySatisfied(feature: FeatureInterface | undefined, context: Context) {
sjaanus marked this conversation as resolved.
Show resolved Hide resolved
if (!feature?.name) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can feature not have name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is what the existing type was telling me so I'm respecting it here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feature's should have a name, but at least in this SDK we allow people to define their own bootstrapper so it's possible people put goop in there. This one is probably mostly legacy but respecting the existing types seems very sane to me

return true;
}

if (feature.dependencies && feature.dependencies.length) {
return feature.dependencies.every(parent => {
const parentFeature = this.repository.getToggle(parent.feature);

if (parentFeature?.dependencies?.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make it more readable, Does this mean length exists or length === 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, it should also cater for undefined dependencies. Setting === 0 breaks the spec

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move it to precondition check though

return false;
}

if (parent.enabled !== false && parent.variants?.length) {
return parent.variants.includes(this.getVariant(parent.feature, context).name);
}

return parent.enabled !== false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make it more readable, and not do negate operator, but instead ===

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by default enabled is not provided so it's undefined. enabled: false is a special case for kill-switches in parent flag.This code is explicit about explicit enabled false and everything else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reorganized the conditions a bit but the !== false has to stay as it is

? this.isEnabled(parent.feature, context, () => false)
: !this.isEnabled(parent.feature, context, () => false);
});
}

return true;
}

isEnabled(name: string, context: Context, fallback: Function): boolean {
const feature = this.repository.getToggle(name);
if(!this.isParentDependencySatisfied(feature, context)) {
sighphyre marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
const { enabled } = this.isFeatureEnabled(feature, context, fallback);
if (feature?.impressionData) {
this.emit(
Expand Down Expand Up @@ -92,10 +121,10 @@ export default class UnleashClient extends EventEmitter {
}

if (feature.strategies.length === 0) {
return { enabled: feature.enabled };
return { enabled: feature.enabled } as StrategyResult;
}

let strategyResult = { enabled: false };
let strategyResult: StrategyResult = { enabled: false };

feature.strategies?.some((strategySelector): boolean => {
const strategy = this.getStrategy(strategySelector.name);
Expand Down Expand Up @@ -185,7 +214,7 @@ export default class UnleashClient extends EventEmitter {
): VariantWithFeatureStatus {
const fallback = fallbackVariant || getDefaultVariant();

if (typeof feature === 'undefined') {
if (typeof feature === 'undefined' || !this.isParentDependencySatisfied(feature, context)) {
return { ...fallback, featureEnabled: false };
}

Expand Down
7 changes: 7 additions & 0 deletions src/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { Segment } from './strategy/strategy';
// eslint-disable-next-line import/no-cycle
import { VariantDefinition } from './variant';

export interface Dependency {
feature: string;
variants?: string[];
enabled?: boolean;
}

export interface FeatureInterface {
name: string;
type: string;
Expand All @@ -13,6 +19,7 @@ export interface FeatureInterface {
impressionData: boolean;
strategies: StrategyTransportInterface[];
variants: VariantDefinition[];
dependencies?: Dependency[];
}

export interface ClientFeaturesResponse {
Expand Down