1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: persist env required approvals (#9616)

This commit is contained in:
Mateusz Kwasniewski 2025-03-25 16:04:14 +01:00 committed by GitHub
parent 497cbcdef2
commit 4677b28aee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 69 additions and 9 deletions

View File

@ -3,6 +3,7 @@ import { createTestConfig } from '../../../test/config/test-config';
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import NotFoundError from '../../error/notfound-error';
import {
type IExperimentalOptions,
type IUnleashStores,
SYSTEM_USER,
SYSTEM_USER_AUDIT,
@ -17,9 +18,15 @@ let service: EnvironmentService;
let eventService: EventService;
beforeAll(async () => {
const config = createTestConfig();
const flags: Partial<IExperimentalOptions> = {
flags: { globalChangeRequestConfig: true },
};
const config = createTestConfig({
experimental: flags,
});
db = await dbInit('environment_service_serial', config.getLogger, {
dbInitMethod: 'legacy' as const,
experimental: flags,
});
stores = db.stores;
eventService = createEventsService(db.rawDatabase, config);
@ -49,6 +56,34 @@ test('Can get all', async () => {
expect(environments).toHaveLength(3); // the one we created plus 'default'
});
test('Can manage required approvals', async () => {
const created = await db.stores.environmentStore.create({
name: 'approval_env',
type: 'production',
requiredApprovals: 1,
});
const retrieved = await service.get('approval_env');
await db.stores.environmentStore.update(
{
type: 'production',
protected: false,
requiredApprovals: 2,
},
'approval_env',
);
const updated = await service.get('approval_env');
const groupRetrieved = (await service.getAll()).find(
(env) => env.name === 'approval_env',
);
expect(retrieved).toEqual(created);
expect(updated).toEqual({ ...created, requiredApprovals: 2 });
expect(groupRetrieved).toMatchObject({ ...created, requiredApprovals: 2 });
});
test('Can connect environment to project', async () => {
await db.stores.environmentStore.create({
name: 'test-connection',

View File

@ -9,7 +9,7 @@ export interface IEnvironmentStore extends Store<IEnvironment, string> {
exists(name: string): Promise<boolean>;
create(env: IEnvironmentCreate): Promise<IEnvironment>;
update(
env: Pick<IEnvironment, 'type' | 'protected'>,
env: Pick<IEnvironment, 'type' | 'protected' | 'requiredApprovals'>,
name: string,
): Promise<IEnvironment>;
updateProperty(

View File

@ -12,7 +12,7 @@ import NotFoundError from '../../error/notfound-error';
import type { IEnvironmentStore } from './environment-store-type';
import { snakeCaseKeys } from '../../util/snakeCase';
import type { CreateFeatureStrategySchema } from '../../openapi';
import type { IUnleashConfig } from '../../types';
import type { IFlagResolver, IUnleashConfig } from '../../types';
interface IEnvironmentsTable {
name: string;
@ -21,6 +21,7 @@ interface IEnvironmentsTable {
sort_order: number;
enabled: boolean;
protected: boolean;
required_approvals?: number;
}
interface IEnvironmentsWithCountsTable extends IEnvironmentsTable {
@ -51,6 +52,7 @@ function mapRow(row: IEnvironmentsTable): IEnvironment {
sortOrder: row.sort_order,
enabled: row.enabled,
protected: row.protected,
requiredApprovals: row.required_approvals,
};
}
@ -95,6 +97,7 @@ function fieldToRow(env: IEnvironment): IEnvironmentsTable {
sort_order: env.sortOrder,
enabled: env.enabled,
protected: env.protected,
required_approvals: env.requiredApprovals,
};
}
@ -103,6 +106,8 @@ const TABLE = 'environments';
export default class EnvironmentStore implements IEnvironmentStore {
private logger: Logger;
private flagResolver: IFlagResolver;
private db: Db;
private isOss: boolean;
@ -112,11 +117,16 @@ export default class EnvironmentStore implements IEnvironmentStore {
constructor(
db: Db,
eventBus: EventEmitter,
{ getLogger, isOss }: Pick<IUnleashConfig, 'getLogger' | 'isOss'>,
{
getLogger,
isOss,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'isOss' | 'flagResolver'>,
) {
this.db = db;
this.logger = getLogger('db/environment-store.ts');
this.isOss = isOss;
this.flagResolver = flagResolver;
this.timer = (action) =>
metricsHelper.wrapTimer(eventBus, DB_TIME, {
store: 'environment',
@ -124,12 +134,18 @@ export default class EnvironmentStore implements IEnvironmentStore {
});
}
private allColumns() {
return this.flagResolver.isEnabled('globalChangeRequestConfig')
? [...COLUMNS, 'required_approvals']
: COLUMNS;
}
async importEnvironments(
environments: IEnvironment[],
): Promise<IEnvironment[]> {
const rows = await this.db(TABLE)
.insert(environments.map(fieldToRow))
.returning(COLUMNS)
.returning(this.allColumns())
.onConflict('name')
.ignore();
@ -318,13 +334,13 @@ export default class EnvironmentStore implements IEnvironmentStore {
}
async update(
env: Pick<IEnvironment, 'type' | 'protected'>,
env: Pick<IEnvironment, 'type' | 'protected' | 'requiredApprovals'>,
name: string,
): Promise<IEnvironment> {
const updatedEnv = await this.db<IEnvironmentsTable>(TABLE)
.update(snakeCaseKeys(env))
.where({ name, protected: false })
.returning<IEnvironmentsTable>(COLUMNS);
.returning<IEnvironmentsTable>(this.allColumns());
return mapRow(updatedEnv[0]);
}
@ -332,7 +348,7 @@ export default class EnvironmentStore implements IEnvironmentStore {
async create(env: IEnvironmentCreate): Promise<IEnvironment> {
const row = await this.db<IEnvironmentsTable>(TABLE)
.insert(snakeCaseKeys(env))
.returning<IEnvironmentsTable>(COLUMNS);
.returning<IEnvironmentsTable>(this.allColumns());
return mapRow(row[0]);
}

View File

@ -60,7 +60,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
}
async update(
env: Pick<IEnvironment, 'type' | 'protected'>,
env: Pick<IEnvironment, 'type' | 'protected' | 'requiredApprovals'>,
name: string,
): Promise<IEnvironment> {
const found = this.environments.find(

View File

@ -58,6 +58,14 @@ export const environmentSchema = {
description:
'The number of enabled toggles for the project environment',
},
requiredApprovals: {
type: 'integer',
nullable: true,
description:
'Experimental field. The number of approvals required before a change request can be applied in this environment.',
minimum: 1,
example: 3,
},
},
components: {},
} as const;

View File

@ -44,6 +44,7 @@ test('Can list all existing environments', async () => {
sortOrder: 1,
type: 'production',
protected: true,
requiredApprovals: null,
projectCount: 1,
apiTokenCount: 0,
enabledToggleCount: 0,