mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +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 dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
import {
|
import {
|
||||||
|
type IExperimentalOptions,
|
||||||
type IUnleashStores,
|
type IUnleashStores,
|
||||||
SYSTEM_USER,
|
SYSTEM_USER,
|
||||||
SYSTEM_USER_AUDIT,
|
SYSTEM_USER_AUDIT,
|
||||||
@ -17,9 +18,15 @@ let service: EnvironmentService;
|
|||||||
let eventService: EventService;
|
let eventService: EventService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
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, {
|
db = await dbInit('environment_service_serial', config.getLogger, {
|
||||||
dbInitMethod: 'legacy' as const,
|
dbInitMethod: 'legacy' as const,
|
||||||
|
experimental: flags,
|
||||||
});
|
});
|
||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
eventService = createEventsService(db.rawDatabase, config);
|
eventService = createEventsService(db.rawDatabase, config);
|
||||||
@ -49,6 +56,34 @@ test('Can get all', async () => {
|
|||||||
expect(environments).toHaveLength(3); // the one we created plus 'default'
|
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 () => {
|
test('Can connect environment to project', async () => {
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: 'test-connection',
|
name: 'test-connection',
|
||||||
|
@ -9,7 +9,7 @@ export interface IEnvironmentStore extends Store<IEnvironment, string> {
|
|||||||
exists(name: string): Promise<boolean>;
|
exists(name: string): Promise<boolean>;
|
||||||
create(env: IEnvironmentCreate): Promise<IEnvironment>;
|
create(env: IEnvironmentCreate): Promise<IEnvironment>;
|
||||||
update(
|
update(
|
||||||
env: Pick<IEnvironment, 'type' | 'protected'>,
|
env: Pick<IEnvironment, 'type' | 'protected' | 'requiredApprovals'>,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<IEnvironment>;
|
): Promise<IEnvironment>;
|
||||||
updateProperty(
|
updateProperty(
|
||||||
|
@ -12,7 +12,7 @@ import NotFoundError from '../../error/notfound-error';
|
|||||||
import type { IEnvironmentStore } from './environment-store-type';
|
import type { IEnvironmentStore } from './environment-store-type';
|
||||||
import { snakeCaseKeys } from '../../util/snakeCase';
|
import { snakeCaseKeys } from '../../util/snakeCase';
|
||||||
import type { CreateFeatureStrategySchema } from '../../openapi';
|
import type { CreateFeatureStrategySchema } from '../../openapi';
|
||||||
import type { IUnleashConfig } from '../../types';
|
import type { IFlagResolver, IUnleashConfig } from '../../types';
|
||||||
|
|
||||||
interface IEnvironmentsTable {
|
interface IEnvironmentsTable {
|
||||||
name: string;
|
name: string;
|
||||||
@ -21,6 +21,7 @@ interface IEnvironmentsTable {
|
|||||||
sort_order: number;
|
sort_order: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
protected: boolean;
|
protected: boolean;
|
||||||
|
required_approvals?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IEnvironmentsWithCountsTable extends IEnvironmentsTable {
|
interface IEnvironmentsWithCountsTable extends IEnvironmentsTable {
|
||||||
@ -51,6 +52,7 @@ function mapRow(row: IEnvironmentsTable): IEnvironment {
|
|||||||
sortOrder: row.sort_order,
|
sortOrder: row.sort_order,
|
||||||
enabled: row.enabled,
|
enabled: row.enabled,
|
||||||
protected: row.protected,
|
protected: row.protected,
|
||||||
|
requiredApprovals: row.required_approvals,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +97,7 @@ function fieldToRow(env: IEnvironment): IEnvironmentsTable {
|
|||||||
sort_order: env.sortOrder,
|
sort_order: env.sortOrder,
|
||||||
enabled: env.enabled,
|
enabled: env.enabled,
|
||||||
protected: env.protected,
|
protected: env.protected,
|
||||||
|
required_approvals: env.requiredApprovals,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +106,8 @@ const TABLE = 'environments';
|
|||||||
export default class EnvironmentStore implements IEnvironmentStore {
|
export default class EnvironmentStore implements IEnvironmentStore {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
private db: Db;
|
private db: Db;
|
||||||
|
|
||||||
private isOss: boolean;
|
private isOss: boolean;
|
||||||
@ -112,11 +117,16 @@ export default class EnvironmentStore implements IEnvironmentStore {
|
|||||||
constructor(
|
constructor(
|
||||||
db: Db,
|
db: Db,
|
||||||
eventBus: EventEmitter,
|
eventBus: EventEmitter,
|
||||||
{ getLogger, isOss }: Pick<IUnleashConfig, 'getLogger' | 'isOss'>,
|
{
|
||||||
|
getLogger,
|
||||||
|
isOss,
|
||||||
|
flagResolver,
|
||||||
|
}: Pick<IUnleashConfig, 'getLogger' | 'isOss' | 'flagResolver'>,
|
||||||
) {
|
) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.logger = getLogger('db/environment-store.ts');
|
this.logger = getLogger('db/environment-store.ts');
|
||||||
this.isOss = isOss;
|
this.isOss = isOss;
|
||||||
|
this.flagResolver = flagResolver;
|
||||||
this.timer = (action) =>
|
this.timer = (action) =>
|
||||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||||
store: 'environment',
|
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(
|
async importEnvironments(
|
||||||
environments: IEnvironment[],
|
environments: IEnvironment[],
|
||||||
): Promise<IEnvironment[]> {
|
): Promise<IEnvironment[]> {
|
||||||
const rows = await this.db(TABLE)
|
const rows = await this.db(TABLE)
|
||||||
.insert(environments.map(fieldToRow))
|
.insert(environments.map(fieldToRow))
|
||||||
.returning(COLUMNS)
|
.returning(this.allColumns())
|
||||||
.onConflict('name')
|
.onConflict('name')
|
||||||
.ignore();
|
.ignore();
|
||||||
|
|
||||||
@ -318,13 +334,13 @@ export default class EnvironmentStore implements IEnvironmentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
env: Pick<IEnvironment, 'type' | 'protected'>,
|
env: Pick<IEnvironment, 'type' | 'protected' | 'requiredApprovals'>,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<IEnvironment> {
|
): Promise<IEnvironment> {
|
||||||
const updatedEnv = await this.db<IEnvironmentsTable>(TABLE)
|
const updatedEnv = await this.db<IEnvironmentsTable>(TABLE)
|
||||||
.update(snakeCaseKeys(env))
|
.update(snakeCaseKeys(env))
|
||||||
.where({ name, protected: false })
|
.where({ name, protected: false })
|
||||||
.returning<IEnvironmentsTable>(COLUMNS);
|
.returning<IEnvironmentsTable>(this.allColumns());
|
||||||
|
|
||||||
return mapRow(updatedEnv[0]);
|
return mapRow(updatedEnv[0]);
|
||||||
}
|
}
|
||||||
@ -332,7 +348,7 @@ export default class EnvironmentStore implements IEnvironmentStore {
|
|||||||
async create(env: IEnvironmentCreate): Promise<IEnvironment> {
|
async create(env: IEnvironmentCreate): Promise<IEnvironment> {
|
||||||
const row = await this.db<IEnvironmentsTable>(TABLE)
|
const row = await this.db<IEnvironmentsTable>(TABLE)
|
||||||
.insert(snakeCaseKeys(env))
|
.insert(snakeCaseKeys(env))
|
||||||
.returning<IEnvironmentsTable>(COLUMNS);
|
.returning<IEnvironmentsTable>(this.allColumns());
|
||||||
|
|
||||||
return mapRow(row[0]);
|
return mapRow(row[0]);
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
env: Pick<IEnvironment, 'type' | 'protected'>,
|
env: Pick<IEnvironment, 'type' | 'protected' | 'requiredApprovals'>,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<IEnvironment> {
|
): Promise<IEnvironment> {
|
||||||
const found = this.environments.find(
|
const found = this.environments.find(
|
||||||
|
@ -58,6 +58,14 @@ export const environmentSchema = {
|
|||||||
description:
|
description:
|
||||||
'The number of enabled toggles for the project environment',
|
'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: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -44,6 +44,7 @@ test('Can list all existing environments', async () => {
|
|||||||
sortOrder: 1,
|
sortOrder: 1,
|
||||||
type: 'production',
|
type: 'production',
|
||||||
protected: true,
|
protected: true,
|
||||||
|
requiredApprovals: null,
|
||||||
projectCount: 1,
|
projectCount: 1,
|
||||||
apiTokenCount: 0,
|
apiTokenCount: 0,
|
||||||
enabledToggleCount: 0,
|
enabledToggleCount: 0,
|
||||||
|
Loading…
Reference in New Issue
Block a user