UNPKG

unleash-server

Version:

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

216 lines • 7.32 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); 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 FEATURE_COLUMNS = [ 'name', 'description', 'type', 'project', 'stale', 'created_at', 'impression_data', 'last_seen_at', 'archived_at', ]; const TABLE = 'features'; const FEATURE_ENVIRONMENTS_TABLE = 'feature_environments'; class FeatureToggleStore { constructor(db, eventBus, getLogger) { this.db = db; this.logger = getLogger('feature-toggle-store.ts'); this.timer = (action) => metrics_helper_1.default.wrapTimer(eventBus, metric_events_1.DB_TIME, { store: 'feature-toggle', action, }); } async count(query = { archived: false }) { const { archived, ...rest } = query; return this.db .from(TABLE) .count('*') .where(rest) .modify(FeatureToggleStore.filterByArchived, archived) .then((res) => Number(res[0].count)); } async deleteAll() { await this.db(TABLE).del(); } destroy() { } async get(name) { return this.db .first(FEATURE_COLUMNS) .from(TABLE) .where({ name }) .then(this.rowToFeature); } async getAll(query = { archived: false }) { const { archived, ...rest } = query; const rows = await this.db .select(FEATURE_COLUMNS) .from(TABLE) .where(rest) .modify(FeatureToggleStore.filterByArchived, archived); return rows.map(this.rowToFeature); } /** * Get projectId from feature filtered by name. Used by Rbac middleware * @deprecated * @param name */ async getProjectId(name) { return this.db .first(['project']) .from(TABLE) .where({ name }) .then((r) => (r ? r.project : undefined)) .catch((e) => { this.logger.error(e); return undefined; }); } async exists(name) { const result = await this.db.raw('SELECT EXISTS (SELECT 1 FROM features WHERE name = ?) AS present', [name]); const { present } = result.rows[0]; return present; } async setLastSeen(toggleNames) { const now = new Date(); try { await this.db(TABLE) .update({ last_seen_at: now }) .whereIn('name', this.db(TABLE) .select('name') .whereIn('name', toggleNames) .forUpdate() .skipLocked()); } catch (err) { this.logger.error('Could not update lastSeen, error: ', err); } } rowToFeature(row) { if (!row) { throw new notfound_error_1.default('No feature toggle found'); } return { name: row.name, description: row.description, type: row.type, project: row.project, stale: row.stale, createdAt: row.created_at, lastSeenAt: row.last_seen_at, impressionData: row.impression_data, archivedAt: row.archived_at, archived: row.archived_at != null, }; } rowToEnvVariants(variantRows) { if (!variantRows.length) { return []; } const sortedVariants = variantRows[0].variants || []; sortedVariants.sort((a, b) => a.name.localeCompare(b.name)); return sortedVariants; } dtoToRow(project, data) { const row = { name: data.name, description: data.description, type: data.type, project, archived_at: data.archived ? new Date() : null, stale: data.stale, created_at: data.createdAt, impression_data: data.impressionData, }; if (!row.created_at) { delete row.created_at; } return row; } async create(project, data) { try { const row = await this.db(TABLE) .insert(this.dtoToRow(project, data)) .returning(FEATURE_COLUMNS); return this.rowToFeature(row[0]); } catch (err) { this.logger.error('Could not insert feature, error: ', err); } return undefined; } async update(project, data) { const row = await this.db(TABLE) .where({ name: data.name }) .update(this.dtoToRow(project, data)) .returning(FEATURE_COLUMNS); return this.rowToFeature(row[0]); } async archive(name) { const now = new Date(); const row = await this.db(TABLE) .where({ name }) .update({ archived_at: now }) .returning(FEATURE_COLUMNS); return this.rowToFeature(row[0]); } async delete(name) { await this.db(TABLE) .where({ name }) // Feature toggle must be archived to allow deletion .whereNotNull('archived_at') .del(); } async revive(name) { const row = await this.db(TABLE) .where({ name }) .update({ archived_at: null }) .returning(FEATURE_COLUMNS); return this.rowToFeature(row[0]); } async getVariants(featureName) { if (!(await this.exists(featureName))) { throw new notfound_error_1.default('No feature toggle found'); } const row = await this.db(`${TABLE} as f`) .select('fe.variants') .join(`${FEATURE_ENVIRONMENTS_TABLE} as fe`, 'fe.feature_name', 'f.name') .where({ name: featureName }) .limit(1); return this.rowToEnvVariants(row); } async getVariantsForEnv(featureName, environment) { const row = await this.db(`${TABLE} as f`) .select('fev.variants') .join(`${FEATURE_ENVIRONMENTS_TABLE} as fev`, 'fev.feature_name', 'f.name') .where({ name: featureName }) .andWhere({ environment }); return this.rowToEnvVariants(row); } async saveVariants(project, featureName, newVariants) { const variantsString = JSON.stringify(newVariants); await this.db('feature_environments') .update('variants', variantsString) .where('feature_name', featureName); const row = await this.db(TABLE) .select(FEATURE_COLUMNS) .where({ project: project, name: featureName }); const toggle = this.rowToFeature(row[0]); toggle.variants = newVariants; return toggle; } } exports.default = FeatureToggleStore; FeatureToggleStore.filterByArchived = (queryBuilder, archived) => { return archived ? queryBuilder.whereNotNull('archived_at') : queryBuilder.whereNull('archived_at'); }; module.exports = FeatureToggleStore; //# sourceMappingURL=feature-toggle-store.js.map