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

chore: find segment strategies in CRs (#5365)

This PR adds the ability to detect which strategies use a specific
segment in active change requests.

It does not wire this functionality up to anything just yet. Follow-up
PRs will integrate this with the segment service and eventually with the
front end.
This commit is contained in:
Thomas Heartman 2023-11-21 10:29:43 +01:00 committed by GitHub
parent 7a8c8c8d29
commit 27252f7728
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 8 deletions

View File

@ -113,7 +113,7 @@ describe.each([
])('Should handle %s changes correctly', (_, addOrUpdateStrategy) => { ])('Should handle %s changes correctly', (_, addOrUpdateStrategy) => {
test.each([ test.each([
['Draft', true], ['Draft', true],
['In Review', true], ['In review', true],
['Scheduled', true], ['Scheduled', true],
['Approved', true], ['Approved', true],
['Rejected', false], ['Rejected', false],
@ -133,3 +133,76 @@ describe.each([
}, },
); );
}); });
test.each([
['Draft', true],
['In review', true],
['Scheduled', true],
['Approved', true],
['Rejected', false],
['Cancelled', false],
['Applied', false],
])(
'addStrategy events in %s CRs should show up only of the CR is active',
async (state, isActiveCr) => {
await createCR(state);
const segmentId = 3;
await addStrategyToCr(segmentId, FLAG_NAME);
const result = await readModel.getStrategiesUsedInActiveChangeRequests(
segmentId,
);
if (isActiveCr) {
expect(result).toStrictEqual([
{
projectId: 'default',
strategyName: 'flexibleRollout',
environment: 'default',
featureName: FLAG_NAME,
},
]);
} else {
expect(result).toStrictEqual([]);
}
},
);
test.each([
['Draft', true],
['In review', true],
['Scheduled', true],
['Approved', true],
['Rejected', false],
['Cancelled', false],
['Applied', false],
])(
`updateStrategy events in %s CRs should show up only of the CR is active`,
async (state, isActiveCr) => {
await createCR(state);
const segmentId = 3;
const strategyId = randomId();
await updateStrategyInCr(strategyId, segmentId, FLAG_NAME);
const result = await readModel.getStrategiesUsedInActiveChangeRequests(
segmentId,
);
if (isActiveCr) {
expect(result).toMatchObject([
{
id: strategyId,
projectId: 'default',
strategyName: 'flexibleRollout',
environment: 'default',
featureName: FLAG_NAME,
},
]);
} else {
expect(result).toStrictEqual([]);
}
},
);

View File

@ -1,3 +1,17 @@
type NewStrategy = {
projectId: string;
featureName: string;
strategyName: string;
environment: string;
};
type ExistingStrategy = NewStrategy & { id: string };
export type ChangeRequestStrategy = NewStrategy | ExistingStrategy;
export interface IChangeRequestSegmentUsageReadModel { export interface IChangeRequestSegmentUsageReadModel {
isSegmentUsedInActiveChangeRequests(segmentId: number): Promise<boolean>; isSegmentUsedInActiveChangeRequests(segmentId: number): Promise<boolean>;
getStrategiesUsedInActiveChangeRequests(
segmentId: number,
): Promise<ChangeRequestStrategy[]>;
} }

View File

@ -1,16 +1,32 @@
import { IChangeRequestSegmentUsageReadModel } from './change-request-segment-usage-read-model'; import {
ChangeRequestStrategy,
IChangeRequestSegmentUsageReadModel,
} from './change-request-segment-usage-read-model';
export class FakeChangeRequestSegmentUsageReadModel export class FakeChangeRequestSegmentUsageReadModel
implements IChangeRequestSegmentUsageReadModel implements IChangeRequestSegmentUsageReadModel
{ {
private isSegmentUsedInActiveChangeRequestsValue: boolean; private isSegmentUsedInActiveChangeRequestsValue: boolean;
strategiesUsedInActiveChangeRequests: ChangeRequestStrategy[];
constructor(isSegmentUsedInActiveChangeRequests = false) { constructor(
isSegmentUsedInActiveChangeRequests = false,
strategiesUsedInActiveChangeRequests = [],
) {
this.isSegmentUsedInActiveChangeRequestsValue = this.isSegmentUsedInActiveChangeRequestsValue =
isSegmentUsedInActiveChangeRequests; isSegmentUsedInActiveChangeRequests;
this.strategiesUsedInActiveChangeRequests =
strategiesUsedInActiveChangeRequests;
} }
public async isSegmentUsedInActiveChangeRequests(): Promise<boolean> { public async isSegmentUsedInActiveChangeRequests(): Promise<boolean> {
return this.isSegmentUsedInActiveChangeRequestsValue; return this.isSegmentUsedInActiveChangeRequestsValue;
} }
public async getStrategiesUsedInActiveChangeRequests(): Promise<
ChangeRequestStrategy[]
> {
return this.strategiesUsedInActiveChangeRequests;
}
} }

View File

@ -1,5 +1,8 @@
import { Db } from '../../db/db'; import { Db } from '../../db/db';
import { IChangeRequestSegmentUsageReadModel } from './change-request-segment-usage-read-model'; import {
ChangeRequestStrategy,
IChangeRequestSegmentUsageReadModel,
} from './change-request-segment-usage-read-model';
export class ChangeRequestSegmentUsageReadModel export class ChangeRequestSegmentUsageReadModel
implements IChangeRequestSegmentUsageReadModel implements IChangeRequestSegmentUsageReadModel
@ -9,7 +12,6 @@ export class ChangeRequestSegmentUsageReadModel
constructor(db: Db) { constructor(db: Db) {
this.db = db; this.db = db;
} }
public async isSegmentUsedInActiveChangeRequests( public async isSegmentUsedInActiveChangeRequests(
segmentId: number, segmentId: number,
): Promise<boolean> { ): Promise<boolean> {
@ -17,7 +19,7 @@ export class ChangeRequestSegmentUsageReadModel
`SELECT events.* `SELECT events.*
FROM change_request_events events FROM change_request_events events
JOIN change_requests cr ON events.change_request_id = cr.id JOIN change_requests cr ON events.change_request_id = cr.id
WHERE cr.state IN ('Draft', 'In Review', 'Scheduled', 'Approved') WHERE cr.state IN ('Draft', 'In review', 'Scheduled', 'Approved')
AND events.action IN ('updateStrategy', 'addStrategy');`, AND events.action IN ('updateStrategy', 'addStrategy');`,
); );
@ -27,4 +29,34 @@ export class ChangeRequestSegmentUsageReadModel
return isUsed; return isUsed;
} }
mapRow = (row): ChangeRequestStrategy => {
const { payload, project, environment, feature } = row;
return {
projectId: project,
featureName: feature,
environment: environment,
strategyName: payload.name,
...(payload.id ? { id: payload.id } : {}),
};
};
public async getStrategiesUsedInActiveChangeRequests(
segmentId: number,
): Promise<ChangeRequestStrategy[]> {
const query = this.db.raw(
`SELECT events.*, cr.project, cr.environment
FROM change_request_events events
JOIN change_requests cr ON events.change_request_id = cr.id
WHERE cr.state NOT IN ('Applied', 'Cancelled', 'Rejected')
AND events.action IN ('updateStrategy', 'addStrategy');`,
);
const queryResult = await query;
const strategies = queryResult.rows
.filter((row) => row.payload?.segments?.includes(segmentId))
.map(this.mapRow);
return strategies;
}
} }

View File

@ -26,7 +26,8 @@ export const segmentStrategiesSchema = {
}, },
featureName: { featureName: {
type: 'string', type: 'string',
description: 'The ID of the strategy', description:
'The name of the feature flag that this strategy belongs to.',
example: 'new-signup-flow', example: 'new-signup-flow',
}, },
projectId: { projectId: {

View File

@ -249,7 +249,7 @@ test('should not delete segments used by strategies in CRs', async () => {
await db.rawDatabase.table('change_requests').insert({ await db.rawDatabase.table('change_requests').insert({
id: CR_ID, id: CR_ID,
environment: 'default', environment: 'default',
state: 'In Review', state: 'In review',
project: 'default', project: 'default',
created_by: user.id, created_by: user.id,
created_at: '2023-01-01 00:00:00', created_at: '2023-01-01 00:00:00',