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:
parent
497cbcdef2
commit
4677b28aee
@ -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',
|
||||
|
@ -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(
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user