mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: bulk revive features (#3321)
This commit is contained in:
parent
75d2930bcd
commit
138ac98094
@ -316,6 +316,14 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
return this.rowToFeature(row[0]);
|
||||
}
|
||||
|
||||
async batchRevive(names: string[]): Promise<FeatureToggle[]> {
|
||||
const rows = await this.db(TABLE)
|
||||
.whereIn('name', names)
|
||||
.update({ archived_at: null })
|
||||
.returning(FEATURE_COLUMNS);
|
||||
return rows.map((row) => this.rowToFeature(row));
|
||||
}
|
||||
|
||||
async getVariants(featureName: string): Promise<IVariant[]> {
|
||||
if (!(await this.exists(featureName))) {
|
||||
throw new NotFoundError('No feature toggle found');
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { Response } from 'express';
|
||||
import { IUnleashConfig } from '../../../types/option';
|
||||
import { IFlagResolver, IProjectParam, IUnleashServices } from '../../../types';
|
||||
import {
|
||||
IFlagResolver,
|
||||
IProjectParam,
|
||||
IUnleashServices,
|
||||
UPDATE_FEATURE,
|
||||
} from '../../../types';
|
||||
import { Logger } from '../../../logger';
|
||||
import { extractUsername } from '../../../util/extract-user';
|
||||
import { DELETE_FEATURE } from '../../../types/permissions';
|
||||
@ -14,6 +19,7 @@ import Controller from '../../controller';
|
||||
|
||||
const PATH = '/:projectId/archive';
|
||||
const PATH_DELETE = `${PATH}/delete`;
|
||||
const PATH_REVIVE = `${PATH}/revive`;
|
||||
|
||||
export default class ProjectArchiveController extends Controller {
|
||||
private readonly logger: Logger;
|
||||
@ -52,6 +58,22 @@ export default class ProjectArchiveController extends Controller {
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: PATH_REVIVE,
|
||||
acceptAnyContentType: true,
|
||||
handler: this.reviveFeatures,
|
||||
permission: UPDATE_FEATURE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Archive'],
|
||||
operationId: 'reviveFeatures',
|
||||
requestBody: createRequestSchema('batchFeaturesSchema'),
|
||||
responses: { 200: emptyResponse },
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async deleteFeatures(
|
||||
@ -67,6 +89,20 @@ export default class ProjectArchiveController extends Controller {
|
||||
await this.featureService.deleteFeatures(features, projectId, user);
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async reviveFeatures(
|
||||
req: IAuthRequest<IProjectParam, any, BatchFeaturesSchema>,
|
||||
res: Response<void>,
|
||||
): Promise<void> {
|
||||
if (!this.flagResolver.isEnabled('bulkOperations')) {
|
||||
throw new NotFoundError('Bulk operations are not enabled');
|
||||
}
|
||||
const { projectId } = req.params;
|
||||
const { features } = req.body;
|
||||
const user = extractUsername(req);
|
||||
await this.featureService.reviveFeatures(features, projectId, user);
|
||||
res.status(200).end();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProjectArchiveController;
|
||||
|
@ -1374,6 +1374,42 @@ class FeatureToggleService {
|
||||
);
|
||||
}
|
||||
|
||||
async reviveFeatures(
|
||||
featureNames: string[],
|
||||
projectId: string,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
await this.validateFeaturesContext(featureNames, projectId);
|
||||
|
||||
const features = await this.featureToggleStore.getAllByNames(
|
||||
featureNames,
|
||||
);
|
||||
const eligibleFeatures = features.filter(
|
||||
(toggle) => toggle.archivedAt !== null,
|
||||
);
|
||||
const eligibleFeatureNames = eligibleFeatures.map(
|
||||
(toggle) => toggle.name,
|
||||
);
|
||||
const tags = await this.tagStore.getAllByFeatures(eligibleFeatureNames);
|
||||
await this.featureToggleStore.batchRevive(eligibleFeatureNames);
|
||||
await this.eventStore.batchStore(
|
||||
eligibleFeatures.map(
|
||||
(feature) =>
|
||||
new FeatureRevivedEvent({
|
||||
featureName: feature.name,
|
||||
createdBy,
|
||||
project: feature.project,
|
||||
tags: tags
|
||||
.filter((tag) => tag.featureName === feature.name)
|
||||
.map((tag) => ({
|
||||
value: tag.tagValue,
|
||||
type: tag.tagType,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: add project id.
|
||||
async reviveToggle(featureName: string, createdBy: string): Promise<void> {
|
||||
const toggle = await this.featureToggleStore.revive(featureName);
|
||||
|
@ -21,6 +21,7 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
||||
stale: boolean,
|
||||
): Promise<FeatureToggle[]>;
|
||||
batchDelete(featureNames: string[]): Promise<void>;
|
||||
batchRevive(featureNames: string[]): Promise<FeatureToggle[]>;
|
||||
revive(featureName: string): Promise<FeatureToggle>;
|
||||
getAll(query?: Partial<IFeatureToggleQuery>): Promise<FeatureToggle[]>;
|
||||
getAllByNames(names: string[]): Promise<FeatureToggle[]>;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { setupAppWithCustomConfig } from '../../helpers/test-helper';
|
||||
import dbInit from '../../helpers/database-init';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import { DEFAULT_PROJECT } from '../../../../lib/types';
|
||||
|
||||
let app;
|
||||
let db;
|
||||
@ -203,8 +204,13 @@ test('can bulk delete features and recreate after', async () => {
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
await app.request.delete(`/api/admin/features/${feature}`).expect(200);
|
||||
}
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${DEFAULT_PROJECT}/archive`)
|
||||
.send({
|
||||
features,
|
||||
})
|
||||
.expect(202);
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/archive/delete')
|
||||
.send({ features })
|
||||
@ -217,3 +223,33 @@ test('can bulk delete features and recreate after', async () => {
|
||||
.expect(200);
|
||||
}
|
||||
});
|
||||
|
||||
test('can bulk revive features', async () => {
|
||||
const features = ['first.revive.issue', 'second.revive.issue'];
|
||||
for (const feature of features) {
|
||||
await app.request
|
||||
.post('/api/admin/features')
|
||||
.send({
|
||||
name: feature,
|
||||
enabled: false,
|
||||
strategies: [{ name: 'default' }],
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
}
|
||||
await app.request
|
||||
.post(`/api/admin/projects/${DEFAULT_PROJECT}/archive`)
|
||||
.send({
|
||||
features,
|
||||
})
|
||||
.expect(202);
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/archive/revive')
|
||||
.send({ features })
|
||||
.expect(200);
|
||||
for (const feature of features) {
|
||||
await app.request
|
||||
.get(`/api/admin/projects/default/features/${feature}`)
|
||||
.expect(200);
|
||||
}
|
||||
});
|
||||
|
@ -6114,6 +6114,40 @@ If the provided project does not exist, the list of events will be empty.",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/projects/{projectId}/archive/revive": {
|
||||
"post": {
|
||||
"operationId": "reviveFeatures",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "projectId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/batchFeaturesSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "batchFeaturesSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "This response has no body.",
|
||||
},
|
||||
},
|
||||
"tags": [
|
||||
"Archive",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/projects/{projectId}/environments": {
|
||||
"post": {
|
||||
"operationId": "addEnvironmentToProject",
|
||||
|
10
src/test/fixtures/fake-feature-toggle-store.ts
vendored
10
src/test/fixtures/fake-feature-toggle-store.ts
vendored
@ -54,6 +54,16 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async batchRevive(featureNames: string[]): Promise<FeatureToggle[]> {
|
||||
const features = this.features.filter((f) =>
|
||||
featureNames.includes(f.name),
|
||||
);
|
||||
for (const feature of features) {
|
||||
feature.archived = false;
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
async count(query: Partial<IFeatureToggleQuery>): Promise<number> {
|
||||
return this.features.filter(this.getFilterQuery(query)).length;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user