1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

refactor: extract segment usage read model (#5301)

This PR adds a way to tell if a specific segment is being used in any
active change requests. It's the first step towards preventing segments
that are being used in change requests from being deleted.

It does that by checking the db for any unclosed CRs and using those CR
ids to look for "addStrategy" and "updateStrategy" events in the cr
events table.

## Upcoming PRs

This only puts in a way to detect it, but doesn't add that to anything.
That'll be in an upcoming iteration.
This commit is contained in:
Thomas Heartman 2023-11-08 14:50:12 +01:00 committed by GitHub
parent 3e9d88f789
commit f45454fbfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 199 additions and 0 deletions

View File

@ -0,0 +1,135 @@
import { IUser } from 'lib/server-impl';
import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
import getLogger from '../../../test/fixtures/no-logger';
import { IChangeRequestSegmentUsageReadModel } from './change-request-segment-usage-read-model';
import { createChangeRequestSegmentUsageModel } from './createChangeRequestSegmentUsageReadModel';
import { randomId } from '../../../lib/util';
let db: ITestDb;
let user: IUser;
const CR_ID = 123456;
const FLAG_NAME = 'crarm-test-flag';
let readModel: IChangeRequestSegmentUsageReadModel;
beforeAll(async () => {
db = await dbInit('change_request_access_read_model_serial', getLogger);
user = await db.stores.userStore.insert({
username: 'cr-creator',
});
readModel = createChangeRequestSegmentUsageModel(db.rawDatabase);
await db.stores.featureToggleStore.create('default', {
name: FLAG_NAME,
});
});
afterAll(async () => {
await db.destroy();
});
afterEach(async () => {
await db.rawDatabase.table('change_requests').where('id', CR_ID).delete();
await db.rawDatabase
.table('change_request_events')
.where('change_request_id', CR_ID)
.delete();
});
const createCR = async (state) => {
await db.rawDatabase.table('change_requests').insert({
id: CR_ID,
environment: 'default',
state,
project: 'default',
created_by: user.id,
created_at: '2023-01-01 00:00:00',
min_approvals: 1,
title: 'My change request',
});
};
const addChangeRequestChange = async (flagName, action, change) => {
await db.rawDatabase.table('change_request_events').insert({
feature: flagName,
action,
payload: change,
created_at: '2023-01-01 00:01:00',
change_request_id: CR_ID,
created_by: user.id,
});
};
const addStrategyToCr = async (segmentId: number, flagName: string) => {
await addChangeRequestChange(flagName, 'addStrategy', {
name: 'flexibleRollout',
title: '',
disabled: false,
segments: [segmentId],
variants: [],
parameters: {
groupId: flagName,
rollout: '100',
stickiness: 'default',
},
constraints: [],
});
};
const updateStrategyInCr = async (
strategyId: string,
segmentId: number,
flagName: string,
) => {
await addChangeRequestChange(flagName, 'updateStrategy', {
id: strategyId,
name: 'flexibleRollout',
title: '',
disabled: false,
segments: [segmentId],
variants: [],
parameters: {
groupId: flagName,
rollout: '100',
stickiness: 'default',
},
constraints: [],
});
};
describe.each([
[
'updateStrategy',
(segmentId: number) =>
updateStrategyInCr(randomId(), segmentId, FLAG_NAME),
],
[
'addStrategy',
(segmentId: number) => addStrategyToCr(segmentId, FLAG_NAME),
],
])('Should handle %s changes correctly', (_, addOrUpdateStrategy) => {
test.each([
['Draft', true],
['In Review', true],
['Scheduled', true],
['Approved', true],
['Rejected', false],
['Cancelled', false],
['Applied', false],
])(
'Changes in %s CRs should make it %s',
async (state, expectedOutcome) => {
await createCR(state);
const segmentId = 3;
await addOrUpdateStrategy(segmentId);
expect(
await readModel.isSegmentUsedInActiveChangeRequests(segmentId),
).toBe(expectedOutcome);
},
);
});

View File

@ -0,0 +1,3 @@
export interface IChangeRequestSegmentUsageReadModel {
isSegmentUsedInActiveChangeRequests(segmentId: number): Promise<boolean>;
}

View File

@ -0,0 +1,15 @@
import { Db } from 'lib/server-impl';
import { ChangeRequestSegmentUsageReadModel } from './sql-change-request-segment-usage-read-model';
import { FakeChangeRequestSegmentUsageReadModel } from './fake-change-request-segment-usage-read-model';
import { IChangeRequestSegmentUsageReadModel } from './change-request-segment-usage-read-model';
export const createChangeRequestSegmentUsageModel = (
db: Db,
): IChangeRequestSegmentUsageReadModel => {
return new ChangeRequestSegmentUsageReadModel(db);
};
export const createFakeChangeRequestAccessService =
(): IChangeRequestSegmentUsageReadModel => {
return new FakeChangeRequestSegmentUsageReadModel();
};

View File

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

View File

@ -0,0 +1,30 @@
import { Db } from '../../db/db';
import { IChangeRequestSegmentUsageReadModel } from './change-request-segment-usage-read-model';
export class ChangeRequestSegmentUsageReadModel
implements IChangeRequestSegmentUsageReadModel
{
private db: Db;
constructor(db: Db) {
this.db = db;
}
public async isSegmentUsedInActiveChangeRequests(
segmentId: number,
): Promise<boolean> {
const result = await this.db.raw(
`SELECT events.*
FROM change_request_events events
JOIN change_requests cr ON events.change_request_id = cr.id
WHERE cr.state IN ('Draft', 'In Review', 'Scheduled', 'Approved')
AND events.action IN ('updateStrategy', 'addStrategy');`,
);
const isUsed = result.rows.some((row) =>
row.payload?.segments?.includes(segmentId),
);
return isUsed;
}
}