1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

feat: bulk stale features (#3311)

This commit is contained in:
Jaanus Sellin 2023-03-15 08:37:06 +02:00 committed by GitHub
parent 240bb7b027
commit 6c813ab066
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 215 additions and 15 deletions

View File

@ -283,6 +283,17 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
return rows.map((row) => this.rowToFeature(row));
}
async batchStale(
names: string[],
stale: boolean,
): Promise<FeatureToggle[]> {
const rows = await this.db(TABLE)
.whereIn('name', names)
.update({ stale })
.returning(FEATURE_COLUMNS);
return rows.map((row) => this.rowToFeature(row));
}
async delete(name: string): Promise<void> {
await this.db(TABLE)
.where({ name }) // Feature toggle must be archived to allow deletion

View File

@ -9,7 +9,7 @@ import {
apiTokensSchema,
applicationSchema,
applicationsSchema,
archiveFeaturesSchema,
batchFeaturesSchema,
changePasswordSchema,
clientApplicationSchema,
clientFeatureSchema,
@ -144,6 +144,7 @@ import { bulkRegistrationSchema } from './spec/bulk-registration-schema';
import { bulkMetricsSchema } from './spec/bulk-metrics-schema';
import { clientMetricsEnvSchema } from './spec/client-metrics-env-schema';
import { updateTagsSchema } from './spec/update-tags-schema';
import { batchStaleSchema } from './spec/batch-stale-schema';
// All schemas in `openapi/spec` should be listed here.
export const schemas = {
@ -156,7 +157,8 @@ export const schemas = {
apiTokensSchema,
applicationSchema,
applicationsSchema,
archiveFeaturesSchema,
batchFeaturesSchema,
batchStaleSchema,
bulkRegistrationSchema,
bulkMetricsSchema,
changePasswordSchema,

View File

@ -1,7 +1,7 @@
import { FromSchema } from 'json-schema-to-ts';
export const archiveFeaturesSchema = {
$id: '#/components/schemas/archiveFeaturesSchema',
export const batchFeaturesSchema = {
$id: '#/components/schemas/batchFeaturesSchema',
type: 'object',
required: ['features'],
properties: {
@ -17,4 +17,4 @@ export const archiveFeaturesSchema = {
},
} as const;
export type ArchiveFeaturesSchema = FromSchema<typeof archiveFeaturesSchema>;
export type BatchFeaturesSchema = FromSchema<typeof batchFeaturesSchema>;

View File

@ -0,0 +1,23 @@
import { FromSchema } from 'json-schema-to-ts';
export const batchStaleSchema = {
$id: '#/components/schemas/batchStaleSchema',
type: 'object',
required: ['features', 'stale'],
properties: {
features: {
type: 'array',
items: {
type: 'string',
},
},
stale: {
type: 'boolean',
},
},
components: {
schemas: {},
},
} as const;
export type BatchStaleSchema = FromSchema<typeof batchStaleSchema>;

View File

@ -131,4 +131,4 @@ export * from './import-toggles-validate-schema';
export * from './import-toggles-schema';
export * from './stickiness-schema';
export * from './tags-bulk-add-schema';
export * from './archive-features-schema';
export * from './batch-features-schema';

View File

@ -20,7 +20,7 @@ import { extractUsername } from '../../../util';
import { IAuthRequest } from '../../unleash-types';
import {
AdminFeaturesQuerySchema,
ArchiveFeaturesSchema,
BatchFeaturesSchema,
CreateFeatureSchema,
CreateFeatureStrategySchema,
createRequestSchema,
@ -46,6 +46,7 @@ import {
} from '../../../services';
import { querySchema } from '../../../schema/feature-schema';
import NotFoundError from '../../../error/notfound-error';
import { BatchStaleSchema } from '../../../openapi/spec/batch-stale-schema';
interface FeatureStrategyParams {
projectId: string;
@ -76,6 +77,7 @@ export interface IFeatureProjectUserParams extends ProjectParam {
const PATH = '/:projectId/features';
const PATH_ARCHIVE = '/:projectId/archive';
const PATH_STALE = '/:projectId/stale';
const PATH_FEATURE = `${PATH}/:featureName`;
const PATH_FEATURE_CLONE = `${PATH_FEATURE}/clone`;
const PATH_ENV = `${PATH_FEATURE}/environments/:environment`;
@ -418,8 +420,24 @@ export default class ProjectFeaturesController extends Controller {
operationId: 'archiveFeatures',
description:
'This endpoint archives the specified features.',
summary: 'Archive a list of features',
requestBody: createRequestSchema('archiveFeaturesSchema'),
summary: 'Archives a list of features',
requestBody: createRequestSchema('batchFeaturesSchema'),
responses: { 202: emptyResponse },
}),
],
});
this.route({
method: 'post',
path: PATH_STALE,
handler: this.staleFeatures,
permission: UPDATE_FEATURE,
middleware: [
openApiService.validPath({
tags: ['Features'],
operationId: 'staleFeatures',
description: 'This endpoint stales the specified features.',
summary: 'Stales a list of features',
requestBody: createRequestSchema('batchStaleSchema'),
responses: { 202: emptyResponse },
}),
],
@ -603,7 +621,7 @@ export default class ProjectFeaturesController extends Controller {
}
async archiveFeatures(
req: IAuthRequest<{ projectId: string }, void, ArchiveFeaturesSchema>,
req: IAuthRequest<{ projectId: string }, void, BatchFeaturesSchema>,
res: Response,
): Promise<void> {
if (!this.flagResolver.isEnabled('bulkOperations')) {
@ -618,6 +636,27 @@ export default class ProjectFeaturesController extends Controller {
res.status(202).end();
}
async staleFeatures(
req: IAuthRequest<{ projectId: string }, void, BatchStaleSchema>,
res: Response,
): Promise<void> {
if (!this.flagResolver.isEnabled('bulkOperations')) {
throw new NotFoundError('Bulk operations are not enabled');
}
const { features, stale } = req.body;
const { projectId } = req.params;
const userName = extractUsername(req);
await this.featureService.setToggleStaleness(
features,
stale,
userName,
projectId,
);
res.status(202).end();
}
async getFeatureEnvironment(
req: Request<FeatureStrategyParams, any, any, any>,
res: Response<FeatureEnvironmentSchema>,

View File

@ -1101,6 +1101,37 @@ class FeatureToggleService {
);
}
async setToggleStaleness(
featureNames: string[],
stale: boolean,
createdBy: string,
projectId: string,
): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId);
const features = await this.featureToggleStore.getAllByNames(
featureNames,
);
const relevantFeatures = features.filter(
(feature) => feature.stale !== stale,
);
await this.featureToggleStore.batchStale(
relevantFeatures.map((feature) => feature.name),
stale,
);
await this.eventStore.batchStore(
relevantFeatures.map(
(feature) =>
new FeatureStaleEvent({
stale: stale,
project: projectId,
featureName: feature.name,
createdBy,
}),
),
);
}
async updateEnabled(
project: string,
featureName: string,

View File

@ -163,7 +163,7 @@ export class FeatureStaleEvent extends BaseEvent {
project: string;
featureName: string;
createdBy: string | IUser;
tags: ITag[];
tags?: ITag[];
}) {
super(
p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,

View File

@ -16,6 +16,10 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
update(project: string, data: FeatureToggleDTO): Promise<FeatureToggle>;
archive(featureName: string): Promise<FeatureToggle>;
batchArchive(featureNames: string[]): Promise<FeatureToggle[]>;
batchStale(
featureNames: string[],
stale: boolean,
): Promise<FeatureToggle[]>;
revive(featureName: string): Promise<FeatureToggle>;
getAll(query?: Partial<IFeatureToggleQuery>): Promise<FeatureToggle[]>;
getAllByNames(names: string[]): Promise<FeatureToggle[]>;

View File

@ -2845,3 +2845,26 @@ test('Should be able to bulk archive features', async () => {
features: [{}, { name: featureName1 }, { name: featureName2 }],
});
});
test('Should batch stale features', async () => {
const staledFeatureName1 = 'staledFeature1';
const staledFeatureName2 = 'staledFeature2';
await createFeatureToggle(staledFeatureName1);
await createFeatureToggle(staledFeatureName2);
await app.request
.post(`/api/admin/projects/${DEFAULT_PROJECT}/stale`)
.send({
features: [staledFeatureName1, staledFeatureName2],
stale: true,
})
.expect(202);
const { body } = await app.request
.get(
`/api/admin/projects/${DEFAULT_PROJECT}/features/${staledFeatureName1}`,
)
.expect(200);
expect(body.stale).toBeTruthy();
});

View File

@ -328,7 +328,7 @@ exports[`should serve the OpenAPI spec 1`] = `
},
"type": "object",
},
"archiveFeaturesSchema": {
"batchFeaturesSchema": {
"properties": {
"features": {
"items": {
@ -342,6 +342,24 @@ exports[`should serve the OpenAPI spec 1`] = `
],
"type": "object",
},
"batchStaleSchema": {
"properties": {
"features": {
"items": {
"type": "string",
},
"type": "array",
},
"stale": {
"type": "boolean",
},
},
"required": [
"features",
"stale",
],
"type": "object",
},
"bulkMetricsSchema": {
"properties": {
"applications": {
@ -5998,11 +6016,11 @@ If the provided project does not exist, the list of events will be empty.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/archiveFeaturesSchema",
"$ref": "#/components/schemas/batchFeaturesSchema",
},
},
},
"description": "archiveFeaturesSchema",
"description": "batchFeaturesSchema",
"required": true,
},
"responses": {
@ -6010,7 +6028,7 @@ If the provided project does not exist, the list of events will be empty.",
"description": "This response has no body.",
},
},
"summary": "Archive a list of features",
"summary": "Archives a list of features",
"tags": [
"Features",
],
@ -7396,6 +7414,42 @@ If the provided project does not exist, the list of events will be empty.",
],
},
},
"/api/admin/projects/{projectId}/stale": {
"post": {
"description": "This endpoint stales the specified features.",
"operationId": "staleFeatures",
"parameters": [
{
"in": "path",
"name": "projectId",
"required": true,
"schema": {
"type": "string",
},
},
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/batchStaleSchema",
},
},
},
"description": "batchStaleSchema",
"required": true,
},
"responses": {
"202": {
"description": "This response has no body.",
},
},
"summary": "Stales a list of features",
"tags": [
"Features",
],
},
},
"/api/admin/projects/{projectId}/stickiness": {
"get": {
"operationId": "getProjectDefaultStickiness",

View File

@ -34,6 +34,19 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
return features;
}
async batchStale(
featureNames: string[],
stale: boolean,
): Promise<FeatureToggle[]> {
const features = this.features.filter((feature) =>
featureNames.includes(feature.name),
);
for (const feature of features) {
feature.stale = stale;
}
return features;
}
async count(query: Partial<IFeatureToggleQuery>): Promise<number> {
return this.features.filter(this.getFilterQuery(query)).length;
}