UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.

295 lines • 12 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FeatureEnvironmentStore = void 0; const metrics_helper_1 = __importDefault(require("../util/metrics-helper")); const metric_events_1 = require("../metric-events"); const notfound_error_1 = __importDefault(require("../error/notfound-error")); const uuid_1 = require("uuid"); const T = { featureEnvs: 'feature_environments', featureStrategies: 'feature_strategies', features: 'features', }; class FeatureEnvironmentStore { constructor(db, eventBus, getLogger) { this.db = db; this.logger = getLogger('feature-environment-store.ts'); this.timer = (action) => metrics_helper_1.default.wrapTimer(eventBus, metric_events_1.DB_TIME, { store: 'feature-environments', action, }); } async delete({ featureName, environment, }) { await this.db(T.featureEnvs) .where('feature_name', featureName) .andWhere('environment', environment) .del(); } async deleteAll() { await this.db(T.featureEnvs).del(); } destroy() { } async exists({ featureName, environment, }) { const result = await this.db.raw(`SELECT EXISTS (SELECT 1 FROM ${T.featureEnvs} WHERE feature_name = ? AND environment = ?) AS present`, [featureName, environment]); const { present } = result.rows[0]; return present; } async get({ featureName, environment, }) { const md = await this.db(T.featureEnvs) .where('feature_name', featureName) .andWhere('environment', environment) .first(); if (md) { return { enabled: md.enabled, featureName, environment, variants: md.variants, }; } throw new notfound_error_1.default(`Could not find ${featureName} in ${environment}`); } async getAll(query) { let rows = this.db(T.featureEnvs); if (query) { rows = rows.where(query); } return (await rows).map((r) => ({ enabled: r.enabled, featureName: r.feature_name, environment: r.environment, variants: r.variants, })); } async disableEnvironmentIfNoStrategies(featureName, environment) { const result = await this.db.raw(`SELECT EXISTS (SELECT 1 FROM ${T.featureStrategies} WHERE feature_name = ? AND environment = ?) AS enabled`, [featureName, environment]); const { enabled } = result.rows[0]; if (!enabled) { await this.db(T.featureEnvs) .update({ enabled: false }) .where({ feature_name: featureName, environment }); } } async addEnvironmentToFeature(featureName, environment, enabled = false) { await this.db('feature_environments') .insert({ feature_name: featureName, environment, enabled }) .onConflict(['environment', 'feature_name']) .merge('enabled'); } // TODO: move to project store. async disconnectFeatures(environment, project) { const featureSelector = this.db('features') .where({ project }) .select('name'); await this.db(T.featureEnvs) .where({ environment }) .andWhere('feature_name', 'IN', featureSelector) .del(); await this.db('feature_strategies').where({ environment, project_name: project, }); } async featureHasEnvironment(environment, featureName) { const result = await this.db.raw(`SELECT EXISTS (SELECT 1 FROM ${T.featureEnvs} WHERE feature_name = ? AND environment = ?) AS present`, [featureName, environment]); const { present } = result.rows[0]; return present; } async getEnvironmentsForFeature(featureName) { const envs = await this.db(T.featureEnvs).where('feature_name', featureName); if (envs) { return envs.map((r) => ({ featureName: r.feature_name, environment: r.environment, variants: r.variants || [], enabled: r.enabled, })); } return []; } async getEnvironmentMetaData(environment, featureName) { const md = await this.db(T.featureEnvs) .where('feature_name', featureName) .andWhere('environment', environment) .first(); if (md) { return { enabled: md.enabled, featureName, environment, }; } throw new notfound_error_1.default(`Could not find ${featureName} in ${environment}`); } async isEnvironmentEnabled(featureName, environment) { const row = await this.db(T.featureEnvs) .select('enabled') .where({ feature_name: featureName, environment }) .first(); return row.enabled; } async removeEnvironmentForFeature(featureName, environment) { await this.db(T.featureEnvs) .where({ feature_name: featureName, environment }) .del(); } async setEnvironmentEnabledStatus(environment, featureName, enabled) { return this.db(T.featureEnvs).update({ enabled }).where({ environment, feature_name: featureName, enabled: !enabled, }); } async connectProject(environment, projectId, idempotent) { const query = this.db('project_environments').insert({ environment_name: environment, project_id: projectId, }); if (idempotent) { await query.onConflict(['environment_name', 'project_id']).ignore(); } else { await query; } } async connectFeatures(environment, projectId) { const featuresToEnable = await this.db('features') .select('name') .where({ project: projectId, }); const rows = featuresToEnable.map((f) => ({ environment, feature_name: f.name, enabled: false, })); if (rows.length > 0) { await this.db('feature_environments') .insert(rows) .onConflict(['environment', 'feature_name']) .ignore(); } } async disconnectProject(environment, projectId) { await this.db('project_environments') .where({ environment_name: environment, project_id: projectId }) .del(); } // TODO: remove this once variants per env are GA async clonePreviousVariants(environment, project) { const rows = await this.db(`${T.features} as f`) .select([ this.db.raw(`'${environment}' as environment`), 'fe.enabled', 'fe.feature_name', 'fe.variants', ]) .distinctOn(['environment', 'feature_name']) .join(`${T.featureEnvs} as fe`, 'f.name', 'fe.feature_name') .whereNot({ environment }) .andWhere({ project }); const newRows = rows.map((row) => { const r = row; return { variants: JSON.stringify(r.variants), enabled: r.enabled, environment: r.environment, feature_name: r.feature_name, }; }); if (newRows.length > 0) { await this.db(T.featureEnvs) .insert(newRows) .onConflict(['environment', 'feature_name']) .merge(['variants']); } } async connectFeatureToEnvironmentsForProject(featureName, projectId, enabledIn = {}) { const environmentsToEnable = await this.db('project_environments') .select('environment_name') .where({ project_id: projectId }); await Promise.all(environmentsToEnable.map(async (env) => { await this.db('feature_environments') .insert({ environment: env.environment_name, feature_name: featureName, enabled: enabledIn[env.environment_name] || false, }) .onConflict(['environment', 'feature_name']) .ignore(); })); } async copyEnvironmentFeaturesByProjects(sourceEnvironment, destinationEnvironment, projects) { await this.db.raw(`INSERT INTO ${T.featureEnvs} ( SELECT distinct ? AS environment, feature_name, enabled FROM ${T.featureEnvs} INNER JOIN ${T.features} ON ${T.featureEnvs}.feature_name = ${T.features}.name WHERE environment = ? AND project = ANY(?))`, [destinationEnvironment, sourceEnvironment, projects]); } async addVariantsToFeatureEnvironment(featureName, environment, variants) { let v = variants || []; v.sort((a, b) => a.name.localeCompare(b.name)); await this.db(T.featureEnvs) .insert({ variants: JSON.stringify(v), enabled: false, feature_name: featureName, environment: environment, }) .onConflict(['feature_name', 'environment']) .merge(['variants']); } async addFeatureEnvironment(featureEnvironment) { let v = featureEnvironment.variants || []; v.sort((a, b) => a.name.localeCompare(b.name)); await this.db(T.featureEnvs) .insert({ variants: JSON.stringify(v), enabled: featureEnvironment.enabled, feature_name: featureEnvironment.featureName, environment: featureEnvironment.environment, }) .onConflict(['feature_name', 'environment']) .merge(['variants', 'enabled']); } async cloneStrategies(sourceEnvironment, destinationEnvironment) { let sourceFeatureStrategies = await this.db('feature_strategies').where({ environment: sourceEnvironment, }); const clonedStrategyRows = sourceFeatureStrategies.map((featureStrategy) => { return { id: (0, uuid_1.v4)(), feature_name: featureStrategy.feature_name, project_name: featureStrategy.project_name, environment: destinationEnvironment, strategy_name: featureStrategy.strategy_name, parameters: JSON.stringify(featureStrategy.parameters), constraints: JSON.stringify(featureStrategy.constraints), sort_order: featureStrategy.sort_order, }; }); if (clonedStrategyRows.length === 0) { return Promise.resolve(); } await this.db('feature_strategies').insert(clonedStrategyRows); const newStrategyMapping = new Map(); sourceFeatureStrategies.forEach((sourceStrategy, index) => { newStrategyMapping.set(sourceStrategy.id, clonedStrategyRows[index].id); }); const segmentsToClone = await this.db('feature_strategy_segment as fss') .select(['id', 'segment_id']) .join('feature_strategies AS fs', 'fss.feature_strategy_id', 'fs.id') .where('environment', sourceEnvironment); const clonedSegmentIdRows = segmentsToClone.map((existingSegmentRow) => { return { feature_strategy_id: newStrategyMapping.get(existingSegmentRow.id), segment_id: existingSegmentRow.segment_id, }; }); if (clonedSegmentIdRows.length > 0) { await this.db('feature_strategy_segment').insert(clonedSegmentIdRows); } } } exports.FeatureEnvironmentStore = FeatureEnvironmentStore; //# sourceMappingURL=feature-environment-store.js.map