diff --git a/src/lib/db/feature-strategy-store.ts b/src/lib/db/feature-strategy-store.ts index 7ccc0bab3f..df04e4bc9b 100644 --- a/src/lib/db/feature-strategy-store.ts +++ b/src/lib/db/feature-strategy-store.ts @@ -47,6 +47,7 @@ interface IFeatureStrategiesTable { strategy_name: string; parameters: object; constraints: string; + sort_order: number; created_at?: Date; } @@ -60,6 +61,7 @@ function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy { parameters: row.parameters, constraints: (row.constraints as unknown as IConstraint[]) || [], createdAt: row.created_at, + sortOrder: row.sort_order, }; } @@ -73,6 +75,7 @@ function mapInput(input: IFeatureStrategy): IFeatureStrategiesTable { parameters: input.parameters, constraints: JSON.stringify(input.constraints || []), created_at: input.createdAt, + sort_order: input.sortOrder, }; } @@ -179,13 +182,13 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { environment: string, ): Promise { const stopTimer = this.timer('getForFeature'); - const rows = await this.db( - T.featureStrategies, - ).where({ - project_name: projectId, - feature_name: featureName, - environment, - }); + const rows = await this.db(T.featureStrategies) + .where({ + project_name: projectId, + feature_name: featureName, + environment, + }) + .orderBy('sort_order', 'asc'); stopTimer(); return rows.map(mapRow); } @@ -211,6 +214,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { 'feature_strategies.strategy_name as strategy_name', 'feature_strategies.parameters as parameters', 'feature_strategies.constraints as constraints', + 'feature_strategies.sort_order as sort_order', ) .fullOuterJoin( 'feature_environments', @@ -262,6 +266,12 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { featureToggle.environments = Object.values( featureToggle.environments, ); + featureToggle.environments = featureToggle.environments.map((e) => { + e.strategies = e.strategies.sort( + (a, b) => a.sortOrder - b.sortOrder, + ); + return e; + }); featureToggle.archived = archived; return featureToggle; } @@ -359,6 +369,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { name: r.strategy_name, constraints: r.constraints || [], parameters: r.parameters, + sortOrder: r.sort_order, id: r.strategy_id, }; if (!includeId) { diff --git a/src/lib/routes/admin-api/project/features.ts b/src/lib/routes/admin-api/project/features.ts index ad8a54717e..65a24c6ded 100644 --- a/src/lib/routes/admin-api/project/features.ts +++ b/src/lib/routes/admin-api/project/features.ts @@ -22,6 +22,7 @@ interface FeatureStrategyParams { projectId: string; featureName: string; environment: string; + sortOrder?: number; } interface FeatureParams extends ProjectParam { diff --git a/src/lib/services/feature-toggle-service-v2.ts b/src/lib/services/feature-toggle-service-v2.ts index e957bf1c94..a7d7d816d4 100644 --- a/src/lib/services/feature-toggle-service-v2.ts +++ b/src/lib/services/feature-toggle-service-v2.ts @@ -106,6 +106,7 @@ class FeatureToggleServiceV2 { strategyName: strategyConfig.name, constraints: strategyConfig.constraints, parameters: strategyConfig.parameters, + sortOrder: strategyConfig.sortOrder, projectId, featureName, environment, @@ -212,6 +213,7 @@ class FeatureToggleServiceV2 { name: strat.strategyName, constraints: strat.constraints, parameters: strat.parameters, + sortOrder: strat.sortOrder, })); } throw new NotFoundError( diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index f1d3d2daa5..67d0d6c38b 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -14,6 +14,7 @@ export interface IStrategyConfig { name: string; constraints: IConstraint[]; parameters: Object; + sortOrder?: number; } export interface IFeatureStrategy { id: string; @@ -22,6 +23,7 @@ export interface IFeatureStrategy { environment: string; strategyName: string; parameters: object; + sortOrder?: number; constraints: IConstraint[]; createdAt?: Date; } diff --git a/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts b/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts index c7f76f3241..3815950798 100644 --- a/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts +++ b/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts @@ -4,6 +4,9 @@ import getLogger from '../../../../fixtures/no-logger'; let app: IUnleashTest; let db: ITestDb; +const sortOrderFirst = 0; +const sortOrderSecond = 10; +const sortOrderDefault = 9999; beforeAll(async () => { db = await dbInit('feature_strategy_api_serial', getLogger); @@ -31,6 +34,44 @@ afterAll(async () => { await db.destroy(); }); +async function addStrategies(featureName: string, envName: string) { + await app.request + .post( + `/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`, + ) + .send({ + name: 'default', + parameters: { + userId: 'string', + }, + }) + .expect(200); + await app.request + .post( + `/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`, + ) + .send({ + name: 'default', + parameters: { + userId: 'string', + }, + sortOrder: sortOrderFirst, + }) + .expect(200); + await app.request + .post( + `/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`, + ) + .send({ + name: 'default', + parameters: { + userId: 'string', + }, + sortOrder: sortOrderSecond, + }) + .expect(200); +} + test('Trying to add a strategy configuration to environment not connected to toggle should fail', async () => { await app.request .post('/api/admin/features') @@ -842,3 +883,84 @@ test('Can delete strategy from feature toggle', async () => { ) .expect(200); }); + +test('List of strategies should respect sortOrder', async () => { + const envName = 'sortOrderdel-strategy'; + const featureName = 'feature.sort.order.one'; + // Create environment + await db.stores.environmentStore.create({ + name: envName, + displayName: 'Enable feature for environment', + type: 'test', + }); + // Connect environment to project + await app.request + .post('/api/admin/projects/default/environments') + .send({ + environment: envName, + }) + .expect(200); + await app.request + .post('/api/admin/projects/default/features') + .send({ name: featureName }) + .expect(201); + await addStrategies(featureName, envName); + const { body } = await app.request.get( + `/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`, + ); + const strategies = body; + expect(strategies[0].sortOrder).toBe(sortOrderFirst); + expect(strategies[1].sortOrder).toBe(sortOrderSecond); + expect(strategies[2].sortOrder).toBe(sortOrderDefault); +}); + +test('Feature strategies list should respect strategy sortorders for each environment', async () => { + const envName = 'sort-order-within-environment-one'; + const secondEnv = 'sort-order-within-environment-two'; + const featureName = 'feature.sort.order.environment.list'; + // Create environment + await db.stores.environmentStore.create({ + name: envName, + displayName: 'Sort orders within environment', + type: 'test', + }); + await db.stores.environmentStore.create({ + name: secondEnv, + displayName: 'Sort orders within environment', + type: 'test', + }); + // Connect environment to project + await app.request + .post('/api/admin/projects/default/environments') + .send({ + environment: envName, + }) + .expect(200); + await app.request + .post('/api/admin/projects/default/environments') + .send({ + environment: secondEnv, + }) + .expect(200); + + await app.request + .post('/api/admin/projects/default/features') + .send({ name: featureName }) + .expect(201); + + await addStrategies(featureName, envName); + await addStrategies(featureName, secondEnv); + + const response = await app.request.get( + `/api/admin/projects/default/features/${featureName}`, + ); + const { body } = response; + let { strategies } = body.environments.find((e) => e.name === envName); + expect(strategies[0].sortOrder).toBe(sortOrderFirst); + expect(strategies[1].sortOrder).toBe(sortOrderSecond); + expect(strategies[2].sortOrder).toBe(sortOrderDefault); + strategies = body.environments.find((e) => e.name === secondEnv).strategies; + expect(strategies[0].sortOrder).toBe(sortOrderFirst); + expect(strategies[1].sortOrder).toBe(sortOrderSecond); + expect(strategies[2].sortOrder).toBe(sortOrderDefault); +});