unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
144 lines • 6.4 kB
JavaScript
import { ALL } from '../../types/models/api-token.js';
import { offlineUnleashClient } from './offline-unleash-client.js';
import { generateObjectCombinations } from './generateObjectCombinations.js';
import groupBy from 'lodash.groupby';
import { omitKeys } from '../../util/index.js';
import { validateQueryComplexity } from './validateQueryComplexity.js';
import { getDefaultVariant } from './feature-evaluator/variant.js';
import { cleanContext } from './clean-context.js';
export class PlaygroundService {
constructor(config, { featureToggleService, privateProjectChecker, }, segmentReadModel) {
this.logger = config.getLogger('services/playground-service.ts');
this.flagResolver = config.flagResolver;
this.featureToggleService = featureToggleService;
this.privateProjectChecker = privateProjectChecker;
this.segmentReadModel = segmentReadModel;
}
async evaluateAdvancedQuery(projects, environments, context, userId) {
// used for runtime control, do not remove
const { payload } = this.flagResolver.getVariant('advancedPlayground');
const limit = payload?.value &&
Number.isInteger(Number.parseInt(payload?.value, 10))
? Number.parseInt(payload?.value, 10)
: 15000;
const segments = await this.segmentReadModel.getActive();
let filteredProjects = projects;
const projectAccess = await this.privateProjectChecker.getUserAccessibleProjects(userId);
if (projectAccess.mode === 'all') {
filteredProjects = projects;
}
else if (projects === ALL) {
filteredProjects = projectAccess.projects;
}
else {
filteredProjects = projects.filter((project) => projectAccess.projects.includes(project));
}
const environmentFeatures = await Promise.all(environments.map((env) => this.resolveFeatures(filteredProjects, env)));
const { context: cleanedContext, removedProperties } = cleanContext(context);
const contexts = generateObjectCombinations(cleanedContext);
validateQueryComplexity(environments.length, environmentFeatures[0]?.features.length ?? 0, contexts.length, limit);
const results = await Promise.all(environmentFeatures.flatMap(({ features, featureProject, environment }) => contexts.map((singleContext) => this.evaluate({
features,
featureProject,
context: singleContext,
segments,
environment,
}))));
const items = results.flat();
const itemsByName = groupBy(items, (item) => item.name);
const result = Object.values(itemsByName).map((entries) => {
const groupedEnvironments = groupBy(entries, (entry) => entry.environment);
return {
name: entries[0].name,
projectId: entries[0].projectId,
environments: groupedEnvironments,
};
});
return {
result,
invalidContextProperties: removedProperties,
};
}
async evaluate({ featureProject, features, segments, context, environment, }) {
const [head, ...rest] = features;
if (!head) {
return [];
}
else {
const client = await offlineUnleashClient({
features: [head, ...rest],
context,
logError: this.logger.error,
segments,
});
const variantsMap = features.reduce((acc, feature) => {
acc[feature.name] = feature.variants;
return acc;
}, {});
const clientContext = {
...context,
currentTime: context.currentTime
? new Date(context.currentTime)
: undefined,
};
return client
.getFeatureToggleDefinitions()
.map((feature) => {
const strategyEvaluationResult = client.isEnabled(feature.name, clientContext);
const hasUnsatisfiedDependency = strategyEvaluationResult.hasUnsatisfiedDependency;
const isEnabled = strategyEvaluationResult.result === true &&
feature.enabled &&
!hasUnsatisfiedDependency;
const variant = {
...(isEnabled
? client.forceGetVariant(feature.name, strategyEvaluationResult, clientContext)
: getDefaultVariant()),
feature_enabled: isEnabled,
};
return {
isEnabled,
isEnabledInCurrentEnvironment: feature.enabled,
hasUnsatisfiedDependency,
strategies: {
result: strategyEvaluationResult.result,
data: strategyEvaluationResult.strategies,
},
projectId: featureProject[feature.name],
variant,
name: feature.name,
environment,
context,
variants: strategyEvaluationResult.variants ||
variantsMap[feature.name] ||
[],
};
});
}
}
async resolveFeatures(projects, environment) {
const features = await this.featureToggleService.getPlaygroundFeatures({
project: projects === ALL ? undefined : projects,
environment,
});
const featureProject = features.reduce((obj, feature) => {
obj[feature.name] = feature.project;
return obj;
}, {});
return { features, featureProject, environment };
}
async evaluateQuery(projects, environment, context) {
const [{ features, featureProject }, segments] = await Promise.all([
this.resolveFeatures(projects, environment),
this.segmentReadModel.getActive(),
]);
const result = await this.evaluate({
features,
featureProject,
segments,
context,
environment,
});
return result.map((item) => omitKeys(item, 'environment', 'context'));
}
}
//# sourceMappingURL=playground-service.js.map