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:
parent
240bb7b027
commit
6c813ab066
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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>;
|
23
src/lib/openapi/spec/batch-stale-schema.ts
Normal file
23
src/lib/openapi/spec/batch-stale-schema.ts
Normal 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>;
|
@ -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';
|
||||
|
@ -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>,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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[]>;
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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",
|
||||
|
13
src/test/fixtures/fake-feature-toggle-store.ts
vendored
13
src/test/fixtures/fake-feature-toggle-store.ts
vendored
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user