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:
parent
3e9d88f789
commit
f45454fbfd
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
@ -0,0 +1,3 @@
|
||||
export interface IChangeRequestSegmentUsageReadModel {
|
||||
isSegmentUsedInActiveChangeRequests(segmentId: number): Promise<boolean>;
|
||||
}
|
@ -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();
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user