UNPKG

unleash-server

Version:

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

229 lines • 9.74 kB
import memoizee from 'memoizee'; import joi from 'joi'; const { ValidationError } = joi; import { getAddons } from '../addons/index.js'; import { AddonConfigCreatedEvent, AddonConfigDeletedEvent, AddonConfigUpdatedEvent, } from '../types/index.js'; import { addonSchema } from './addon-schema.js'; import NameExistsError from '../error/name-exists-error.js'; import { SYSTEM_USER_AUDIT, } from '../types/index.js'; import { minutesToMilliseconds } from 'date-fns'; import { omitKeys } from '../util/index.js'; import { BadDataError, NotFoundError } from '../error/index.js'; import { IEventTypes } from '../events/index.js'; const MASKED_VALUE = '*****'; const WILDCARD_OPTION = '*'; export default class AddonService { constructor({ addonStore, featureToggleStore, }, { getLogger, server, flagResolver, eventBus, }, tagTypeService, eventService, integrationEventsService, addons) { this.addonStore = addonStore; this.featureToggleStore = featureToggleStore; this.logger = getLogger('services/addon-service.js'); this.tagTypeService = tagTypeService; this.eventService = eventService; this.eventHandlers = new Map(); this.addonProviders = addons || getAddons({ getLogger, unleashUrl: server.unleashUrl, integrationEventsService, flagResolver, eventBus, }); this.sensitiveParams = this.loadSensitiveParams(this.addonProviders); if (addonStore) { this.registerEventHandler(); } // Memoized private function this.fetchAddonConfigs = memoizee(async () => addonStore.getAll({ enabled: true }), { promise: true, maxAge: minutesToMilliseconds(1), }); } loadSensitiveParams(addonProviders) { const providerDefinitions = Object.values(addonProviders).map((p) => p.definition); return providerDefinitions.reduce((obj, definition) => { const sensitiveParams = definition.parameters ?.filter((p) => p.sensitive) .map((p) => p.name); const o = { ...obj }; o[definition.name] = sensitiveParams; return o; }, {}); } registerEventHandler() { IEventTypes.forEach((eventName) => { const handler = this.handleEvent(eventName); this.eventHandlers.set(eventName, handler); this.eventService.onEvent(eventName, handler); }); } handleEvent(eventName) { const { addonProviders } = this; return async (event) => { const addonInstances = await this.fetchAddonConfigs(); const tasks = addonInstances .filter((addon) => addon.events.includes(eventName)) .filter((addon) => !event.project || !addon.projects || addon.projects.length === 0 || addon.projects[0] === WILDCARD_OPTION || addon.projects.includes(event.project)) .filter((addon) => !event.environment || !addon.environments || addon.environments.length === 0 || addon.environments[0] === WILDCARD_OPTION || addon.environments.includes(event.environment)) .filter((addon) => addonProviders[addon.provider]) .map((addon) => addonProviders[addon.provider].handleEvent(event, addon.parameters, addon.id)); await Promise.all(tasks); }; } // Should be used by the controller. async getAddons() { const addonConfigs = await this.addonStore.getAll(); return addonConfigs.map((a) => this.filterSensitiveFields(a)); } filterSensitiveFields(addonConfig) { const { sensitiveParams } = this; const a = { ...addonConfig }; a.parameters = Object.keys(a.parameters).reduce((obj, paramKey) => { const o = { ...obj }; if (sensitiveParams[a.provider].includes(paramKey)) { o[paramKey] = MASKED_VALUE; } else { o[paramKey] = a.parameters[paramKey]; } return o; }, {}); return a; } async getAddon(id) { const addonConfig = await this.addonStore.get(id); if (addonConfig === undefined) { throw new NotFoundError(); } return this.filterSensitiveFields(addonConfig); } getProviderDefinitions() { const { addonProviders } = this; return Object.values(addonProviders).map((p) => p.definition); } async addTagTypes(providerName) { const provider = this.addonProviders[providerName]; if (provider) { const tagTypes = provider.definition.tagTypes || []; const createTags = tagTypes.map(async (tagType) => { try { await this.tagTypeService.validateUnique(tagType.name); await this.tagTypeService.createTagType(tagType, SYSTEM_USER_AUDIT); } catch (err) { if (!(err instanceof NameExistsError)) { this.logger.error(err); } } }); await Promise.all(createTags); } return Promise.resolve(); } async createAddon(data, auditUser) { const addonConfig = await addonSchema.validateAsync(data); await this.validateKnownProvider(addonConfig); await this.validateRequiredParameters(addonConfig); const addon = this.addonProviders[addonConfig.provider]; if (addon.definition.deprecated) { throw new BadDataError(addon.definition.deprecated); } const createdAddon = await this.addonStore.insert(addonConfig); await this.addTagTypes(createdAddon.provider); this.logger.info(`User ${auditUser.username} created addon ${addonConfig.provider}`); await this.eventService.storeEvent(new AddonConfigCreatedEvent({ data: omitKeys(createdAddon, 'parameters'), auditUser, })); return createdAddon; } async updateAddon(id, data, auditUser) { const existingConfig = await this.addonStore.get(id); if (existingConfig === undefined) { throw new NotFoundError(); } // because getting an early 404 here makes more sense const addonConfig = await addonSchema.validateAsync(data); await this.validateKnownProvider(addonConfig); await this.validateRequiredParameters(addonConfig); if (this.sensitiveParams[addonConfig.provider].length > 0) { addonConfig.parameters = Object.keys(addonConfig.parameters).reduce((params, key) => { const o = { ...params }; if (addonConfig.parameters[key] === MASKED_VALUE) { o[key] = existingConfig.parameters[key]; } else { o[key] = addonConfig.parameters[key]; } return o; }, {}); } const result = await this.addonStore.update(id, addonConfig); await this.eventService.storeEvent(new AddonConfigUpdatedEvent({ preData: omitKeys(existingConfig, 'parameters'), data: omitKeys(result, 'parameters'), auditUser, })); this.logger.info(`User ${auditUser} updated addon ${id}`); return result; } async removeAddon(id, auditUser) { const existingConfig = await this.addonStore.get(id); if (existingConfig === undefined) { /// No config, no need to delete return; } await this.addonStore.delete(id); await this.eventService.storeEvent(new AddonConfigDeletedEvent({ preData: omitKeys(existingConfig, 'parameters'), auditUser, })); this.logger.info(`User ${auditUser} removed addon ${id}`); } async validateKnownProvider(config) { if (!config.provider) { throw new ValidationError('No addon provider supplied. The property was either missing or an empty value.', [], undefined); } const p = this.addonProviders[config.provider]; if (!p) { throw new ValidationError(`Unknown addon provider ${config.provider}`, [], undefined); } else { return true; } } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async validateRequiredParameters({ provider, parameters, }) { const providerDefinition = this.addonProviders[provider].definition; const requiredParamsMissing = providerDefinition.parameters ?.filter((p) => p.required) .map((p) => p.name) .filter((requiredParam) => !Object.keys(parameters).includes(requiredParam)) || []; if (requiredParamsMissing.length > 0) { throw new ValidationError(`Missing required parameters: ${requiredParamsMissing.join(',')} `, [], undefined); } return true; } destroy() { this.eventHandlers.forEach((handler, eventName) => { try { this.eventService.off(eventName, handler); } catch (error) { this.logger.debug(`Failed to remove event handler for ${eventName}:`, error); } }); this.eventHandlers.clear(); Object.values(this.addonProviders).forEach((addon) => { addon.destroy?.(); }); } } //# sourceMappingURL=addon-service.js.map