unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
229 lines • 9.74 kB
JavaScript
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