mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-05 17:53:12 +02:00
Merge branch 'master' into fix/sort-order
This commit is contained in:
commit
240786ea37
@ -12,7 +12,7 @@
|
|||||||
"ecmaVersion": 2019,
|
"ecmaVersion": 2019,
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
"plugins": ["@typescript-eslint","prettier"],
|
"plugins": ["@typescript-eslint", "prettier", "import"],
|
||||||
"root": true,
|
"root": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-var-requires": 0,
|
"@typescript-eslint/no-var-requires": 0,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
 [](https://coveralls.io/github/Unleash/unleash?branch=master) [](https://www.npmjs.com/package/unleash-server) [](https://hub.docker.com/r/unleashorg/unleash-server)
|
 [](https://coveralls.io/github/Unleash/unleash?branch=master) [](https://www.npmjs.com/package/unleash-server) [](https://hub.docker.com/r/unleashorg/unleash-server)
|
||||||
|
|
||||||
[](https://www.heroku.com/deploy/?template=https://github.com/Unleash/unleash) [](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Unleash/unleash/tree/master&refcode=0e1d75187044) [](https://twitter.com/intent/follow?screen_name=getunleash)
|
[](https://www.heroku.com/deploy/?template=https://github.com/Unleash/unleash) [](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Unleash/unleash/tree/master&refcode=0e1d75187044) [](https://twitter.com/intent/follow?screen_name=getunleash)
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "unleash-server",
|
"name": "unleash-server",
|
||||||
"description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.",
|
"description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.",
|
||||||
"version": "4.2.0-0",
|
"version": "4.2.0-2",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"unleash",
|
"unleash",
|
||||||
"feature toggle",
|
"feature toggle",
|
||||||
@ -90,7 +90,7 @@
|
|||||||
"gravatar-url": "^3.1.0",
|
"gravatar-url": "^3.1.0",
|
||||||
"helmet": "^4.1.0",
|
"helmet": "^4.1.0",
|
||||||
"joi": "^17.3.0",
|
"joi": "^17.3.0",
|
||||||
"js-yaml": "^3.14.0",
|
"js-yaml": "^4.1.0",
|
||||||
"knex": "0.95.11",
|
"knex": "0.95.11",
|
||||||
"log4js": "^6.0.0",
|
"log4js": "^6.0.0",
|
||||||
"memoizee": "^0.4.15",
|
"memoizee": "^0.4.15",
|
||||||
@ -109,7 +109,7 @@
|
|||||||
"response-time": "^2.3.2",
|
"response-time": "^2.3.2",
|
||||||
"serve-favicon": "^2.5.0",
|
"serve-favicon": "^2.5.0",
|
||||||
"stoppable": "^1.1.0",
|
"stoppable": "^1.1.0",
|
||||||
"unleash-frontend": "4.2.0",
|
"unleash-frontend": "4.2.2",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -118,7 +118,7 @@
|
|||||||
"@types/express-session": "1.17.4",
|
"@types/express-session": "1.17.4",
|
||||||
"@types/faker": "5.5.8",
|
"@types/faker": "5.5.8",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/js-yaml": "3.12.7",
|
"@types/js-yaml": "4.0.3",
|
||||||
"@types/memoizee": "0.4.6",
|
"@types/memoizee": "0.4.6",
|
||||||
"@types/node": "16.6.1",
|
"@types/node": "16.6.1",
|
||||||
"@types/node-fetch": "2.5.12",
|
"@types/node-fetch": "2.5.12",
|
||||||
@ -127,23 +127,23 @@
|
|||||||
"@types/stoppable": "1.1.1",
|
"@types/stoppable": "1.1.1",
|
||||||
"@types/supertest": "2.0.11",
|
"@types/supertest": "2.0.11",
|
||||||
"@types/uuid": "8.3.1",
|
"@types/uuid": "8.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "4.32.0",
|
"@typescript-eslint/eslint-plugin": "4.33.0",
|
||||||
"@typescript-eslint/parser": "4.32.0",
|
"@typescript-eslint/parser": "4.33.0",
|
||||||
"copyfiles": "2.4.1",
|
"copyfiles": "2.4.1",
|
||||||
"coveralls": "3.1.1",
|
"coveralls": "3.1.1",
|
||||||
"del-cli": "4.0.1",
|
"del-cli": "4.0.1",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
"eslint-config-airbnb-base": "14.2.1",
|
"eslint-config-airbnb-base": "14.2.1",
|
||||||
"eslint-config-airbnb-typescript": "12.3.1",
|
"eslint-config-airbnb-typescript": "14.0.0",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"eslint-plugin-import": "2.24.2",
|
"eslint-plugin-import": "2.24.2",
|
||||||
"eslint-plugin-prettier": "4.0.0",
|
"eslint-plugin-prettier": "4.0.0",
|
||||||
"faker": "5.5.3",
|
"faker": "5.5.3",
|
||||||
"fetch-mock": "9.11.0",
|
"fetch-mock": "9.11.0",
|
||||||
"husky": "7.0.2",
|
"husky": "7.0.2",
|
||||||
"jest": "27.2.2",
|
"jest": "27.2.4",
|
||||||
"jest-fetch-mock": "3.0.3",
|
"jest-fetch-mock": "3.0.3",
|
||||||
"lint-staged": "11.1.2",
|
"lint-staged": "11.2.0",
|
||||||
"prettier": "2.4.1",
|
"prettier": "2.4.1",
|
||||||
"proxyquire": "2.1.3",
|
"proxyquire": "2.1.3",
|
||||||
"source-map-support": "0.5.20",
|
"source-map-support": "0.5.20",
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["config:base"],
|
||||||
"config:base"
|
|
||||||
],
|
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"matchPackagePatterns": ["*"],
|
||||||
"automerge": true
|
"automerge": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -4,14 +4,14 @@ import Addon from './addon';
|
|||||||
import slackDefinition from './slack-definition';
|
import slackDefinition from './slack-definition';
|
||||||
import { IAddonConfig, IEvent } from '../types/model';
|
import { IAddonConfig, IEvent } from '../types/model';
|
||||||
|
|
||||||
const {
|
import {
|
||||||
FEATURE_CREATED,
|
FEATURE_CREATED,
|
||||||
FEATURE_UPDATED,
|
FEATURE_UPDATED,
|
||||||
FEATURE_ARCHIVED,
|
FEATURE_ARCHIVED,
|
||||||
FEATURE_REVIVED,
|
FEATURE_REVIVED,
|
||||||
FEATURE_STALE_ON,
|
FEATURE_STALE_ON,
|
||||||
FEATURE_STALE_OFF,
|
FEATURE_STALE_OFF,
|
||||||
} = require('../types/events');
|
} from '../types/events';
|
||||||
|
|
||||||
export default class SlackAddon extends Addon {
|
export default class SlackAddon extends Addon {
|
||||||
unleashUrl: string;
|
unleashUrl: string;
|
||||||
@ -123,7 +123,7 @@ This was changed by ${createdBy}.`;
|
|||||||
const stale = data.stale ? '("stale")' : '';
|
const stale = data.stale ? '("stale")' : '';
|
||||||
const typeStr = `*Type*: ${data.type}`;
|
const typeStr = `*Type*: ${data.type}`;
|
||||||
const project = `*Project*: ${data.project}`;
|
const project = `*Project*: ${data.project}`;
|
||||||
const strategies = `*Activation strategies*: \`\`\`${YAML.safeDump(
|
const strategies = `*Activation strategies*: \`\`\`${YAML.dump(
|
||||||
data.strategies,
|
data.strategies,
|
||||||
{ skipInvalid: true },
|
{ skipInvalid: true },
|
||||||
)}\`\`\``;
|
)}\`\`\``;
|
||||||
|
@ -109,7 +109,7 @@ export default class TeamsAddon extends Addon {
|
|||||||
const { data } = event;
|
const { data } = event;
|
||||||
const typeStr = `*Type*: ${data.type}`;
|
const typeStr = `*Type*: ${data.type}`;
|
||||||
const project = `*Project*: ${data.project}`;
|
const project = `*Project*: ${data.project}`;
|
||||||
const strategies = `*Activation strategies*: \n${YAML.safeDump(
|
const strategies = `*Activation strategies*: \n${YAML.dump(
|
||||||
data.strategies,
|
data.strategies,
|
||||||
{ skipInvalid: true },
|
{ skipInvalid: true },
|
||||||
)}`;
|
)}`;
|
||||||
|
@ -10,7 +10,6 @@ import { snakeCaseKeys } from '../util/snakeCase';
|
|||||||
|
|
||||||
interface IEnvironmentsTable {
|
interface IEnvironmentsTable {
|
||||||
name: string;
|
name: string;
|
||||||
display_name: string;
|
|
||||||
created_at?: Date;
|
created_at?: Date;
|
||||||
type: string;
|
type: string;
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
@ -20,7 +19,6 @@ interface IEnvironmentsTable {
|
|||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
'type',
|
'type',
|
||||||
'display_name',
|
|
||||||
'name',
|
'name',
|
||||||
'created_at',
|
'created_at',
|
||||||
'sort_order',
|
'sort_order',
|
||||||
@ -31,7 +29,6 @@ const COLUMNS = [
|
|||||||
function mapRow(row: IEnvironmentsTable): IEnvironment {
|
function mapRow(row: IEnvironmentsTable): IEnvironment {
|
||||||
return {
|
return {
|
||||||
name: row.name,
|
name: row.name,
|
||||||
displayName: row.display_name,
|
|
||||||
type: row.type,
|
type: row.type,
|
||||||
sortOrder: row.sort_order,
|
sortOrder: row.sort_order,
|
||||||
enabled: row.enabled,
|
enabled: row.enabled,
|
||||||
@ -42,7 +39,6 @@ function mapRow(row: IEnvironmentsTable): IEnvironment {
|
|||||||
function fieldToRow(env: IEnvironment): IEnvironmentsTable {
|
function fieldToRow(env: IEnvironment): IEnvironmentsTable {
|
||||||
return {
|
return {
|
||||||
name: env.name,
|
name: env.name,
|
||||||
display_name: env.displayName,
|
|
||||||
type: env.type,
|
type: env.type,
|
||||||
sort_order: env.sortOrder,
|
sort_order: env.sortOrder,
|
||||||
enabled: env.enabled,
|
enabled: env.enabled,
|
||||||
@ -95,10 +91,14 @@ export default class EnvironmentStore implements IEnvironmentStore {
|
|||||||
throw new NotFoundError(`Could not find environment with name: ${key}`);
|
throw new NotFoundError(`Could not find environment with name: ${key}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<IEnvironment[]> {
|
async getAll(query?: Object): Promise<IEnvironment[]> {
|
||||||
const rows = await this.db<IEnvironmentsTable>(TABLE)
|
let qB = this.db<IEnvironmentsTable>(TABLE)
|
||||||
.select('*')
|
.select('*')
|
||||||
.orderBy('sort_order', 'created_at');
|
.orderBy('sort_order', 'created_at');
|
||||||
|
if (query) {
|
||||||
|
qB = qB.where(query);
|
||||||
|
}
|
||||||
|
const rows = await qB;
|
||||||
return rows.map(mapRow);
|
return rows.map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +144,7 @@ export default class EnvironmentStore implements IEnvironmentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
env: Pick<IEnvironment, 'displayName' | 'type' | 'protected'>,
|
env: Pick<IEnvironment, 'type' | 'protected'>,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<IEnvironment> {
|
): Promise<IEnvironment> {
|
||||||
const updatedEnv = await this.db<IEnvironmentsTable>(TABLE)
|
const updatedEnv = await this.db<IEnvironmentsTable>(TABLE)
|
||||||
|
@ -10,7 +10,10 @@ import { DB_TIME } from '../metric-events';
|
|||||||
import { IFeatureEnvironment } from '../types/model';
|
import { IFeatureEnvironment } from '../types/model';
|
||||||
import NotFoundError from '../error/notfound-error';
|
import NotFoundError from '../error/notfound-error';
|
||||||
|
|
||||||
const T = { featureEnvs: 'feature_environments' };
|
const T = {
|
||||||
|
featureEnvs: 'feature_environments',
|
||||||
|
featureStrategies: 'feature_strategies',
|
||||||
|
};
|
||||||
|
|
||||||
interface IFeatureEnvironmentRow {
|
interface IFeatureEnvironmentRow {
|
||||||
environment: string;
|
environment: string;
|
||||||
@ -92,6 +95,22 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async disableEnvironmentIfNoStrategies(
|
||||||
|
featureName: string,
|
||||||
|
environment: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const result = await this.db.raw(
|
||||||
|
`SELECT EXISTS (SELECT 1 FROM ${T.featureStrategies} WHERE feature_name = ? AND environment = ?) AS enabled`,
|
||||||
|
[featureName, environment],
|
||||||
|
);
|
||||||
|
const { enabled } = result.rows[0];
|
||||||
|
if (!enabled) {
|
||||||
|
await this.db(T.featureEnvs)
|
||||||
|
.update({ enabled: false })
|
||||||
|
.where({ feature_name: featureName, environment });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async addEnvironmentToFeature(
|
async addEnvironmentToFeature(
|
||||||
featureName: string,
|
featureName: string,
|
||||||
environment: string,
|
environment: string,
|
||||||
|
@ -213,7 +213,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
'environments.name as environment_name',
|
'environments.name as environment_name',
|
||||||
'environments.type as environment_type',
|
'environments.type as environment_type',
|
||||||
'environments.sort_order as environment_sort_order',
|
'environments.sort_order as environment_sort_order',
|
||||||
'environments.display_name as environment_display_name',
|
|
||||||
'feature_strategies.id as strategy_id',
|
'feature_strategies.id as strategy_id',
|
||||||
'feature_strategies.strategy_name as strategy_name',
|
'feature_strategies.strategy_name as strategy_name',
|
||||||
'feature_strategies.parameters as parameters',
|
'feature_strategies.parameters as parameters',
|
||||||
@ -266,7 +265,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
env.enabled = r.enabled;
|
env.enabled = r.enabled;
|
||||||
env.type = r.environment_type;
|
env.type = r.environment_type;
|
||||||
env.sortOrder = r.environment_sort_order;
|
env.sortOrder = r.environment_sort_order;
|
||||||
env.displayName = r.environment_display_name;
|
|
||||||
if (!env.strategies) {
|
if (!env.strategies) {
|
||||||
env.strategies = [];
|
env.strategies = [];
|
||||||
}
|
}
|
||||||
@ -300,7 +298,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
private getEnvironment(r: any): IEnvironmentOverview {
|
private getEnvironment(r: any): IEnvironmentOverview {
|
||||||
return {
|
return {
|
||||||
name: r.environment,
|
name: r.environment,
|
||||||
displayName: r.display_name,
|
|
||||||
enabled: r.enabled,
|
enabled: r.enabled,
|
||||||
type: r.environment_type,
|
type: r.environment_type,
|
||||||
sortOrder: r.environment_sort_order,
|
sortOrder: r.environment_sort_order,
|
||||||
@ -321,7 +318,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
'features.stale as stale',
|
'features.stale as stale',
|
||||||
'feature_environments.enabled as enabled',
|
'feature_environments.enabled as enabled',
|
||||||
'feature_environments.environment as environment',
|
'feature_environments.environment as environment',
|
||||||
'environments.display_name as display_name',
|
|
||||||
'environments.type as environment_type',
|
'environments.type as environment_type',
|
||||||
'environments.sort_order as environment_sort_order',
|
'environments.sort_order as environment_sort_order',
|
||||||
)
|
)
|
||||||
|
@ -45,16 +45,20 @@ export default class FeatureToggleClientStore
|
|||||||
r: any,
|
r: any,
|
||||||
includeId: boolean = true,
|
includeId: boolean = true,
|
||||||
): IStrategyConfig {
|
): IStrategyConfig {
|
||||||
const strategy = {
|
if (includeId) {
|
||||||
name: r.strategy_name,
|
return {
|
||||||
constraints: r.constraints || [],
|
name: r.strategy_name,
|
||||||
parameters: r.parameters,
|
constraints: r.constraints || [],
|
||||||
id: r.strategy_id,
|
parameters: r.parameters,
|
||||||
};
|
id: r.strategy_id,
|
||||||
if (!includeId) {
|
};
|
||||||
delete strategy.id;
|
} else {
|
||||||
|
return {
|
||||||
|
name: r.strategy_name,
|
||||||
|
constraints: r.constraints || [],
|
||||||
|
parameters: r.parameters,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return strategy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAll(
|
private async getAll(
|
||||||
@ -74,28 +78,29 @@ export default class FeatureToggleClientStore
|
|||||||
'features.variants as variants',
|
'features.variants as variants',
|
||||||
'features.created_at as created_at',
|
'features.created_at as created_at',
|
||||||
'features.last_seen_at as last_seen_at',
|
'features.last_seen_at as last_seen_at',
|
||||||
'feature_environments.enabled as enabled',
|
'fe.enabled as enabled',
|
||||||
'feature_environments.environment as environment',
|
'fe.environment as environment',
|
||||||
'feature_strategies.id as strategy_id',
|
'fs.id as strategy_id',
|
||||||
'feature_strategies.strategy_name as strategy_name',
|
'fs.strategy_name as strategy_name',
|
||||||
'feature_strategies.parameters as parameters',
|
'fs.parameters as parameters',
|
||||||
'feature_strategies.constraints as constraints',
|
'fs.constraints as constraints',
|
||||||
)
|
)
|
||||||
.fullOuterJoin(
|
.fullOuterJoin(
|
||||||
'feature_environments',
|
this.db('feature_strategies')
|
||||||
'feature_environments.feature_name',
|
.select('*')
|
||||||
|
.where({ environment })
|
||||||
|
.as('fs'),
|
||||||
|
'fs.feature_name',
|
||||||
|
'features.name',
|
||||||
|
)
|
||||||
|
.fullOuterJoin(
|
||||||
|
this.db('feature_environments')
|
||||||
|
.select('feature_name', 'enabled', 'environment')
|
||||||
|
.where({ environment })
|
||||||
|
.as('fe'),
|
||||||
|
'fe.feature_name',
|
||||||
'features.name',
|
'features.name',
|
||||||
)
|
)
|
||||||
.fullOuterJoin('feature_strategies', function () {
|
|
||||||
this.on(
|
|
||||||
'feature_strategies.feature_name',
|
|
||||||
'features.name',
|
|
||||||
).andOn(
|
|
||||||
'feature_strategies.environment',
|
|
||||||
'feature_environments.environment',
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.where('feature_environments.environment', environment)
|
|
||||||
.where({ archived });
|
.where({ archived });
|
||||||
|
|
||||||
if (featureQuery) {
|
if (featureQuery) {
|
||||||
@ -117,6 +122,7 @@ export default class FeatureToggleClientStore
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = await query;
|
const rows = await query;
|
||||||
stopTimer();
|
stopTimer();
|
||||||
const featureToggles = rows.reduce((acc, r) => {
|
const featureToggles = rows.reduce((acc, r) => {
|
||||||
@ -132,7 +138,7 @@ export default class FeatureToggleClientStore
|
|||||||
if (r.strategy_name) {
|
if (r.strategy_name) {
|
||||||
feature.strategies.push(this.getAdminStrategy(r, isAdmin));
|
feature.strategies.push(this.getAdminStrategy(r, isAdmin));
|
||||||
}
|
}
|
||||||
feature.enabled = r.enabled;
|
feature.enabled = !!r.enabled;
|
||||||
feature.name = r.name;
|
feature.name = r.name;
|
||||||
feature.description = r.description;
|
feature.description = r.description;
|
||||||
feature.project = r.project;
|
feature.project = r.project;
|
||||||
|
@ -6,6 +6,7 @@ import { IProject } from '../types/model';
|
|||||||
import {
|
import {
|
||||||
IProjectHealthUpdate,
|
IProjectHealthUpdate,
|
||||||
IProjectInsert,
|
IProjectInsert,
|
||||||
|
IProjectQuery,
|
||||||
IProjectStore,
|
IProjectStore,
|
||||||
} from '../types/stores/project-store';
|
} from '../types/stores/project-store';
|
||||||
import { DEFAULT_ENV } from '../util/constants';
|
import { DEFAULT_ENV } from '../util/constants';
|
||||||
@ -43,10 +44,11 @@ class ProjectStore implements IProjectStore {
|
|||||||
return present;
|
return present;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<IProject[]> {
|
async getAll(query: IProjectQuery = {}): Promise<IProject[]> {
|
||||||
const rows = await this.db
|
const rows = await this.db
|
||||||
.select(COLUMNS)
|
.select(COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
|
.where(query)
|
||||||
.orderBy('name', 'asc');
|
.orderBy('name', 'asc');
|
||||||
|
|
||||||
return rows.map(this.mapRow);
|
return rows.map(this.mapRow);
|
||||||
|
@ -300,12 +300,13 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.info('Deleting strategy');
|
this.logger.info('Deleting strategy');
|
||||||
const { environment, projectId } = req.params;
|
const { environment, projectId, featureName } = req.params;
|
||||||
const userName = extractUsername(req);
|
const userName = extractUsername(req);
|
||||||
const { strategyId } = req.params;
|
const { strategyId } = req.params;
|
||||||
this.logger.info(strategyId);
|
this.logger.info(strategyId);
|
||||||
const strategy = await this.featureService.deleteStrategy(
|
const strategy = await this.featureService.deleteStrategy(
|
||||||
strategyId,
|
strategyId,
|
||||||
|
featureName,
|
||||||
userName,
|
userName,
|
||||||
projectId,
|
projectId,
|
||||||
environment,
|
environment,
|
||||||
|
@ -1,15 +1,28 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
import Controller from '../../controller';
|
import Controller from '../../controller';
|
||||||
import { IUnleashConfig } from '../../../types/option';
|
import { IUnleashConfig } from '../../../types/option';
|
||||||
import { IUnleashServices } from '../../../types/services';
|
import { IUnleashServices } from '../../../types/services';
|
||||||
import ProjectFeaturesController from './features';
|
import ProjectFeaturesController from './features';
|
||||||
import EnvironmentsController from './environments';
|
import EnvironmentsController from './environments';
|
||||||
import ProjectHealthReport from './health-report';
|
import ProjectHealthReport from './health-report';
|
||||||
|
import ProjectService from '../../../services/project-service';
|
||||||
|
|
||||||
export default class ProjectApi extends Controller {
|
export default class ProjectApi extends Controller {
|
||||||
|
private projectService: ProjectService;
|
||||||
|
|
||||||
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
||||||
super(config);
|
super(config);
|
||||||
|
this.projectService = services.projectService;
|
||||||
|
this.get('/', this.getProjects);
|
||||||
this.use('/', new ProjectFeaturesController(config, services).router);
|
this.use('/', new ProjectFeaturesController(config, services).router);
|
||||||
this.use('/', new EnvironmentsController(config, services).router);
|
this.use('/', new EnvironmentsController(config, services).router);
|
||||||
this.use('/', new ProjectHealthReport(config, services).router);
|
this.use('/', new ProjectHealthReport(config, services).router);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjects(req: Request, res: Response): Promise<void> {
|
||||||
|
const projects = await this.projectService.getProjects({
|
||||||
|
id: 'default',
|
||||||
|
});
|
||||||
|
res.json({ version: 1, projects }).end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class StateController extends Controller {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (mime.getType(req.file.originalname) === 'text/yaml') {
|
if (mime.getType(req.file.originalname) === 'text/yaml') {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data = YAML.safeLoad(req.file.buffer);
|
data = YAML.load(req.file.buffer);
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
data = JSON.parse(req.file.buffer);
|
data = JSON.parse(req.file.buffer);
|
||||||
@ -93,7 +93,7 @@ class StateController extends Controller {
|
|||||||
if (downloadFile) {
|
if (downloadFile) {
|
||||||
res.attachment(`export-${timestamp}.yml`);
|
res.attachment(`export-${timestamp}.yml`);
|
||||||
}
|
}
|
||||||
res.type('yaml').send(YAML.safeDump(data, { skipInvalid: true }));
|
res.type('yaml').send(YAML.dump(data, { skipInvalid: true }));
|
||||||
} else {
|
} else {
|
||||||
if (downloadFile) {
|
if (downloadFile) {
|
||||||
res.attachment(`export-${timestamp}.json`);
|
res.attachment(`export-${timestamp}.json`);
|
||||||
|
@ -140,8 +140,12 @@ export default class ClientMetricsService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const value = await clientMetricsSchema.validateAsync(data);
|
const value = await clientMetricsSchema.validateAsync(data);
|
||||||
const toggleNames = Object.keys(value.bucket.toggles);
|
const toggleNames = Object.keys(value.bucket.toggles);
|
||||||
await this.featureToggleStore.setLastSeen(toggleNames);
|
|
||||||
await this.clientMetricsStore.insert(value);
|
if (toggleNames.length > 0) {
|
||||||
|
await this.featureToggleStore.setLastSeen(toggleNames);
|
||||||
|
await this.clientMetricsStore.insert(value);
|
||||||
|
}
|
||||||
|
|
||||||
await this.clientInstanceStore.insert({
|
await this.clientInstanceStore.insert({
|
||||||
appName: value.appName,
|
appName: value.appName,
|
||||||
instanceId: value.instanceId,
|
instanceId: value.instanceId,
|
||||||
|
@ -3,7 +3,6 @@ import { IUnleashStores } from '../types/stores';
|
|||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { IEventStore } from '../types/stores/event-store';
|
import { IEventStore } from '../types/stores/event-store';
|
||||||
import { IEvent } from '../types/model';
|
import { IEvent } from '../types/model';
|
||||||
import { FEATURE_METADATA_UPDATED } from '../types/events';
|
|
||||||
|
|
||||||
export default class EventService {
|
export default class EventService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -23,10 +22,7 @@ export default class EventService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getEventsForToggle(name: string): Promise<IEvent[]> {
|
async getEventsForToggle(name: string): Promise<IEvent[]> {
|
||||||
const events = await this.eventStore.getEventsFilterByType(name);
|
return this.eventStore.getEventsFilterByType(name);
|
||||||
return events.filter(
|
|
||||||
(e: IEvent) => e.type !== FEATURE_METADATA_UPDATED,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventsForProject(project: string): Promise<IEvent[]> {
|
async getEventsForProject(project: string): Promise<IEvent[]> {
|
||||||
|
@ -226,6 +226,7 @@ class FeatureToggleServiceV2 {
|
|||||||
*/
|
*/
|
||||||
async deleteStrategy(
|
async deleteStrategy(
|
||||||
id: string,
|
id: string,
|
||||||
|
featureName: string,
|
||||||
userName: string,
|
userName: string,
|
||||||
project: string = 'default',
|
project: string = 'default',
|
||||||
environment: string = DEFAULT_ENV,
|
environment: string = DEFAULT_ENV,
|
||||||
@ -240,6 +241,11 @@ class FeatureToggleServiceV2 {
|
|||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// If there are no strategies left for environment disable it
|
||||||
|
await this.featureEnvironmentStore.disableEnvironmentIfNoStrategies(
|
||||||
|
featureName,
|
||||||
|
environment,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStrategiesForEnvironment(
|
async getStrategiesForEnvironment(
|
||||||
@ -576,7 +582,7 @@ class FeatureToggleServiceV2 {
|
|||||||
? FEATURE_ENVIRONMENT_ENABLED
|
? FEATURE_ENVIRONMENT_ENABLED
|
||||||
: FEATURE_ENVIRONMENT_DISABLED,
|
: FEATURE_ENVIRONMENT_DISABLED,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data,
|
data: { name: featureName },
|
||||||
tags,
|
tags,
|
||||||
project: projectId,
|
project: projectId,
|
||||||
environment,
|
environment,
|
||||||
|
@ -63,6 +63,9 @@ export default class ProjectHealthService {
|
|||||||
archived: boolean = false,
|
archived: boolean = false,
|
||||||
): Promise<IProjectOverview> {
|
): Promise<IProjectOverview> {
|
||||||
const project = await this.projectStore.get(projectId);
|
const project = await this.projectStore.get(projectId);
|
||||||
|
const environments = await this.projectStore.getEnvironmentsForProject(
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
const features = await this.featureToggleService.getFeatureOverview(
|
const features = await this.featureToggleService.getFeatureOverview(
|
||||||
projectId,
|
projectId,
|
||||||
archived,
|
archived,
|
||||||
@ -72,6 +75,7 @@ export default class ProjectHealthService {
|
|||||||
name: project.name,
|
name: project.name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
health: project.health,
|
health: project.health,
|
||||||
|
environments,
|
||||||
features,
|
features,
|
||||||
members,
|
members,
|
||||||
version: 1,
|
version: 1,
|
||||||
|
@ -24,13 +24,12 @@ import { IEnvironmentStore } from '../types/stores/environment-store';
|
|||||||
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
||||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||||
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
||||||
import { IProjectStore } from '../types/stores/project-store';
|
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
||||||
import { IRole } from '../types/stores/access-store';
|
import { IRole } from '../types/stores/access-store';
|
||||||
import { IEventStore } from '../types/stores/event-store';
|
import { IEventStore } from '../types/stores/event-store';
|
||||||
import FeatureToggleServiceV2 from './feature-toggle-service-v2';
|
import FeatureToggleServiceV2 from './feature-toggle-service-v2';
|
||||||
import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
|
import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
|
||||||
import NoAccessError from '../error/no-access-error';
|
import NoAccessError from '../error/no-access-error';
|
||||||
import { DEFAULT_ENV } from '../util/constants';
|
|
||||||
|
|
||||||
const getCreatedBy = (user: User) => user.email || user.username;
|
const getCreatedBy = (user: User) => user.email || user.username;
|
||||||
|
|
||||||
@ -92,8 +91,8 @@ export default class ProjectService {
|
|||||||
this.logger = config.getLogger('services/project-service.js');
|
this.logger = config.getLogger('services/project-service.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(): Promise<IProjectWithCount[]> {
|
async getProjects(query?: IProjectQuery): Promise<IProjectWithCount[]> {
|
||||||
const projects = await this.store.getAll();
|
const projects = await this.store.getAll(query);
|
||||||
const projectsWithCount = await Promise.all(
|
const projectsWithCount = await Promise.all(
|
||||||
projects.map(async (p) => {
|
projects.map(async (p) => {
|
||||||
let featureCount = 0;
|
let featureCount = 0;
|
||||||
@ -123,8 +122,17 @@ export default class ProjectService {
|
|||||||
|
|
||||||
await this.store.create(data);
|
await this.store.create(data);
|
||||||
|
|
||||||
// TODO: we should only connect to enabled environments
|
const enabledEnvironments = await this.environmentStore.getAll({
|
||||||
await this.featureEnvironmentStore.connectProject(DEFAULT_ENV, data.id);
|
enabled: true,
|
||||||
|
});
|
||||||
|
await Promise.all(
|
||||||
|
enabledEnvironments.map(async (e) => {
|
||||||
|
await this.featureEnvironmentStore.connectProject(
|
||||||
|
e.name,
|
||||||
|
data.id,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
await this.accessService.createDefaultProjectRoles(user, data.id);
|
await this.accessService.createDefaultProjectRoles(user, data.id);
|
||||||
|
|
||||||
@ -302,6 +310,9 @@ export default class ProjectService {
|
|||||||
archived: boolean = false,
|
archived: boolean = false,
|
||||||
): Promise<IProjectOverview> {
|
): Promise<IProjectOverview> {
|
||||||
const project = await this.store.get(projectId);
|
const project = await this.store.get(projectId);
|
||||||
|
const environments = await this.store.getEnvironmentsForProject(
|
||||||
|
projectId,
|
||||||
|
);
|
||||||
const features = await this.featureToggleService.getFeatureOverview(
|
const features = await this.featureToggleService.getFeatureOverview(
|
||||||
projectId,
|
projectId,
|
||||||
archived,
|
archived,
|
||||||
@ -309,6 +320,7 @@ export default class ProjectService {
|
|||||||
const members = await this.store.getMembers(projectId);
|
const members = await this.store.getMembers(projectId);
|
||||||
return {
|
return {
|
||||||
name: project.name,
|
name: project.name,
|
||||||
|
environments,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
health: project.health,
|
health: project.health,
|
||||||
features,
|
features,
|
||||||
|
@ -533,12 +533,10 @@ test('exporting to new format works', async () => {
|
|||||||
});
|
});
|
||||||
await stores.environmentStore.create({
|
await stores.environmentStore.create({
|
||||||
name: 'dev',
|
name: 'dev',
|
||||||
displayName: 'Development',
|
|
||||||
type: 'development',
|
type: 'development',
|
||||||
});
|
});
|
||||||
await stores.environmentStore.create({
|
await stores.environmentStore.create({
|
||||||
name: 'prod',
|
name: 'prod',
|
||||||
displayName: 'Production',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await stores.featureToggleStore.create('fancy', {
|
await stores.featureToggleStore.create('fancy', {
|
||||||
@ -575,12 +573,10 @@ test('featureStrategies can keep existing', async () => {
|
|||||||
});
|
});
|
||||||
await stores.environmentStore.create({
|
await stores.environmentStore.create({
|
||||||
name: 'dev',
|
name: 'dev',
|
||||||
displayName: 'Development',
|
|
||||||
type: 'development',
|
type: 'development',
|
||||||
});
|
});
|
||||||
await stores.environmentStore.create({
|
await stores.environmentStore.create({
|
||||||
name: 'prod',
|
name: 'prod',
|
||||||
displayName: 'Production',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await stores.featureToggleStore.create('fancy', {
|
await stores.featureToggleStore.create('fancy', {
|
||||||
@ -623,12 +619,10 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
|
|||||||
});
|
});
|
||||||
await stores.environmentStore.create({
|
await stores.environmentStore.create({
|
||||||
name: 'dev',
|
name: 'dev',
|
||||||
displayName: 'Development',
|
|
||||||
type: 'development',
|
type: 'development',
|
||||||
});
|
});
|
||||||
await stores.environmentStore.create({
|
await stores.environmentStore.create({
|
||||||
name: 'prod',
|
name: 'prod',
|
||||||
displayName: 'Production',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await stores.featureToggleStore.create('fancy', {
|
await stores.featureToggleStore.create('fancy', {
|
||||||
|
@ -12,8 +12,7 @@ export const readFile: (file: string) => Promise<string> = (file) =>
|
|||||||
export const parseFile: (file: string, data: string) => any = (
|
export const parseFile: (file: string, data: string) => any = (
|
||||||
file: string,
|
file: string,
|
||||||
data: string,
|
data: string,
|
||||||
) =>
|
) => (mime.lookup(file) === 'text/yaml' ? YAML.load(data) : JSON.parse(data));
|
||||||
mime.lookup(file) === 'text/yaml' ? YAML.safeLoad(data) : JSON.parse(data);
|
|
||||||
|
|
||||||
export const filterExisting: (
|
export const filterExisting: (
|
||||||
keepExisting: boolean,
|
keepExisting: boolean,
|
||||||
|
@ -103,7 +103,6 @@ export interface IVariant {
|
|||||||
|
|
||||||
export interface IEnvironment {
|
export interface IEnvironment {
|
||||||
name: string;
|
name: string;
|
||||||
displayName: string;
|
|
||||||
type: string;
|
type: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -112,14 +111,13 @@ export interface IEnvironment {
|
|||||||
|
|
||||||
export interface IEnvironmentCreate {
|
export interface IEnvironmentCreate {
|
||||||
name: string;
|
name: string;
|
||||||
displayName: string;
|
|
||||||
type: string;
|
type: string;
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEnvironmentOverview {
|
export interface IEnvironmentOverview {
|
||||||
name: string;
|
name: string;
|
||||||
displayName: string;
|
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
type: string;
|
type: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
@ -137,6 +135,7 @@ export interface IFeatureOverview {
|
|||||||
export interface IProjectOverview {
|
export interface IProjectOverview {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
environments: string[];
|
||||||
features: IFeatureOverview[];
|
features: IFeatureOverview[];
|
||||||
members: number;
|
members: number;
|
||||||
version: number;
|
version: number;
|
||||||
|
@ -5,7 +5,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, 'displayName' | 'type' | 'protected'>,
|
env: Pick<IEnvironment, 'type' | 'protected'>,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<IEnvironment>;
|
): Promise<IEnvironment>;
|
||||||
updateProperty(
|
updateProperty(
|
||||||
|
@ -34,7 +34,10 @@ export interface IFeatureEnvironmentStore
|
|||||||
environment: string,
|
environment: string,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
disableEnvironmentIfNoStrategies(
|
||||||
|
featureName: string,
|
||||||
|
environment: string,
|
||||||
|
): Promise<void>;
|
||||||
disconnectFeatures(environment: string, project: string): Promise<void>;
|
disconnectFeatures(environment: string, project: string): Promise<void>;
|
||||||
connectFeatures(environment: string, projectId: string): Promise<void>;
|
connectFeatures(environment: string, projectId: string): Promise<void>;
|
||||||
|
|
||||||
|
@ -17,6 +17,10 @@ export interface IProjectHealthUpdate {
|
|||||||
health: number;
|
health: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IProjectQuery {
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IProjectStore extends Store<IProject, string> {
|
export interface IProjectStore extends Store<IProject, string> {
|
||||||
hasProject(id: string): Promise<boolean>;
|
hasProject(id: string): Promise<boolean>;
|
||||||
updateHealth(healthUpdate: IProjectHealthUpdate): Promise<void>;
|
updateHealth(healthUpdate: IProjectHealthUpdate): Promise<void>;
|
||||||
@ -28,4 +32,5 @@ export interface IProjectStore extends Store<IProject, string> {
|
|||||||
getEnvironmentsForProject(id: string): Promise<string[]>;
|
getEnvironmentsForProject(id: string): Promise<string[]>;
|
||||||
getMembers(projectId: string): Promise<number>;
|
getMembers(projectId: string): Promise<number>;
|
||||||
count(): Promise<number>;
|
count(): Promise<number>;
|
||||||
|
getAll(query?: IProjectQuery): Promise<IProject[]>;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`ALTER TABLE environments
|
||||||
|
DROP COLUMN display_name`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`ALTER TABLE environments
|
||||||
|
ADD COLUMN display_name TEXT`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
INSERT INTO environments(name, type, enabled)
|
||||||
|
VALUES ('development', 'development', true),
|
||||||
|
('production', 'production', true);
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
DELETE
|
||||||
|
FROM environments
|
||||||
|
WHERE name IN ('development', 'production');
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
exports.up = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
INSERT INTO project_environments(project_id, environment_name)
|
||||||
|
SELECT id, 'default'
|
||||||
|
FROM projects
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
DELETE
|
||||||
|
FROM project_environments
|
||||||
|
WHERE environment_name = 'default';
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
@ -24,7 +24,6 @@ test('Can list all existing environments', async () => {
|
|||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
expect(res.body.version).toBe(1);
|
expect(res.body.version).toBe(1);
|
||||||
expect(res.body.environments[0]).toStrictEqual({
|
expect(res.body.environments[0]).toStrictEqual({
|
||||||
displayName: 'Default Environment',
|
|
||||||
name: DEFAULT_ENV,
|
name: DEFAULT_ENV,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sortOrder: 1,
|
sortOrder: 1,
|
||||||
@ -38,7 +37,6 @@ test('Can update sort order', async () => {
|
|||||||
const envName = 'update-sort-order';
|
const envName = 'update-sort-order';
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
@ -81,7 +79,6 @@ test('Can update environment enabled status', async () => {
|
|||||||
const envName = 'enable-environment';
|
const envName = 'enable-environment';
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
@ -95,7 +92,6 @@ test('Can update environment disabled status', async () => {
|
|||||||
|
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -128,7 +124,6 @@ test('Can get specific environment', async () => {
|
|||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
type: 'production',
|
type: 'production',
|
||||||
displayName: 'Fun!',
|
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
.get(`/api/admin/environments/${envName}`)
|
.get(`/api/admin/environments/${envName}`)
|
||||||
|
@ -36,7 +36,6 @@ test('Should add environment to project', async () => {
|
|||||||
// Endpoint to create env does not exists anymore
|
// Endpoint to create env does not exists anymore
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: 'test',
|
name: 'test',
|
||||||
displayName: 'Test Env',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
@ -67,7 +66,6 @@ test('Should remove environment from project', async () => {
|
|||||||
|
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name,
|
name,
|
||||||
displayName: 'Test Env',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -174,7 +174,6 @@ test('Project overview includes environment connected to feature', async () => {
|
|||||||
});
|
});
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: 'project-overview',
|
name: 'project-overview',
|
||||||
displayName: 'Project Overview',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
@ -208,7 +207,6 @@ test('Disconnecting environment from project, removes environment from features
|
|||||||
});
|
});
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: 'dis-project-overview',
|
name: 'dis-project-overview',
|
||||||
displayName: 'Project Overview',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await app.request
|
await app.request
|
||||||
@ -236,7 +234,6 @@ test('Can enable/disable environment for feature with strategies', async () => {
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -386,7 +383,6 @@ test('Can get environment info for feature toggle', async () => {
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -550,7 +546,6 @@ test('Can add strategy to feature toggle to a "some-env-2"', async () => {
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -591,13 +586,11 @@ test('Environments are returned in sortOrder', async () => {
|
|||||||
// Create environments
|
// Create environments
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: sortedLast,
|
name: sortedLast,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
sortOrder: 8000,
|
sortOrder: 8000,
|
||||||
});
|
});
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: sortedSecond,
|
name: sortedSecond,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
sortOrder: 8,
|
sortOrder: 8,
|
||||||
});
|
});
|
||||||
@ -659,7 +652,6 @@ test('Can get strategies for feature and environment', async () => {
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -714,7 +706,6 @@ test('Can update a strategy based on id', async () => {
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -767,7 +758,6 @@ test('Trying to update a non existing feature strategy should yield 404', async
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -798,7 +788,6 @@ test('Can patch a strategy based on id', async () => {
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -851,7 +840,6 @@ test('Trying to get a non existing feature strategy should yield 404', async ()
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -880,7 +868,6 @@ test('Can not enable environment for feature without strategies', async () => {
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: environment,
|
name: environment,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -922,7 +909,6 @@ test('Enabling environment creates a FEATURE_ENVIRONMENT_ENABLED event', async (
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: environment,
|
name: environment,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -965,7 +951,6 @@ test('Disabling environment creates a FEATURE_ENVIRONMENT_DISABLED event', async
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: environment,
|
name: environment,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -1009,7 +994,6 @@ test('Can delete strategy from feature toggle', async () => {
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -1053,7 +1037,6 @@ test('List of strategies should respect sortOrder', async () => {
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Enable feature for environment',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -1084,12 +1067,10 @@ test('Feature strategies list should respect strategy sortorders for each enviro
|
|||||||
// Create environment
|
// Create environment
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: envName,
|
name: envName,
|
||||||
displayName: 'Sort orders within environment',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: secondEnv,
|
name: secondEnv,
|
||||||
displayName: 'Sort orders within environment',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
// Connect environment to project
|
// Connect environment to project
|
||||||
@ -1127,3 +1108,140 @@ test('Feature strategies list should respect strategy sortorders for each enviro
|
|||||||
expect(strategies[1].sortOrder).toBe(sortOrderSecond);
|
expect(strategies[1].sortOrder).toBe(sortOrderSecond);
|
||||||
expect(strategies[2].sortOrder).toBe(sortOrderDefault);
|
expect(strategies[2].sortOrder).toBe(sortOrderDefault);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Deleting last strategy for feature environment should disable that environment', async () => {
|
||||||
|
const envName = 'last_strategy_delete_env';
|
||||||
|
const featureName = 'last_strategy_delete_feature';
|
||||||
|
// Create environment
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: envName,
|
||||||
|
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);
|
||||||
|
let strategyId;
|
||||||
|
await app.request
|
||||||
|
.post(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
||||||
|
)
|
||||||
|
.send({
|
||||||
|
name: 'default',
|
||||||
|
parameters: {
|
||||||
|
userId: 'string',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
strategyId = res.body.id;
|
||||||
|
});
|
||||||
|
// Enable feature_environment
|
||||||
|
await app.request
|
||||||
|
.post(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}/on`,
|
||||||
|
)
|
||||||
|
.send({})
|
||||||
|
.expect(200);
|
||||||
|
await app.request
|
||||||
|
.get(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}`,
|
||||||
|
)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.enabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
// Delete last strategy, this should also disable the environment
|
||||||
|
await app.request.delete(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies/${strategyId}`,
|
||||||
|
);
|
||||||
|
await app.request
|
||||||
|
.get(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}`,
|
||||||
|
)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.enabled).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Deleting strategy for feature environment should not disable that environment as long as there are other strategies', async () => {
|
||||||
|
const envName = 'any_strategy_delete_env';
|
||||||
|
const featureName = 'any_strategy_delete_feature';
|
||||||
|
// Create environment
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: envName,
|
||||||
|
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);
|
||||||
|
let strategyId;
|
||||||
|
await app.request
|
||||||
|
.post(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
||||||
|
)
|
||||||
|
.send({
|
||||||
|
name: 'default',
|
||||||
|
parameters: {
|
||||||
|
userId: 'string',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
strategyId = res.body.id;
|
||||||
|
});
|
||||||
|
await app.request
|
||||||
|
.post(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
||||||
|
)
|
||||||
|
.send({
|
||||||
|
name: 'default',
|
||||||
|
parameters: {
|
||||||
|
customerId: 'string',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
// Enable feature_environment
|
||||||
|
await app.request
|
||||||
|
.post(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}/on`,
|
||||||
|
)
|
||||||
|
.send({})
|
||||||
|
.expect(200);
|
||||||
|
await app.request
|
||||||
|
.get(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}`,
|
||||||
|
)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.enabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
// Delete a strategy, this should also disable the environment
|
||||||
|
await app.request.delete(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies/${strategyId}`,
|
||||||
|
);
|
||||||
|
await app.request
|
||||||
|
.get(
|
||||||
|
`/api/admin/projects/default/features/${featureName}/environments/${envName}`,
|
||||||
|
)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.enabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -49,6 +49,8 @@ test('Project with no stale toggles should have 100% health rating', async () =>
|
|||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
expect(res.body.health).toBe(100);
|
expect(res.body.health).toBe(100);
|
||||||
|
expect(res.body.environments).toHaveLength(1);
|
||||||
|
expect(res.body.environments).toStrictEqual(['default']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
32
src/test/e2e/api/admin/project/projects.e2e.test.ts
Normal file
32
src/test/e2e/api/admin/project/projects.e2e.test.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import dbInit from '../../../helpers/database-init';
|
||||||
|
import { setupApp } from '../../../helpers/test-helper';
|
||||||
|
import getLogger from '../../../../fixtures/no-logger';
|
||||||
|
import ProjectStore from '../../../../../lib/db/project-store';
|
||||||
|
|
||||||
|
let app;
|
||||||
|
let db;
|
||||||
|
|
||||||
|
let projectStore: ProjectStore;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('projects_api_serial', getLogger);
|
||||||
|
app = await setupApp(db.stores);
|
||||||
|
projectStore = db.stores.projectStore;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should ONLY return default project', async () => {
|
||||||
|
projectStore.create({ id: 'test2', name: 'test', description: '' });
|
||||||
|
|
||||||
|
const { body } = await app.request
|
||||||
|
.get('/api/admin/projects')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Content-Type', /json/);
|
||||||
|
|
||||||
|
expect(body.projects).toHaveLength(1);
|
||||||
|
expect(body.projects[0].id).toBe('default');
|
||||||
|
});
|
@ -142,7 +142,6 @@ test('Can roundtrip. I.e. export and then import', async () => {
|
|||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: environmentId,
|
name: environmentId,
|
||||||
type: 'test',
|
type: 'test',
|
||||||
displayName: 'Environment for export',
|
|
||||||
});
|
});
|
||||||
await db.stores.projectStore.create({
|
await db.stores.projectStore.create({
|
||||||
name: projectId,
|
name: projectId,
|
||||||
@ -191,7 +190,6 @@ test('Roundtrip with tags works', async () => {
|
|||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: environmentId,
|
name: environmentId,
|
||||||
type: 'test',
|
type: 'test',
|
||||||
displayName: 'Environment for export',
|
|
||||||
});
|
});
|
||||||
await db.stores.projectStore.create({
|
await db.stores.projectStore.create({
|
||||||
name: projectId,
|
name: projectId,
|
||||||
@ -253,7 +251,6 @@ test('Roundtrip with strategies in multiple environments works', async () => {
|
|||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: environmentId,
|
name: environmentId,
|
||||||
type: 'test',
|
type: 'test',
|
||||||
displayName: 'Environment for export',
|
|
||||||
});
|
});
|
||||||
await db.stores.projectStore.create({
|
await db.stores.projectStore.create({
|
||||||
name: projectId,
|
name: projectId,
|
||||||
|
@ -176,7 +176,6 @@ test('Can get strategies for specific environment', async () => {
|
|||||||
|
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: 'testing',
|
name: 'testing',
|
||||||
displayName: 'simple test',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
70
src/test/e2e/api/client/feature.env.disabled.e2e.test.ts
Normal file
70
src/test/e2e/api/client/feature.env.disabled.e2e.test.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
|
||||||
|
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||||
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
|
|
||||||
|
let app: IUnleashTest;
|
||||||
|
let db: ITestDb;
|
||||||
|
|
||||||
|
const featureName = 'feature.default.1';
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('feature_api_client', getLogger);
|
||||||
|
app = await setupApp(db.stores);
|
||||||
|
|
||||||
|
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||||
|
'default',
|
||||||
|
{
|
||||||
|
name: featureName,
|
||||||
|
description: 'the #1 feature',
|
||||||
|
},
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.services.featureToggleServiceV2.createStrategy(
|
||||||
|
{ name: 'default', constraints: [], parameters: {} },
|
||||||
|
'default',
|
||||||
|
featureName,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns feature toggle for default env', async () => {
|
||||||
|
await app.services.featureToggleServiceV2.updateEnabled(
|
||||||
|
'default',
|
||||||
|
'feature.default.1',
|
||||||
|
'default',
|
||||||
|
true,
|
||||||
|
'test',
|
||||||
|
);
|
||||||
|
await app.request
|
||||||
|
.get('/api/client/features')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.features).toHaveLength(1);
|
||||||
|
expect(res.body.features[0].enabled).toBe(true);
|
||||||
|
expect(res.body.features[0].strategies).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns feature toggle for default env even if it is removed from project', async () => {
|
||||||
|
await app.services.environmentService.removeEnvironmentFromProject(
|
||||||
|
'default',
|
||||||
|
'default',
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.get('/api/client/features')
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.features).toHaveLength(1);
|
||||||
|
expect(res.body.features[0].enabled).toBe(false);
|
||||||
|
expect(res.body.features[0].strategies).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
@ -28,7 +28,6 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
await environmentStore.create({
|
await environmentStore.create({
|
||||||
name: environment,
|
name: environment,
|
||||||
displayName: '',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ test('should enrich metrics with environment from api-token', async () => {
|
|||||||
|
|
||||||
await environmentStore.create({
|
await environmentStore.create({
|
||||||
name: 'some',
|
name: 'some',
|
||||||
displayName: '',
|
|
||||||
type: 'test',
|
type: 'test',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,8 +30,8 @@
|
|||||||
"environments": [
|
"environments": [
|
||||||
{
|
{
|
||||||
"name": "default",
|
"name": "default",
|
||||||
"displayName": "Default Environment",
|
|
||||||
"type": "production",
|
"type": "production",
|
||||||
|
|
||||||
"sortOrder": 1,
|
"sortOrder": 1,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"protected": true
|
"protected": true
|
||||||
|
@ -22,7 +22,6 @@ afterAll(async () => {
|
|||||||
test('Can get environment', async () => {
|
test('Can get environment', async () => {
|
||||||
const created = await db.stores.environmentStore.create({
|
const created = await db.stores.environmentStore.create({
|
||||||
name: 'testenv',
|
name: 'testenv',
|
||||||
displayName: 'Environment for testing',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -33,7 +32,6 @@ test('Can get environment', async () => {
|
|||||||
test('Can get all', async () => {
|
test('Can get all', async () => {
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: 'testenv2',
|
name: 'testenv2',
|
||||||
displayName: 'Environment for testing',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -44,7 +42,6 @@ test('Can get all', async () => {
|
|||||||
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',
|
||||||
displayName: '',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await stores.featureToggleStore.create('default', {
|
await stores.featureToggleStore.create('default', {
|
||||||
@ -63,7 +60,6 @@ test('Can connect environment to project', async () => {
|
|||||||
expect(f.environments).toEqual([
|
expect(f.environments).toEqual([
|
||||||
{
|
{
|
||||||
name: 'test-connection',
|
name: 'test-connection',
|
||||||
displayName: '',
|
|
||||||
enabled: false,
|
enabled: false,
|
||||||
sortOrder: 9999,
|
sortOrder: 9999,
|
||||||
type: 'production',
|
type: 'production',
|
||||||
@ -75,7 +71,6 @@ test('Can connect environment to project', async () => {
|
|||||||
test('Can remove environment from project', async () => {
|
test('Can remove environment from project', async () => {
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: 'removal-test',
|
name: 'removal-test',
|
||||||
displayName: '',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await stores.featureToggleStore.create('default', {
|
await stores.featureToggleStore.create('default', {
|
||||||
@ -92,7 +87,6 @@ test('Can remove environment from project', async () => {
|
|||||||
expect(f.environments).toEqual([
|
expect(f.environments).toEqual([
|
||||||
{
|
{
|
||||||
name: 'removal-test',
|
name: 'removal-test',
|
||||||
displayName: '',
|
|
||||||
enabled: false,
|
enabled: false,
|
||||||
sortOrder: 9999,
|
sortOrder: 9999,
|
||||||
type: 'production',
|
type: 'production',
|
||||||
@ -113,7 +107,6 @@ test('Can remove environment from project', async () => {
|
|||||||
test('Adding same environment twice should throw a NameExistsError', async () => {
|
test('Adding same environment twice should throw a NameExistsError', async () => {
|
||||||
await db.stores.environmentStore.create({
|
await db.stores.environmentStore.create({
|
||||||
name: 'uniqueness-test',
|
name: 'uniqueness-test',
|
||||||
displayName: '',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
await service.removeEnvironmentFromProject('test-connection', 'default');
|
await service.removeEnvironmentFromProject('test-connection', 'default');
|
||||||
|
@ -132,7 +132,7 @@ test('should validate name, legal', async () => {
|
|||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not be able to create exiting project', async () => {
|
test('should not be able to create existing project', async () => {
|
||||||
const project = {
|
const project = {
|
||||||
id: 'test-delete',
|
id: 'test-delete',
|
||||||
name: 'New project',
|
name: 'New project',
|
||||||
@ -510,3 +510,29 @@ test('should change project when checks pass', async () => {
|
|||||||
|
|
||||||
expect(updatedFeature.project).toBe(projectDestination.id);
|
expect(updatedFeature.project).toBe(projectDestination.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('A newly created project only gets connected to enabled environments', async () => {
|
||||||
|
const project = {
|
||||||
|
id: 'environment-test',
|
||||||
|
name: 'New environment project',
|
||||||
|
description: 'Blah',
|
||||||
|
};
|
||||||
|
const enabledEnv = 'connection_test';
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: enabledEnv,
|
||||||
|
type: 'test',
|
||||||
|
});
|
||||||
|
const disabledEnv = 'do_not_connect';
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: disabledEnv,
|
||||||
|
type: 'test',
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectService.createProject(project, user);
|
||||||
|
const connectedEnvs =
|
||||||
|
await db.stores.projectStore.getEnvironmentsForProject(project.id);
|
||||||
|
expect(connectedEnvs).toHaveLength(2); // default, connection_test
|
||||||
|
expect(connectedEnvs.some((e) => e === enabledEnv)).toBeTruthy();
|
||||||
|
expect(connectedEnvs.some((e) => e === disabledEnv)).toBeFalsy();
|
||||||
|
});
|
||||||
|
@ -123,7 +123,6 @@ test('should add environment to project', async () => {
|
|||||||
|
|
||||||
await environmentStore.create({
|
await environmentStore.create({
|
||||||
name: 'test',
|
name: 'test',
|
||||||
displayName: 'Test Env',
|
|
||||||
type: 'production',
|
type: 'production',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
2
src/test/fixtures/fake-environment-store.ts
vendored
2
src/test/fixtures/fake-environment-store.ts
vendored
@ -37,7 +37,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
env: Pick<IEnvironment, 'displayName' | 'type' | 'protected'>,
|
env: Pick<IEnvironment, 'type' | 'protected'>,
|
||||||
name: string,
|
name: string,
|
||||||
): Promise<IEnvironment> {
|
): Promise<IEnvironment> {
|
||||||
const found = this.environments.find(
|
const found = this.environments.find(
|
||||||
|
@ -148,4 +148,13 @@ export default class FakeFeatureEnvironmentStore
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return Promise.reject(new Error('Not implemented'));
|
return Promise.reject(new Error('Not implemented'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disableEnvironmentIfNoStrategies(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
featureName: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
environment: string,
|
||||||
|
): Promise<void> {
|
||||||
|
return Promise.reject(new Error('Not implemented'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,5 +49,6 @@ To make use of toggle variants, you need to use a compatible client. Client SDK
|
|||||||
- [unleash-client-python](https://github.com/Unleash/unleash-client-python) (from v3.3.0)
|
- [unleash-client-python](https://github.com/Unleash/unleash-client-python) (from v3.3.0)
|
||||||
- [unleash-client-dotnet](https://github.com/Unleash/unleash-client-dotnet) (from v1.3.6)
|
- [unleash-client-dotnet](https://github.com/Unleash/unleash-client-dotnet) (from v1.3.6)
|
||||||
- [unleash-client-go](https://github.com/Unleash/unleash-client-go) (from v3 branch)
|
- [unleash-client-go](https://github.com/Unleash/unleash-client-go) (from v3 branch)
|
||||||
|
- [unleash-client-php](https://github.com/Unleash/unleash-client-php) (from v1.0.0)
|
||||||
|
|
||||||
If you would like to give feedback on this feature, experience issues or have questions, please feel free to open an issue on [GitHub](https://github.com/Unleash/unleash/).
|
If you would like to give feedback on this feature, experience issues or have questions, please feel free to open an issue on [GitHub](https://github.com/Unleash/unleash/).
|
||||||
|
@ -76,4 +76,26 @@ $context = new UnleashContext(
|
|||||||
$unleash->isEnabled("someToggle", $context);
|
$unleash->isEnabled("someToggle", $context);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
**b) Via a UnleashContextProvider**
|
||||||
|
|
||||||
|
This is a bit more advanced approach, where you configure a unleash-context provider. By doing this you do not have to rebuild or to pass the unleash-context object to every place you are calling `$unleash->isEnabled()`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Unleash\Client\UnleashBuilder;
|
||||||
|
|
||||||
|
$contextProvider = new MyAwesomeContextProvider();
|
||||||
|
$unleash = UnleashBuilder::create()
|
||||||
|
->withAppName('my.php-app')
|
||||||
|
->withInstanceId('your-instance-1')
|
||||||
|
->withAppUrl('http://unleash.herokuapp.com/api/')
|
||||||
|
->withContextProvider($contextProvider)
|
||||||
|
->build();
|
||||||
|
|
||||||
|
// Anywhere in the code unleash will get the unleash context from your registered provider.
|
||||||
|
$unleash->isEnabled("someToggle");
|
||||||
|
```
|
||||||
|
|
||||||
> You can read more complete documentation in the [Client SDK repository](https://github.com/Unleash/unleash-client-php).
|
> You can read more complete documentation in the [Client SDK repository](https://github.com/Unleash/unleash-client-php).
|
||||||
|
Loading…
Reference in New Issue
Block a user