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,
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint","prettier"],
|
||||
"plugins": ["@typescript-eslint", "prettier", "import"],
|
||||
"root": true,
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<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)
|
||||
|
||||
|
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",
|
||||
"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": [
|
||||
"unleash",
|
||||
"feature toggle",
|
||||
@ -90,7 +90,7 @@
|
||||
"gravatar-url": "^3.1.0",
|
||||
"helmet": "^4.1.0",
|
||||
"joi": "^17.3.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"knex": "0.95.11",
|
||||
"log4js": "^6.0.0",
|
||||
"memoizee": "^0.4.15",
|
||||
@ -109,7 +109,7 @@
|
||||
"response-time": "^2.3.2",
|
||||
"serve-favicon": "^2.5.0",
|
||||
"stoppable": "^1.1.0",
|
||||
"unleash-frontend": "4.2.0",
|
||||
"unleash-frontend": "4.2.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -118,7 +118,7 @@
|
||||
"@types/express-session": "1.17.4",
|
||||
"@types/faker": "5.5.8",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/js-yaml": "3.12.7",
|
||||
"@types/js-yaml": "4.0.3",
|
||||
"@types/memoizee": "0.4.6",
|
||||
"@types/node": "16.6.1",
|
||||
"@types/node-fetch": "2.5.12",
|
||||
@ -127,23 +127,23 @@
|
||||
"@types/stoppable": "1.1.1",
|
||||
"@types/supertest": "2.0.11",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.32.0",
|
||||
"@typescript-eslint/parser": "4.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.33.0",
|
||||
"@typescript-eslint/parser": "4.33.0",
|
||||
"copyfiles": "2.4.1",
|
||||
"coveralls": "3.1.1",
|
||||
"del-cli": "4.0.1",
|
||||
"eslint": "7.32.0",
|
||||
"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-plugin-import": "2.24.2",
|
||||
"eslint-plugin-prettier": "4.0.0",
|
||||
"faker": "5.5.3",
|
||||
"fetch-mock": "9.11.0",
|
||||
"husky": "7.0.2",
|
||||
"jest": "27.2.2",
|
||||
"jest": "27.2.4",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"lint-staged": "11.1.2",
|
||||
"lint-staged": "11.2.0",
|
||||
"prettier": "2.4.1",
|
||||
"proxyquire": "2.1.3",
|
||||
"source-map-support": "0.5.20",
|
||||
|
@ -1,10 +1,9 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"extends": ["config:base"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"matchPackagePatterns": ["*"],
|
||||
"automerge": true
|
||||
}
|
||||
]
|
||||
|
@ -4,14 +4,14 @@ import Addon from './addon';
|
||||
import slackDefinition from './slack-definition';
|
||||
import { IAddonConfig, IEvent } from '../types/model';
|
||||
|
||||
const {
|
||||
import {
|
||||
FEATURE_CREATED,
|
||||
FEATURE_UPDATED,
|
||||
FEATURE_ARCHIVED,
|
||||
FEATURE_REVIVED,
|
||||
FEATURE_STALE_ON,
|
||||
FEATURE_STALE_OFF,
|
||||
} = require('../types/events');
|
||||
} from '../types/events';
|
||||
|
||||
export default class SlackAddon extends Addon {
|
||||
unleashUrl: string;
|
||||
@ -123,7 +123,7 @@ This was changed by ${createdBy}.`;
|
||||
const stale = data.stale ? '("stale")' : '';
|
||||
const typeStr = `*Type*: ${data.type}`;
|
||||
const project = `*Project*: ${data.project}`;
|
||||
const strategies = `*Activation strategies*: \`\`\`${YAML.safeDump(
|
||||
const strategies = `*Activation strategies*: \`\`\`${YAML.dump(
|
||||
data.strategies,
|
||||
{ skipInvalid: true },
|
||||
)}\`\`\``;
|
||||
|
@ -109,7 +109,7 @@ export default class TeamsAddon extends Addon {
|
||||
const { data } = event;
|
||||
const typeStr = `*Type*: ${data.type}`;
|
||||
const project = `*Project*: ${data.project}`;
|
||||
const strategies = `*Activation strategies*: \n${YAML.safeDump(
|
||||
const strategies = `*Activation strategies*: \n${YAML.dump(
|
||||
data.strategies,
|
||||
{ skipInvalid: true },
|
||||
)}`;
|
||||
|
@ -10,7 +10,6 @@ import { snakeCaseKeys } from '../util/snakeCase';
|
||||
|
||||
interface IEnvironmentsTable {
|
||||
name: string;
|
||||
display_name: string;
|
||||
created_at?: Date;
|
||||
type: string;
|
||||
sort_order: number;
|
||||
@ -20,7 +19,6 @@ interface IEnvironmentsTable {
|
||||
|
||||
const COLUMNS = [
|
||||
'type',
|
||||
'display_name',
|
||||
'name',
|
||||
'created_at',
|
||||
'sort_order',
|
||||
@ -31,7 +29,6 @@ const COLUMNS = [
|
||||
function mapRow(row: IEnvironmentsTable): IEnvironment {
|
||||
return {
|
||||
name: row.name,
|
||||
displayName: row.display_name,
|
||||
type: row.type,
|
||||
sortOrder: row.sort_order,
|
||||
enabled: row.enabled,
|
||||
@ -42,7 +39,6 @@ function mapRow(row: IEnvironmentsTable): IEnvironment {
|
||||
function fieldToRow(env: IEnvironment): IEnvironmentsTable {
|
||||
return {
|
||||
name: env.name,
|
||||
display_name: env.displayName,
|
||||
type: env.type,
|
||||
sort_order: env.sortOrder,
|
||||
enabled: env.enabled,
|
||||
@ -95,10 +91,14 @@ export default class EnvironmentStore implements IEnvironmentStore {
|
||||
throw new NotFoundError(`Could not find environment with name: ${key}`);
|
||||
}
|
||||
|
||||
async getAll(): Promise<IEnvironment[]> {
|
||||
const rows = await this.db<IEnvironmentsTable>(TABLE)
|
||||
async getAll(query?: Object): Promise<IEnvironment[]> {
|
||||
let qB = this.db<IEnvironmentsTable>(TABLE)
|
||||
.select('*')
|
||||
.orderBy('sort_order', 'created_at');
|
||||
if (query) {
|
||||
qB = qB.where(query);
|
||||
}
|
||||
const rows = await qB;
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ export default class EnvironmentStore implements IEnvironmentStore {
|
||||
}
|
||||
|
||||
async update(
|
||||
env: Pick<IEnvironment, 'displayName' | 'type' | 'protected'>,
|
||||
env: Pick<IEnvironment, 'type' | 'protected'>,
|
||||
name: string,
|
||||
): Promise<IEnvironment> {
|
||||
const updatedEnv = await this.db<IEnvironmentsTable>(TABLE)
|
||||
|
@ -10,7 +10,10 @@ import { DB_TIME } from '../metric-events';
|
||||
import { IFeatureEnvironment } from '../types/model';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
|
||||
const T = { featureEnvs: 'feature_environments' };
|
||||
const T = {
|
||||
featureEnvs: 'feature_environments',
|
||||
featureStrategies: 'feature_strategies',
|
||||
};
|
||||
|
||||
interface IFeatureEnvironmentRow {
|
||||
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(
|
||||
featureName: string,
|
||||
environment: string,
|
||||
|
@ -213,7 +213,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
'environments.name as environment_name',
|
||||
'environments.type as environment_type',
|
||||
'environments.sort_order as environment_sort_order',
|
||||
'environments.display_name as environment_display_name',
|
||||
'feature_strategies.id as strategy_id',
|
||||
'feature_strategies.strategy_name as strategy_name',
|
||||
'feature_strategies.parameters as parameters',
|
||||
@ -266,7 +265,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
env.enabled = r.enabled;
|
||||
env.type = r.environment_type;
|
||||
env.sortOrder = r.environment_sort_order;
|
||||
env.displayName = r.environment_display_name;
|
||||
if (!env.strategies) {
|
||||
env.strategies = [];
|
||||
}
|
||||
@ -300,7 +298,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
private getEnvironment(r: any): IEnvironmentOverview {
|
||||
return {
|
||||
name: r.environment,
|
||||
displayName: r.display_name,
|
||||
enabled: r.enabled,
|
||||
type: r.environment_type,
|
||||
sortOrder: r.environment_sort_order,
|
||||
@ -321,7 +318,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
'features.stale as stale',
|
||||
'feature_environments.enabled as enabled',
|
||||
'feature_environments.environment as environment',
|
||||
'environments.display_name as display_name',
|
||||
'environments.type as environment_type',
|
||||
'environments.sort_order as environment_sort_order',
|
||||
)
|
||||
|
@ -45,16 +45,20 @@ export default class FeatureToggleClientStore
|
||||
r: any,
|
||||
includeId: boolean = true,
|
||||
): IStrategyConfig {
|
||||
const strategy = {
|
||||
name: r.strategy_name,
|
||||
constraints: r.constraints || [],
|
||||
parameters: r.parameters,
|
||||
id: r.strategy_id,
|
||||
};
|
||||
if (!includeId) {
|
||||
delete strategy.id;
|
||||
if (includeId) {
|
||||
return {
|
||||
name: r.strategy_name,
|
||||
constraints: r.constraints || [],
|
||||
parameters: r.parameters,
|
||||
id: r.strategy_id,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: r.strategy_name,
|
||||
constraints: r.constraints || [],
|
||||
parameters: r.parameters,
|
||||
};
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
|
||||
private async getAll(
|
||||
@ -74,28 +78,29 @@ export default class FeatureToggleClientStore
|
||||
'features.variants as variants',
|
||||
'features.created_at as created_at',
|
||||
'features.last_seen_at as last_seen_at',
|
||||
'feature_environments.enabled as enabled',
|
||||
'feature_environments.environment as environment',
|
||||
'feature_strategies.id as strategy_id',
|
||||
'feature_strategies.strategy_name as strategy_name',
|
||||
'feature_strategies.parameters as parameters',
|
||||
'feature_strategies.constraints as constraints',
|
||||
'fe.enabled as enabled',
|
||||
'fe.environment as environment',
|
||||
'fs.id as strategy_id',
|
||||
'fs.strategy_name as strategy_name',
|
||||
'fs.parameters as parameters',
|
||||
'fs.constraints as constraints',
|
||||
)
|
||||
.fullOuterJoin(
|
||||
'feature_environments',
|
||||
'feature_environments.feature_name',
|
||||
this.db('feature_strategies')
|
||||
.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',
|
||||
)
|
||||
.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 });
|
||||
|
||||
if (featureQuery) {
|
||||
@ -117,6 +122,7 @@ export default class FeatureToggleClientStore
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const rows = await query;
|
||||
stopTimer();
|
||||
const featureToggles = rows.reduce((acc, r) => {
|
||||
@ -132,7 +138,7 @@ export default class FeatureToggleClientStore
|
||||
if (r.strategy_name) {
|
||||
feature.strategies.push(this.getAdminStrategy(r, isAdmin));
|
||||
}
|
||||
feature.enabled = r.enabled;
|
||||
feature.enabled = !!r.enabled;
|
||||
feature.name = r.name;
|
||||
feature.description = r.description;
|
||||
feature.project = r.project;
|
||||
|
@ -6,6 +6,7 @@ import { IProject } from '../types/model';
|
||||
import {
|
||||
IProjectHealthUpdate,
|
||||
IProjectInsert,
|
||||
IProjectQuery,
|
||||
IProjectStore,
|
||||
} from '../types/stores/project-store';
|
||||
import { DEFAULT_ENV } from '../util/constants';
|
||||
@ -43,10 +44,11 @@ class ProjectStore implements IProjectStore {
|
||||
return present;
|
||||
}
|
||||
|
||||
async getAll(): Promise<IProject[]> {
|
||||
async getAll(query: IProjectQuery = {}): Promise<IProject[]> {
|
||||
const rows = await this.db
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
.where(query)
|
||||
.orderBy('name', 'asc');
|
||||
|
||||
return rows.map(this.mapRow);
|
||||
|
@ -300,12 +300,13 @@ export default class ProjectFeaturesController extends Controller {
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
this.logger.info('Deleting strategy');
|
||||
const { environment, projectId } = req.params;
|
||||
const { environment, projectId, featureName } = req.params;
|
||||
const userName = extractUsername(req);
|
||||
const { strategyId } = req.params;
|
||||
this.logger.info(strategyId);
|
||||
const strategy = await this.featureService.deleteStrategy(
|
||||
strategyId,
|
||||
featureName,
|
||||
userName,
|
||||
projectId,
|
||||
environment,
|
||||
|
@ -1,15 +1,28 @@
|
||||
import { Request, Response } from 'express';
|
||||
import Controller from '../../controller';
|
||||
import { IUnleashConfig } from '../../../types/option';
|
||||
import { IUnleashServices } from '../../../types/services';
|
||||
import ProjectFeaturesController from './features';
|
||||
import EnvironmentsController from './environments';
|
||||
import ProjectHealthReport from './health-report';
|
||||
import ProjectService from '../../../services/project-service';
|
||||
|
||||
export default class ProjectApi extends Controller {
|
||||
private projectService: ProjectService;
|
||||
|
||||
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
||||
super(config);
|
||||
this.projectService = services.projectService;
|
||||
this.get('/', this.getProjects);
|
||||
this.use('/', new ProjectFeaturesController(config, services).router);
|
||||
this.use('/', new EnvironmentsController(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
|
||||
if (mime.getType(req.file.originalname) === 'text/yaml') {
|
||||
// @ts-ignore
|
||||
data = YAML.safeLoad(req.file.buffer);
|
||||
data = YAML.load(req.file.buffer);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
data = JSON.parse(req.file.buffer);
|
||||
@ -93,7 +93,7 @@ class StateController extends Controller {
|
||||
if (downloadFile) {
|
||||
res.attachment(`export-${timestamp}.yml`);
|
||||
}
|
||||
res.type('yaml').send(YAML.safeDump(data, { skipInvalid: true }));
|
||||
res.type('yaml').send(YAML.dump(data, { skipInvalid: true }));
|
||||
} else {
|
||||
if (downloadFile) {
|
||||
res.attachment(`export-${timestamp}.json`);
|
||||
|
@ -140,8 +140,12 @@ export default class ClientMetricsService {
|
||||
): Promise<void> {
|
||||
const value = await clientMetricsSchema.validateAsync(data);
|
||||
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({
|
||||
appName: value.appName,
|
||||
instanceId: value.instanceId,
|
||||
|
@ -3,7 +3,6 @@ import { IUnleashStores } from '../types/stores';
|
||||
import { Logger } from '../logger';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import { IEvent } from '../types/model';
|
||||
import { FEATURE_METADATA_UPDATED } from '../types/events';
|
||||
|
||||
export default class EventService {
|
||||
private logger: Logger;
|
||||
@ -23,10 +22,7 @@ export default class EventService {
|
||||
}
|
||||
|
||||
async getEventsForToggle(name: string): Promise<IEvent[]> {
|
||||
const events = await this.eventStore.getEventsFilterByType(name);
|
||||
return events.filter(
|
||||
(e: IEvent) => e.type !== FEATURE_METADATA_UPDATED,
|
||||
);
|
||||
return this.eventStore.getEventsFilterByType(name);
|
||||
}
|
||||
|
||||
async getEventsForProject(project: string): Promise<IEvent[]> {
|
||||
|
@ -226,6 +226,7 @@ class FeatureToggleServiceV2 {
|
||||
*/
|
||||
async deleteStrategy(
|
||||
id: string,
|
||||
featureName: string,
|
||||
userName: string,
|
||||
project: string = 'default',
|
||||
environment: string = DEFAULT_ENV,
|
||||
@ -240,6 +241,11 @@ class FeatureToggleServiceV2 {
|
||||
id,
|
||||
},
|
||||
});
|
||||
// If there are no strategies left for environment disable it
|
||||
await this.featureEnvironmentStore.disableEnvironmentIfNoStrategies(
|
||||
featureName,
|
||||
environment,
|
||||
);
|
||||
}
|
||||
|
||||
async getStrategiesForEnvironment(
|
||||
@ -576,7 +582,7 @@ class FeatureToggleServiceV2 {
|
||||
? FEATURE_ENVIRONMENT_ENABLED
|
||||
: FEATURE_ENVIRONMENT_DISABLED,
|
||||
createdBy: userName,
|
||||
data,
|
||||
data: { name: featureName },
|
||||
tags,
|
||||
project: projectId,
|
||||
environment,
|
||||
|
@ -63,6 +63,9 @@ export default class ProjectHealthService {
|
||||
archived: boolean = false,
|
||||
): Promise<IProjectOverview> {
|
||||
const project = await this.projectStore.get(projectId);
|
||||
const environments = await this.projectStore.getEnvironmentsForProject(
|
||||
projectId,
|
||||
);
|
||||
const features = await this.featureToggleService.getFeatureOverview(
|
||||
projectId,
|
||||
archived,
|
||||
@ -72,6 +75,7 @@ export default class ProjectHealthService {
|
||||
name: project.name,
|
||||
description: project.description,
|
||||
health: project.health,
|
||||
environments,
|
||||
features,
|
||||
members,
|
||||
version: 1,
|
||||
|
@ -24,13 +24,12 @@ import { IEnvironmentStore } from '../types/stores/environment-store';
|
||||
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-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 { IEventStore } from '../types/stores/event-store';
|
||||
import FeatureToggleServiceV2 from './feature-toggle-service-v2';
|
||||
import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
|
||||
import NoAccessError from '../error/no-access-error';
|
||||
import { DEFAULT_ENV } from '../util/constants';
|
||||
|
||||
const getCreatedBy = (user: User) => user.email || user.username;
|
||||
|
||||
@ -92,8 +91,8 @@ export default class ProjectService {
|
||||
this.logger = config.getLogger('services/project-service.js');
|
||||
}
|
||||
|
||||
async getProjects(): Promise<IProjectWithCount[]> {
|
||||
const projects = await this.store.getAll();
|
||||
async getProjects(query?: IProjectQuery): Promise<IProjectWithCount[]> {
|
||||
const projects = await this.store.getAll(query);
|
||||
const projectsWithCount = await Promise.all(
|
||||
projects.map(async (p) => {
|
||||
let featureCount = 0;
|
||||
@ -123,8 +122,17 @@ export default class ProjectService {
|
||||
|
||||
await this.store.create(data);
|
||||
|
||||
// TODO: we should only connect to enabled environments
|
||||
await this.featureEnvironmentStore.connectProject(DEFAULT_ENV, data.id);
|
||||
const enabledEnvironments = await this.environmentStore.getAll({
|
||||
enabled: true,
|
||||
});
|
||||
await Promise.all(
|
||||
enabledEnvironments.map(async (e) => {
|
||||
await this.featureEnvironmentStore.connectProject(
|
||||
e.name,
|
||||
data.id,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await this.accessService.createDefaultProjectRoles(user, data.id);
|
||||
|
||||
@ -302,6 +310,9 @@ export default class ProjectService {
|
||||
archived: boolean = false,
|
||||
): Promise<IProjectOverview> {
|
||||
const project = await this.store.get(projectId);
|
||||
const environments = await this.store.getEnvironmentsForProject(
|
||||
projectId,
|
||||
);
|
||||
const features = await this.featureToggleService.getFeatureOverview(
|
||||
projectId,
|
||||
archived,
|
||||
@ -309,6 +320,7 @@ export default class ProjectService {
|
||||
const members = await this.store.getMembers(projectId);
|
||||
return {
|
||||
name: project.name,
|
||||
environments,
|
||||
description: project.description,
|
||||
health: project.health,
|
||||
features,
|
||||
|
@ -533,12 +533,10 @@ test('exporting to new format works', async () => {
|
||||
});
|
||||
await stores.environmentStore.create({
|
||||
name: 'dev',
|
||||
displayName: 'Development',
|
||||
type: 'development',
|
||||
});
|
||||
await stores.environmentStore.create({
|
||||
name: 'prod',
|
||||
displayName: 'Production',
|
||||
type: 'production',
|
||||
});
|
||||
await stores.featureToggleStore.create('fancy', {
|
||||
@ -575,12 +573,10 @@ test('featureStrategies can keep existing', async () => {
|
||||
});
|
||||
await stores.environmentStore.create({
|
||||
name: 'dev',
|
||||
displayName: 'Development',
|
||||
type: 'development',
|
||||
});
|
||||
await stores.environmentStore.create({
|
||||
name: 'prod',
|
||||
displayName: 'Production',
|
||||
type: 'production',
|
||||
});
|
||||
await stores.featureToggleStore.create('fancy', {
|
||||
@ -623,12 +619,10 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
|
||||
});
|
||||
await stores.environmentStore.create({
|
||||
name: 'dev',
|
||||
displayName: 'Development',
|
||||
type: 'development',
|
||||
});
|
||||
await stores.environmentStore.create({
|
||||
name: 'prod',
|
||||
displayName: 'Production',
|
||||
type: 'production',
|
||||
});
|
||||
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 = (
|
||||
file: string,
|
||||
data: string,
|
||||
) =>
|
||||
mime.lookup(file) === 'text/yaml' ? YAML.safeLoad(data) : JSON.parse(data);
|
||||
) => (mime.lookup(file) === 'text/yaml' ? YAML.load(data) : JSON.parse(data));
|
||||
|
||||
export const filterExisting: (
|
||||
keepExisting: boolean,
|
||||
|
@ -103,7 +103,6 @@ export interface IVariant {
|
||||
|
||||
export interface IEnvironment {
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: string;
|
||||
sortOrder: number;
|
||||
enabled: boolean;
|
||||
@ -112,14 +111,13 @@ export interface IEnvironment {
|
||||
|
||||
export interface IEnvironmentCreate {
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: string;
|
||||
sortOrder?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IEnvironmentOverview {
|
||||
name: string;
|
||||
displayName: string;
|
||||
enabled: boolean;
|
||||
type: string;
|
||||
sortOrder: number;
|
||||
@ -137,6 +135,7 @@ export interface IFeatureOverview {
|
||||
export interface IProjectOverview {
|
||||
name: string;
|
||||
description: string;
|
||||
environments: string[];
|
||||
features: IFeatureOverview[];
|
||||
members: number;
|
||||
version: number;
|
||||
|
@ -5,7 +5,7 @@ export interface IEnvironmentStore extends Store<IEnvironment, string> {
|
||||
exists(name: string): Promise<boolean>;
|
||||
create(env: IEnvironmentCreate): Promise<IEnvironment>;
|
||||
update(
|
||||
env: Pick<IEnvironment, 'displayName' | 'type' | 'protected'>,
|
||||
env: Pick<IEnvironment, 'type' | 'protected'>,
|
||||
name: string,
|
||||
): Promise<IEnvironment>;
|
||||
updateProperty(
|
||||
|
@ -34,7 +34,10 @@ export interface IFeatureEnvironmentStore
|
||||
environment: string,
|
||||
enabled: boolean,
|
||||
): Promise<void>;
|
||||
|
||||
disableEnvironmentIfNoStrategies(
|
||||
featureName: string,
|
||||
environment: string,
|
||||
): Promise<void>;
|
||||
disconnectFeatures(environment: string, project: string): Promise<void>;
|
||||
connectFeatures(environment: string, projectId: string): Promise<void>;
|
||||
|
||||
|
@ -17,6 +17,10 @@ export interface IProjectHealthUpdate {
|
||||
health: number;
|
||||
}
|
||||
|
||||
export interface IProjectQuery {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface IProjectStore extends Store<IProject, string> {
|
||||
hasProject(id: string): Promise<boolean>;
|
||||
updateHealth(healthUpdate: IProjectHealthUpdate): Promise<void>;
|
||||
@ -28,4 +32,5 @@ export interface IProjectStore extends Store<IProject, string> {
|
||||
getEnvironmentsForProject(id: string): Promise<string[]>;
|
||||
getMembers(projectId: string): 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.body.version).toBe(1);
|
||||
expect(res.body.environments[0]).toStrictEqual({
|
||||
displayName: 'Default Environment',
|
||||
name: DEFAULT_ENV,
|
||||
enabled: true,
|
||||
sortOrder: 1,
|
||||
@ -38,7 +37,6 @@ test('Can update sort order', async () => {
|
||||
const envName = 'update-sort-order';
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
await app.request
|
||||
@ -81,7 +79,6 @@ test('Can update environment enabled status', async () => {
|
||||
const envName = 'enable-environment';
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
await app.request
|
||||
@ -95,7 +92,6 @@ test('Can update environment disabled status', async () => {
|
||||
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
|
||||
@ -128,7 +124,6 @@ test('Can get specific environment', async () => {
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
type: 'production',
|
||||
displayName: 'Fun!',
|
||||
});
|
||||
await app.request
|
||||
.get(`/api/admin/environments/${envName}`)
|
||||
|
@ -36,7 +36,6 @@ test('Should add environment to project', async () => {
|
||||
// Endpoint to create env does not exists anymore
|
||||
await db.stores.environmentStore.create({
|
||||
name: 'test',
|
||||
displayName: 'Test Env',
|
||||
type: 'test',
|
||||
});
|
||||
await app.request
|
||||
@ -67,7 +66,6 @@ test('Should remove environment from project', async () => {
|
||||
|
||||
await db.stores.environmentStore.create({
|
||||
name,
|
||||
displayName: 'Test Env',
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
|
@ -174,7 +174,6 @@ test('Project overview includes environment connected to feature', async () => {
|
||||
});
|
||||
await db.stores.environmentStore.create({
|
||||
name: 'project-overview',
|
||||
displayName: 'Project Overview',
|
||||
type: 'production',
|
||||
});
|
||||
await app.request
|
||||
@ -208,7 +207,6 @@ test('Disconnecting environment from project, removes environment from features
|
||||
});
|
||||
await db.stores.environmentStore.create({
|
||||
name: 'dis-project-overview',
|
||||
displayName: 'Project Overview',
|
||||
type: 'production',
|
||||
});
|
||||
await app.request
|
||||
@ -236,7 +234,6 @@ test('Can enable/disable environment for feature with strategies', async () => {
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -386,7 +383,6 @@ test('Can get environment info for feature toggle', async () => {
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -550,7 +546,6 @@ test('Can add strategy to feature toggle to a "some-env-2"', async () => {
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -591,13 +586,11 @@ test('Environments are returned in sortOrder', async () => {
|
||||
// Create environments
|
||||
await db.stores.environmentStore.create({
|
||||
name: sortedLast,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
sortOrder: 8000,
|
||||
});
|
||||
await db.stores.environmentStore.create({
|
||||
name: sortedSecond,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
sortOrder: 8,
|
||||
});
|
||||
@ -659,7 +652,6 @@ test('Can get strategies for feature and environment', async () => {
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -714,7 +706,6 @@ test('Can update a strategy based on id', async () => {
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -767,7 +758,6 @@ test('Trying to update a non existing feature strategy should yield 404', async
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -798,7 +788,6 @@ test('Can patch a strategy based on id', async () => {
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'test',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -851,7 +840,6 @@ test('Trying to get a non existing feature strategy should yield 404', async ()
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'production',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -880,7 +868,6 @@ test('Can not enable environment for feature without strategies', async () => {
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: environment,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'test',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -922,7 +909,6 @@ test('Enabling environment creates a FEATURE_ENVIRONMENT_ENABLED event', async (
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: environment,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'test',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -965,7 +951,6 @@ test('Disabling environment creates a FEATURE_ENVIRONMENT_DISABLED event', async
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: environment,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'test',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -1009,7 +994,6 @@ test('Can delete strategy from feature toggle', async () => {
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'test',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -1053,7 +1037,6 @@ test('List of strategies should respect sortOrder', async () => {
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
type: 'test',
|
||||
});
|
||||
// Connect environment to project
|
||||
@ -1084,12 +1067,10 @@ test('Feature strategies list should respect strategy sortorders for each enviro
|
||||
// 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
|
||||
@ -1127,3 +1108,140 @@ test('Feature strategies list should respect strategy sortorders for each enviro
|
||||
expect(strategies[1].sortOrder).toBe(sortOrderSecond);
|
||||
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((res) => {
|
||||
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({
|
||||
name: environmentId,
|
||||
type: 'test',
|
||||
displayName: 'Environment for export',
|
||||
});
|
||||
await db.stores.projectStore.create({
|
||||
name: projectId,
|
||||
@ -191,7 +190,6 @@ test('Roundtrip with tags works', async () => {
|
||||
await db.stores.environmentStore.create({
|
||||
name: environmentId,
|
||||
type: 'test',
|
||||
displayName: 'Environment for export',
|
||||
});
|
||||
await db.stores.projectStore.create({
|
||||
name: projectId,
|
||||
@ -253,7 +251,6 @@ test('Roundtrip with strategies in multiple environments works', async () => {
|
||||
await db.stores.environmentStore.create({
|
||||
name: environmentId,
|
||||
type: 'test',
|
||||
displayName: 'Environment for export',
|
||||
});
|
||||
await db.stores.projectStore.create({
|
||||
name: projectId,
|
||||
|
@ -176,7 +176,6 @@ test('Can get strategies for specific environment', async () => {
|
||||
|
||||
await db.stores.environmentStore.create({
|
||||
name: 'testing',
|
||||
displayName: 'simple 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({
|
||||
name: environment,
|
||||
displayName: '',
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
|
@ -23,7 +23,6 @@ test('should enrich metrics with environment from api-token', async () => {
|
||||
|
||||
await environmentStore.create({
|
||||
name: 'some',
|
||||
displayName: '',
|
||||
type: 'test',
|
||||
});
|
||||
|
||||
|
@ -30,8 +30,8 @@
|
||||
"environments": [
|
||||
{
|
||||
"name": "default",
|
||||
"displayName": "Default Environment",
|
||||
"type": "production",
|
||||
|
||||
"sortOrder": 1,
|
||||
"enabled": true,
|
||||
"protected": true
|
||||
|
@ -22,7 +22,6 @@ afterAll(async () => {
|
||||
test('Can get environment', async () => {
|
||||
const created = await db.stores.environmentStore.create({
|
||||
name: 'testenv',
|
||||
displayName: 'Environment for testing',
|
||||
type: 'production',
|
||||
});
|
||||
|
||||
@ -33,7 +32,6 @@ test('Can get environment', async () => {
|
||||
test('Can get all', async () => {
|
||||
await db.stores.environmentStore.create({
|
||||
name: 'testenv2',
|
||||
displayName: 'Environment for testing',
|
||||
type: 'production',
|
||||
});
|
||||
|
||||
@ -44,7 +42,6 @@ test('Can get all', async () => {
|
||||
test('Can connect environment to project', async () => {
|
||||
await db.stores.environmentStore.create({
|
||||
name: 'test-connection',
|
||||
displayName: '',
|
||||
type: 'production',
|
||||
});
|
||||
await stores.featureToggleStore.create('default', {
|
||||
@ -63,7 +60,6 @@ test('Can connect environment to project', async () => {
|
||||
expect(f.environments).toEqual([
|
||||
{
|
||||
name: 'test-connection',
|
||||
displayName: '',
|
||||
enabled: false,
|
||||
sortOrder: 9999,
|
||||
type: 'production',
|
||||
@ -75,7 +71,6 @@ test('Can connect environment to project', async () => {
|
||||
test('Can remove environment from project', async () => {
|
||||
await db.stores.environmentStore.create({
|
||||
name: 'removal-test',
|
||||
displayName: '',
|
||||
type: 'production',
|
||||
});
|
||||
await stores.featureToggleStore.create('default', {
|
||||
@ -92,7 +87,6 @@ test('Can remove environment from project', async () => {
|
||||
expect(f.environments).toEqual([
|
||||
{
|
||||
name: 'removal-test',
|
||||
displayName: '',
|
||||
enabled: false,
|
||||
sortOrder: 9999,
|
||||
type: 'production',
|
||||
@ -113,7 +107,6 @@ test('Can remove environment from project', async () => {
|
||||
test('Adding same environment twice should throw a NameExistsError', async () => {
|
||||
await db.stores.environmentStore.create({
|
||||
name: 'uniqueness-test',
|
||||
displayName: '',
|
||||
type: 'production',
|
||||
});
|
||||
await service.removeEnvironmentFromProject('test-connection', 'default');
|
||||
|
@ -132,7 +132,7 @@ test('should validate name, legal', async () => {
|
||||
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 = {
|
||||
id: 'test-delete',
|
||||
name: 'New project',
|
||||
@ -510,3 +510,29 @@ test('should change project when checks pass', async () => {
|
||||
|
||||
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({
|
||||
name: 'test',
|
||||
displayName: 'Test Env',
|
||||
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(
|
||||
env: Pick<IEnvironment, 'displayName' | 'type' | 'protected'>,
|
||||
env: Pick<IEnvironment, 'type' | 'protected'>,
|
||||
name: string,
|
||||
): Promise<IEnvironment> {
|
||||
const found = this.environments.find(
|
||||
|
@ -148,4 +148,13 @@ export default class FakeFeatureEnvironmentStore
|
||||
): Promise<void> {
|
||||
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-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-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/).
|
||||
|
@ -76,4 +76,26 @@ $context = new UnleashContext(
|
||||
$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).
|
||||
|
Loading…
Reference in New Issue
Block a user