2021-01-18 12:32:19 +01:00
|
|
|
const { FEATURE_TAGGED, FEATURE_UNTAGGED } = require('../event-type');
|
2021-01-22 13:39:42 +01:00
|
|
|
const { featureSchema, nameSchema, querySchema } = require('./feature-schema');
|
2021-01-18 12:32:19 +01:00
|
|
|
const { tagSchema } = require('./tag-schema');
|
|
|
|
const NameExistsError = require('../error/name-exists-error');
|
|
|
|
const NotFoundError = require('../error/notfound-error');
|
|
|
|
const {
|
|
|
|
FEATURE_ARCHIVED,
|
|
|
|
FEATURE_CREATED,
|
|
|
|
FEATURE_REVIVED,
|
|
|
|
FEATURE_UPDATED,
|
|
|
|
TAG_CREATED,
|
|
|
|
} = require('../event-type');
|
|
|
|
|
|
|
|
class FeatureToggleService {
|
2021-01-22 13:39:42 +01:00
|
|
|
constructor({ featureToggleStore, tagStore, eventStore }, { getLogger }) {
|
2021-01-18 12:32:19 +01:00
|
|
|
this.featureToggleStore = featureToggleStore;
|
|
|
|
this.tagStore = tagStore;
|
|
|
|
this.eventStore = eventStore;
|
|
|
|
this.logger = getLogger('services/feature-toggle-service.js');
|
|
|
|
}
|
|
|
|
|
2021-01-22 13:39:42 +01:00
|
|
|
async getFeatures(query, fields) {
|
|
|
|
const preppedQuery = await this.prepQuery(query);
|
|
|
|
return this.featureToggleStore.getFeatures(preppedQuery, fields);
|
2021-01-18 12:32:19 +01:00
|
|
|
}
|
|
|
|
|
2021-01-22 13:39:42 +01:00
|
|
|
paramToArray(param) {
|
|
|
|
if (!param) {
|
|
|
|
return param;
|
|
|
|
}
|
|
|
|
return Array.isArray(param) ? param : [param];
|
|
|
|
}
|
|
|
|
|
|
|
|
async prepQuery({ tag, project, namePrefix }) {
|
|
|
|
if (!tag && !project && !namePrefix) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const tagQuery = this.paramToArray(tag);
|
|
|
|
const projectQuery = this.paramToArray(project);
|
|
|
|
const query = await querySchema.validateAsync({
|
|
|
|
tag: tagQuery,
|
|
|
|
project: projectQuery,
|
|
|
|
namePrefix,
|
|
|
|
});
|
|
|
|
if (query.tag) {
|
|
|
|
query.tag = query.tag.map(q => q.split(':'));
|
|
|
|
}
|
|
|
|
return query;
|
2021-01-22 13:09:26 +01:00
|
|
|
}
|
|
|
|
|
2021-01-18 12:32:19 +01:00
|
|
|
async getArchivedFeatures() {
|
|
|
|
return this.featureToggleStore.getArchivedFeatures();
|
|
|
|
}
|
|
|
|
|
|
|
|
async addArchivedFeature(feature) {
|
|
|
|
await this.featureToggleStore.addArchivedFeature(feature);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getFeature(name) {
|
|
|
|
return this.featureToggleStore.getFeature(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
async createFeatureToggle(value, userName) {
|
|
|
|
await this.validateName(value);
|
|
|
|
const feature = await featureSchema.validateAsync(value);
|
|
|
|
await this.featureToggleStore.createFeature(feature);
|
|
|
|
await this.eventStore.store({
|
|
|
|
type: FEATURE_CREATED,
|
|
|
|
createdBy: userName,
|
|
|
|
data: feature,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateToggle(updatedFeature, userName) {
|
|
|
|
await this.featureToggleStore.getFeature(updatedFeature.name);
|
|
|
|
const value = await featureSchema.validateAsync(updatedFeature);
|
|
|
|
await this.featureToggleStore.updateFeature(value);
|
|
|
|
await this.eventStore.store({
|
|
|
|
type: FEATURE_UPDATED,
|
|
|
|
createdBy: userName,
|
|
|
|
data: updatedFeature,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async archiveToggle(name, userName) {
|
|
|
|
await this.featureToggleStore.getFeature(name);
|
|
|
|
await this.featureToggleStore.archiveFeature(name);
|
|
|
|
await this.eventStore.store({
|
|
|
|
type: FEATURE_ARCHIVED,
|
|
|
|
createdBy: userName,
|
|
|
|
data: { name },
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async reviveToggle(name, userName) {
|
|
|
|
await this.featureToggleStore.reviveFeature({ name });
|
|
|
|
await this.eventStore.store({
|
|
|
|
type: FEATURE_REVIVED,
|
|
|
|
createdBy: userName,
|
|
|
|
data: { name },
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async toggle(featureName, userName) {
|
|
|
|
const feature = await this.featureToggleStore.getFeature(featureName);
|
|
|
|
const toggle = !feature.enabled;
|
|
|
|
return this.updateField(feature.name, 'enabled', toggle, userName);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Tag releated */
|
|
|
|
async listTags(featureName) {
|
2021-01-22 13:39:42 +01:00
|
|
|
return this.featureToggleStore.getAllTagsForFeature(featureName);
|
2021-01-18 12:32:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async addTag(featureName, tag, userName) {
|
|
|
|
await nameSchema.validateAsync({ name: featureName });
|
|
|
|
const validatedTag = await tagSchema.validateAsync(tag);
|
|
|
|
await this.createTagIfNeeded(validatedTag, userName);
|
2021-01-22 13:39:42 +01:00
|
|
|
await this.featureToggleStore.tagFeature(featureName, validatedTag);
|
2021-01-18 12:32:19 +01:00
|
|
|
await this.eventStore.store({
|
|
|
|
type: FEATURE_TAGGED,
|
|
|
|
createdBy: userName,
|
|
|
|
data: {
|
|
|
|
featureName,
|
|
|
|
tag: validatedTag,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
return validatedTag;
|
|
|
|
}
|
|
|
|
|
|
|
|
async createTagIfNeeded(tag, userName) {
|
|
|
|
try {
|
|
|
|
await this.tagStore.getTag(tag.type, tag.value);
|
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof NotFoundError) {
|
|
|
|
await this.tagStore.createTag(tag);
|
|
|
|
await this.eventStore.store({
|
|
|
|
type: TAG_CREATED,
|
|
|
|
createdBy: userName,
|
|
|
|
data: {
|
|
|
|
tag,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async removeTag(featureName, tag, userName) {
|
2021-01-22 13:39:42 +01:00
|
|
|
await this.featureToggleStore.untagFeature(featureName, tag);
|
2021-01-18 12:32:19 +01:00
|
|
|
await this.eventStore.store({
|
|
|
|
type: FEATURE_UNTAGGED,
|
|
|
|
createdBy: userName,
|
|
|
|
data: {
|
|
|
|
featureName,
|
|
|
|
tag,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Validations */
|
|
|
|
async validateName({ name }) {
|
|
|
|
await nameSchema.validateAsync({ name });
|
|
|
|
await this.validateUniqueFeatureName(name);
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
|
|
|
|
async validateUniqueFeatureName(name) {
|
|
|
|
let msg;
|
|
|
|
try {
|
|
|
|
const feature = await this.featureToggleStore.hasFeature(name);
|
|
|
|
msg = feature.archived
|
|
|
|
? 'An archived toggle with that name already exists'
|
|
|
|
: 'A toggle with that name already exists';
|
|
|
|
} catch (error) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new NameExistsError(msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateField(featureName, field, value, userName) {
|
|
|
|
const feature = await this.featureToggleStore.getFeature(featureName);
|
|
|
|
feature[field] = value;
|
|
|
|
await this.featureToggleStore.updateFeature(feature);
|
|
|
|
await this.eventStore.store({
|
|
|
|
type: FEATURE_UPDATED,
|
|
|
|
createdBy: userName,
|
|
|
|
data: feature,
|
|
|
|
});
|
|
|
|
return feature;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = FeatureToggleService;
|