1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02:00

feat: rename :global: env to "default" (#947)

Our testing and internal validation has proven that
the :global: environment concept confuses people more
than the problems it solves. We have thus decided to
group all configuration that was created before the
environment concept was introduced in to the "default
environment. This would still make everything work
as before in addition to introducing the env concept.

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
Ivar Conradi Østhus 2021-09-24 13:55:00 +02:00 committed by GitHub
parent 833ea32752
commit 4b7e1f4a81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 900 additions and 112 deletions

View File

@ -39,6 +39,17 @@ 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,
protected: env.protected,
};
}
const TABLE = 'environments'; const TABLE = 'environments';
export default class EnvironmentStore implements IEnvironmentStore { export default class EnvironmentStore implements IEnvironmentStore {
@ -58,6 +69,18 @@ export default class EnvironmentStore implements IEnvironmentStore {
}); });
} }
async importEnvironments(
environments: IEnvironment[],
): Promise<IEnvironment[]> {
const rows = await this.db(TABLE)
.insert(environments.map(fieldToRow))
.returning(COLUMNS)
.onConflict('name')
.ignore();
return rows.map(mapRow);
}
async deleteAll(): Promise<void> { async deleteAll(): Promise<void> {
await this.db(TABLE).del(); await this.db(TABLE).del();
} }

View File

@ -9,6 +9,7 @@ import {
IStrategyConfig, IStrategyConfig,
} from '../types/model'; } from '../types/model';
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store'; import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
import { DEFAULT_ENV } from '../util/constants';
export interface FeaturesTable { export interface FeaturesTable {
name: string; name: string;
@ -61,10 +62,7 @@ export default class FeatureToggleClientStore
archived: boolean = false, archived: boolean = false,
isAdmin: boolean = true, isAdmin: boolean = true,
): Promise<IFeatureToggleClient[]> { ): Promise<IFeatureToggleClient[]> {
const environments = [':global:']; const environment = featureQuery?.environment || DEFAULT_ENV;
if (featureQuery?.environment) {
environments.push(featureQuery.environment);
}
const stopTimer = this.timer('getFeatureAdmin'); const stopTimer = this.timer('getFeatureAdmin');
let query = this.db('features') let query = this.db('features')
.select( .select(
@ -97,8 +95,9 @@ export default class FeatureToggleClientStore
'feature_environments.environment', 'feature_environments.environment',
); );
}) })
.whereIn('feature_environments.environment', environments) .where('feature_environments.environment', environment)
.where({ archived }); .where({ archived });
if (featureQuery) { if (featureQuery) {
if (featureQuery.tag) { if (featureQuery.tag) {
const tagQuery = this.db const tagQuery = this.db
@ -133,11 +132,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));
} }
if (feature.enabled === undefined) { feature.enabled = r.enabled;
feature.enabled = r.enabled;
} else {
feature.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;

View File

@ -8,6 +8,7 @@ import {
IProjectInsert, IProjectInsert,
IProjectStore, IProjectStore,
} from '../types/stores/project-store'; } from '../types/stores/project-store';
import { DEFAULT_ENV } from '../util/constants';
const COLUMNS = ['id', 'name', 'description', 'created_at', 'health']; const COLUMNS = ['id', 'name', 'description', 'created_at', 'health'];
const TABLE = 'projects'; const TABLE = 'projects';
@ -99,16 +100,16 @@ class ProjectStore implements IProjectStore {
.onConflict('id') .onConflict('id')
.ignore(); .ignore();
if (rows.length > 0) { if (rows.length > 0) {
await this.addGlobalEnvironment(rows); await this.addDefaultEnvironment(rows);
return rows.map(this.mapRow); return rows.map(this.mapRow);
} }
return []; return [];
} }
async addGlobalEnvironment(projects: any[]): Promise<void> { async addDefaultEnvironment(projects: any[]): Promise<void> {
const environments = projects.map((p) => ({ const environments = projects.map((p) => ({
project_id: p.id, project_id: p.id,
environment_name: ':global:', environment_name: DEFAULT_ENV,
})); }));
await this.db('project_environments') await this.db('project_environments')
.insert(environments) .insert(environments)

View File

@ -16,8 +16,8 @@ import FeatureToggleServiceV2 from '../../services/feature-toggle-service-v2';
import { featureSchema, querySchema } from '../../schema/feature-schema'; import { featureSchema, querySchema } from '../../schema/feature-schema';
import { IFeatureToggleQuery } from '../../types/model'; import { IFeatureToggleQuery } from '../../types/model';
import FeatureTagService from '../../services/feature-tag-service'; import FeatureTagService from '../../services/feature-tag-service';
import { GLOBAL_ENV } from '../../types/environment';
import { IAuthRequest } from '../unleash-types'; import { IAuthRequest } from '../unleash-types';
import { DEFAULT_ENV } from '../../util/constants';
const version = 1; const version = 1;
@ -110,11 +110,11 @@ class FeatureController extends Controller {
private async getLegacyFeatureToggle(name: string): Promise<any> { private async getLegacyFeatureToggle(name: string): Promise<any> {
const feature = await this.featureService2.getFeatureToggle(name); const feature = await this.featureService2.getFeatureToggle(name);
const globalEnv = feature.environments.find( const defaultEnv = feature.environments.find(
(e) => e.name === GLOBAL_ENV, (e) => e.name === DEFAULT_ENV,
); );
const strategies = globalEnv?.strategies || []; const strategies = defaultEnv?.strategies || [];
const enabled = globalEnv?.enabled || false; const enabled = defaultEnv?.enabled || false;
delete feature.environments; delete feature.environments;
return { ...feature, enabled, strategies }; return { ...feature, enabled, strategies };
@ -181,7 +181,7 @@ class FeatureController extends Controller {
await this.featureService2.updateEnabled( await this.featureService2.updateEnabled(
createdFeature.project, createdFeature.project,
createdFeature.name, createdFeature.name,
GLOBAL_ENV, DEFAULT_ENV,
enabled, enabled,
userName, userName,
); );
@ -228,7 +228,7 @@ class FeatureController extends Controller {
await this.featureService2.updateEnabled( await this.featureService2.updateEnabled(
projectId, projectId,
updatedFeature.name, updatedFeature.name,
GLOBAL_ENV, DEFAULT_ENV,
updatedFeature.enabled, updatedFeature.enabled,
userName, userName,
); );
@ -246,7 +246,7 @@ class FeatureController extends Controller {
const feature = await this.featureService2.toggle( const feature = await this.featureService2.toggle(
projectId, projectId,
featureName, featureName,
GLOBAL_ENV, DEFAULT_ENV,
userName, userName,
); );
res.status(200).json(feature); res.status(200).json(feature);
@ -259,7 +259,7 @@ class FeatureController extends Controller {
const feature = await this.featureService2.updateEnabled( const feature = await this.featureService2.updateEnabled(
projectId, projectId,
featureName, featureName,
GLOBAL_ENV, DEFAULT_ENV,
true, true,
userName, userName,
); );
@ -273,7 +273,7 @@ class FeatureController extends Controller {
const feature = await this.featureService2.updateEnabled( const feature = await this.featureService2.updateEnabled(
projectId, projectId,
featureName, featureName,
GLOBAL_ENV, DEFAULT_ENV,
false, false,
userName, userName,
); );

View File

@ -36,6 +36,7 @@ class StateController extends Controller {
this.logger = config.getLogger('/admin-api/state.ts'); this.logger = config.getLogger('/admin-api/state.ts');
this.stateService = stateService; this.stateService = stateService;
this.fileupload('/import', upload.single('file'), this.import, ADMIN); this.fileupload('/import', upload.single('file'), this.import, ADMIN);
this.post('/import', this.import, ADMIN);
this.get('/export', this.export, ADMIN); this.get('/export', this.export, ADMIN);
} }

View File

@ -1,5 +1,6 @@
import joi from 'joi'; import joi from 'joi';
import { ALL, ApiTokenType } from '../types/models/api-token'; import { ALL, ApiTokenType } from '../types/models/api-token';
import { DEFAULT_ENV } from '../util/constants';
export const createApiToken = joi export const createApiToken = joi
.object() .object()
@ -12,6 +13,10 @@ export const createApiToken = joi
.valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT), .valid(ApiTokenType.ADMIN, ApiTokenType.CLIENT),
expiresAt: joi.date().optional(), expiresAt: joi.date().optional(),
project: joi.string().optional().default(ALL), project: joi.string().optional().default(ALL),
environment: joi.string().optional().default(ALL), environment: joi.when('type', {
is: joi.string().valid(ApiTokenType.CLIENT),
then: joi.string().optional().default(DEFAULT_ENV),
otherwise: joi.string().optional().default(ALL),
}),
}) })
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false }); .options({ stripUnknown: true, allowUnknown: false, abortEarly: false });

View File

@ -83,7 +83,7 @@ export class ApiTokenService {
return this.store.delete(secret); return this.store.delete(secret);
} }
private validateAdminToken({ type, project, environment }) { private validateNewApiToken({ type, project, environment }) {
if (type === ApiTokenType.ADMIN && project !== ALL) { if (type === ApiTokenType.ADMIN && project !== ALL) {
throw new BadDataError( throw new BadDataError(
'Admin token cannot be scoped to single project', 'Admin token cannot be scoped to single project',
@ -95,12 +95,18 @@ export class ApiTokenService {
'Admin token cannot be scoped to single environment', 'Admin token cannot be scoped to single environment',
); );
} }
if (type === ApiTokenType.CLIENT && environment === ALL) {
throw new BadDataError(
'Client token cannot be scoped to all environments',
);
}
} }
public async createApiToken( public async createApiToken(
newToken: Omit<IApiTokenCreate, 'secret'>, newToken: Omit<IApiTokenCreate, 'secret'>,
): Promise<IApiToken> { ): Promise<IApiToken> {
this.validateAdminToken(newToken); this.validateNewApiToken(newToken);
const secret = this.generateSecretKey(newToken); const secret = this.generateSecretKey(newToken);
const createNewToken = { ...newToken, secret }; const createNewToken = { ...newToken, secret };

View File

@ -19,7 +19,6 @@ import {
FEATURE_STRATEGY_UPDATE, FEATURE_STRATEGY_UPDATE,
FEATURE_UPDATED, FEATURE_UPDATED,
} from '../types/events'; } from '../types/events';
import { GLOBAL_ENV } from '../types/environment';
import NotFoundError from '../error/notfound-error'; import NotFoundError from '../error/notfound-error';
import { import {
FeatureConfigurationClient, FeatureConfigurationClient,
@ -42,6 +41,7 @@ import {
} from '../types/model'; } from '../types/model';
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store'; import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
import { DEFAULT_ENV } from '../util/constants';
class FeatureToggleServiceV2 { class FeatureToggleServiceV2 {
private logger: Logger; private logger: Logger;
@ -96,7 +96,7 @@ class FeatureToggleServiceV2 {
projectId: string, projectId: string,
featureName: string, featureName: string,
userName: string, userName: string,
environment: string = GLOBAL_ENV, environment: string = DEFAULT_ENV,
): Promise<IStrategyConfig> { ): Promise<IStrategyConfig> {
try { try {
const newFeatureStrategy = const newFeatureStrategy =
@ -226,7 +226,7 @@ class FeatureToggleServiceV2 {
id: string, id: string,
userName: string, userName: string,
project: string = 'default', project: string = 'default',
environment: string = GLOBAL_ENV, environment: string = DEFAULT_ENV,
): Promise<void> { ): Promise<void> {
await this.featureStrategiesStore.delete(id); await this.featureStrategiesStore.delete(id);
await this.eventStore.store({ await this.eventStore.store({
@ -243,7 +243,7 @@ class FeatureToggleServiceV2 {
async getStrategiesForEnvironment( async getStrategiesForEnvironment(
project: string, project: string,
featureName: string, featureName: string,
environment: string = GLOBAL_ENV, environment: string = DEFAULT_ENV,
): Promise<IStrategyConfig[]> { ): Promise<IStrategyConfig[]> {
const hasEnv = await this.featureEnvironmentStore.featureHasEnvironment( const hasEnv = await this.featureEnvironmentStore.featureHasEnvironment(
environment, environment,
@ -405,7 +405,7 @@ class FeatureToggleServiceV2 {
async removeAllStrategiesForEnv( async removeAllStrategiesForEnv(
toggleName: string, toggleName: string,
environment: string = GLOBAL_ENV, environment: string = DEFAULT_ENV,
): Promise<void> { ): Promise<void> {
await this.featureStrategiesStore.removeAllStrategiesForFeatureEnv( await this.featureStrategiesStore.removeAllStrategiesForFeatureEnv(
toggleName, toggleName,
@ -605,11 +605,11 @@ class FeatureToggleServiceV2 {
await this.featureStrategiesStore.getFeatureToggleWithEnvs( await this.featureStrategiesStore.getFeatureToggleWithEnvs(
featureName, featureName,
); );
const globalEnv = feature.environments.find( const defaultEnv = feature.environments.find(
(e) => e.name === GLOBAL_ENV, (e) => e.name === DEFAULT_ENV,
); );
const strategies = globalEnv?.strategies || []; const strategies = defaultEnv?.strategies || [];
const enabled = globalEnv?.enabled || false; const enabled = defaultEnv?.enabled || false;
return { ...feature, enabled, strategies }; return { ...feature, enabled, strategies };
} }

View File

@ -20,7 +20,6 @@ import {
IUserWithRole, IUserWithRole,
RoleName, RoleName,
} from '../types/model'; } from '../types/model';
import { GLOBAL_ENV } from '../types/environment';
import { IEnvironmentStore } from '../types/stores/environment-store'; 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';
@ -31,6 +30,7 @@ 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;
@ -123,7 +123,8 @@ export default class ProjectService {
await this.store.create(data); await this.store.create(data);
await this.featureEnvironmentStore.connectProject(GLOBAL_ENV, data.id); // TODO: we should only connect to enabled environments
await this.featureEnvironmentStore.connectProject(DEFAULT_ENV, data.id);
await this.accessService.createDefaultProjectRoles(user, data.id); await this.accessService.createDefaultProjectRoles(user, data.id);

View File

@ -13,7 +13,7 @@ export const featureStrategySchema = joi
featureName: joi.string(), featureName: joi.string(),
projectId: joi.string(), projectId: joi.string(),
environment: joi.string(), environment: joi.string(),
parameters: joi.object().optional(), parameters: joi.object().optional().allow(null),
constraints: joi.array().optional(), constraints: joi.array().optional(),
strategyName: joi.string(), strategyName: joi.string(),
}) })
@ -26,7 +26,7 @@ export const featureEnvironmentsSchema = joi.object().keys({
}); });
export const environmentSchema = joi.object().keys({ export const environmentSchema = joi.object().keys({
name: nameType.allow(':global:'), name: nameType,
displayName: joi.string().optional().allow(''), displayName: joi.string().optional().allow(''),
type: joi.string().required(), type: joi.string().required(),
sortOrder: joi.number().optional(), sortOrder: joi.number().optional(),

View File

@ -1,11 +1,13 @@
import { stateSchema } from './state-schema'; import { stateSchema } from './state-schema';
import { import {
DROP_ENVIRONMENTS,
DROP_FEATURE_TAGS, DROP_FEATURE_TAGS,
DROP_FEATURES, DROP_FEATURES,
DROP_PROJECTS, DROP_PROJECTS,
DROP_STRATEGIES, DROP_STRATEGIES,
DROP_TAG_TYPES, DROP_TAG_TYPES,
DROP_TAGS, DROP_TAGS,
ENVIRONMENT_IMPORT,
FEATURE_IMPORT, FEATURE_IMPORT,
FEATURE_TAG_IMPORT, FEATURE_TAG_IMPORT,
PROJECT_IMPORT, PROJECT_IMPORT,
@ -28,7 +30,6 @@ import {
IProject, IProject,
IStrategyConfig, IStrategyConfig,
} from '../types/model'; } from '../types/model';
import { GLOBAL_ENV } from '../types/environment';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { import {
IFeatureTag, IFeatureTag,
@ -44,6 +45,8 @@ import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-stor
import { IEnvironmentStore } from '../types/stores/environment-store'; import { IEnvironmentStore } from '../types/stores/environment-store';
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
import { IUnleashStores } from '../types/stores'; import { IUnleashStores } from '../types/stores';
import { DEFAULT_ENV } from '../util/constants';
import { GLOBAL_ENV } from '../types/environment';
export interface IBackupOption { export interface IBackupOption {
includeFeatureToggles: boolean; includeFeatureToggles: boolean;
@ -118,14 +121,47 @@ export default class StateService {
); );
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
replaceGlobalEnvWithDefaultEnv(data: any) {
data.environments?.forEach((e) => {
if (e.name === GLOBAL_ENV) {
e.name = DEFAULT_ENV;
}
});
data.featureEnvironments?.forEach((fe) => {
if (fe.environment === GLOBAL_ENV) {
// eslint-disable-next-line no-param-reassign
fe.environment = DEFAULT_ENV;
}
});
data.featureStrategies?.forEach((fs) => {
if (fs.environment === GLOBAL_ENV) {
// eslint-disable-next-line no-param-reassign
fs.environment = DEFAULT_ENV;
}
});
}
async import({ async import({
data, data,
userName = 'importUser', userName = 'importUser',
dropBeforeImport = false, dropBeforeImport = false,
keepExisting = true, keepExisting = true,
}: IImportData): Promise<void> { }: IImportData): Promise<void> {
if (data.version === 2) {
this.replaceGlobalEnvWithDefaultEnv(data);
}
const importData = await stateSchema.validateAsync(data); const importData = await stateSchema.validateAsync(data);
if (importData.environments) {
await this.importEnvironments({
environments: data.environments,
userName,
dropBeforeImport,
keepExisting,
});
}
if (importData.features) { if (importData.features) {
let projectData; let projectData;
if (!importData.version || importData.version === 1) { if (!importData.version || importData.version === 1) {
@ -245,14 +281,14 @@ export default class StateService {
projectId: f.project, projectId: f.project,
constraints: strategy.constraints || [], constraints: strategy.constraints || [],
parameters: strategy.parameters || {}, parameters: strategy.parameters || {},
environment: GLOBAL_ENV, environment: DEFAULT_ENV,
strategyName: strategy.name, strategyName: strategy.name,
})), })),
); );
const newFeatures = features; const newFeatures = features;
const featureEnvironments = features.map((feature) => ({ const featureEnvironments = features.map((feature) => ({
featureName: feature.name, featureName: feature.name,
environment: GLOBAL_ENV, environment: DEFAULT_ENV,
enabled: feature.enabled, enabled: feature.enabled,
})); }));
return { return {
@ -340,6 +376,42 @@ export default class StateService {
); );
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async importEnvironments({
environments,
userName,
dropBeforeImport,
keepExisting,
}): Promise<void> {
this.logger.info(`Import ${environments.length} projects`);
const oldEnvs = dropBeforeImport
? []
: await this.environmentStore.getAll();
if (dropBeforeImport) {
this.logger.info('Dropping existing environments');
await this.environmentStore.deleteAll();
await this.eventStore.store({
type: DROP_ENVIRONMENTS,
createdBy: userName,
data: { name: 'all-projects' },
});
}
const envsImport = environments.filter((env) =>
keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true,
);
if (envsImport.length > 0) {
const importedEnvs = await this.environmentStore.importEnvironments(
envsImport,
);
const importedEnvironmentEvents = importedEnvs.map((env) => ({
type: ENVIRONMENT_IMPORT,
createdBy: userName,
data: env,
}));
await this.eventStore.batchStore(importedEnvironmentEvents);
}
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async importProjects({ async importProjects({
projects, projects,
@ -576,7 +648,7 @@ export default class StateService {
environments, environments,
featureEnvironments, featureEnvironments,
]) => ({ ]) => ({
version: 2, version: 3,
features, features,
strategies, strategies,
projects, projects,

View File

@ -48,3 +48,5 @@ export const DB_POOL_UPDATE = 'db-pool-update';
export const USER_CREATED = 'user-created'; export const USER_CREATED = 'user-created';
export const USER_UPDATED = 'user-updated'; export const USER_UPDATED = 'user-updated';
export const USER_DELETED = 'user-deleted'; export const USER_DELETED = 'user-deleted';
export const DROP_ENVIRONMENTS = 'drop-environments';
export const ENVIRONMENT_IMPORT = 'environment-import';

View File

@ -14,4 +14,5 @@ export interface IEnvironmentStore extends Store<IEnvironment, string> {
value: string | number | boolean, value: string | number | boolean,
): Promise<void>; ): Promise<void>;
updateSortOrder(id: string, value: number): Promise<void>; updateSortOrder(id: string, value: number): Promise<void>;
importEnvironments(environments: IEnvironment[]): Promise<IEnvironment[]>;
} }

View File

@ -1,2 +1,3 @@
export const MILLISECONDS_IN_DAY = 86400000; export const MILLISECONDS_IN_DAY = 86400000;
export const MILLISECONDS_IN_ONE_HOUR = 3600000; export const MILLISECONDS_IN_ONE_HOUR = 3600000;
export const DEFAULT_ENV = 'default';

View File

@ -0,0 +1,33 @@
'use strict';
const up = function (db, cb) {
db.runSql(
`
INSERT INTO environments(name, display_name, protected, sort_order) VALUES ('default', 'Default Environment', true, 1);
ALTER TABLE feature_strategies ALTER COLUMN environment SET DEFAULT 'default';
ALTER TABLE feature_environments ALTER COLUMN environment SET DEFAULT 'default';
UPDATE feature_strategies SET environment = 'default' WHERE environment = ':global:';
UPDATE feature_environments SET environment = 'default' WHERE environment = ':global:';
UPDATE project_environments SET environment_name = 'default' WHERE environment_name = ':global:';
DELETE FROM environments WHERE name = ':global:';
`,
cb,
);
};
const down = function (db, cb) {
db.runSql(
`
INSERT INTO environments(name, display_name, protected) VALUES (':global:', 'Across all environments', true);
ALTER TABLE feature_strategies ALTER COLUMN environment SET DEFAULT ':global:';
ALTER TABLE feature_environments ALTER COLUMN environment SET DEFAULT ':global:';
UPDATE feature_strategies SET environment = ':global:' WHERE environment = 'default';
UPDATE feature_environments SET environment = ':global:' WHERE environment = 'default';
UPDATE project_environments SET environment_name = ':global:' WHERE environment_name = 'default';
DELETE FROM environments WHERE name = 'default';
`,
cb,
);
};
module.exports = { up, down };

View File

@ -0,0 +1,20 @@
'use strict';
exports.up = function (db, cb) {
db.runSql(
`
UPDATE api_tokens SET environment = 'default' WHERE environment = ':global:';
UPDATE api_tokens SET environment = 'default' WHERE type='client' AND environment is null;
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
UPDATE api_tokens SET environment = null WHERE type='client' AND environment = 'default';
`,
cb,
);
};

View File

@ -2,6 +2,7 @@ import { setupApp } from '../../helpers/test-helper';
import dbInit from '../../helpers/database-init'; import dbInit from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import { ALL, ApiTokenType } from '../../../../lib/types/models/api-token'; import { ALL, ApiTokenType } from '../../../../lib/types/models/api-token';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
let db; let db;
let app; let app;
@ -23,7 +24,6 @@ afterEach(async () => {
}); });
test('returns empty list of tokens', async () => { test('returns empty list of tokens', async () => {
expect.assertions(1);
return app.request return app.request
.get('/api/admin/api-tokens') .get('/api/admin/api-tokens')
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -34,7 +34,6 @@ test('returns empty list of tokens', async () => {
}); });
test('creates new client token', async () => { test('creates new client token', async () => {
expect.assertions(4);
return app.request return app.request
.post('/api/admin/api-tokens') .post('/api/admin/api-tokens')
.send({ .send({
@ -52,7 +51,6 @@ test('creates new client token', async () => {
}); });
test('creates new admin token', async () => { test('creates new admin token', async () => {
expect.assertions(5);
return app.request return app.request
.post('/api/admin/api-tokens') .post('/api/admin/api-tokens')
.send({ .send({
@ -64,6 +62,7 @@ test('creates new admin token', async () => {
.expect((res) => { .expect((res) => {
expect(res.body.username).toBe('default-admin'); expect(res.body.username).toBe('default-admin');
expect(res.body.type).toBe('admin'); expect(res.body.type).toBe('admin');
expect(res.body.environment).toBe(ALL);
expect(res.body.createdAt).toBeTruthy(); expect(res.body.createdAt).toBeTruthy();
expect(res.body.expiresAt).toBeFalsy(); expect(res.body.expiresAt).toBeFalsy();
expect(res.body.secret.length > 16).toBe(true); expect(res.body.secret.length > 16).toBe(true);
@ -71,7 +70,6 @@ test('creates new admin token', async () => {
}); });
test('creates new ADMIN token should fix casing', async () => { test('creates new ADMIN token should fix casing', async () => {
expect.assertions(5);
return app.request return app.request
.post('/api/admin/api-tokens') .post('/api/admin/api-tokens')
.send({ .send({
@ -90,7 +88,6 @@ test('creates new ADMIN token should fix casing', async () => {
}); });
test('creates new admin token with expiry', async () => { test('creates new admin token with expiry', async () => {
expect.assertions(1);
const expiresAt = new Date(); const expiresAt = new Date();
const expiresAtAsISOStr = JSON.parse(JSON.stringify(expiresAt)); const expiresAtAsISOStr = JSON.parse(JSON.stringify(expiresAt));
return app.request return app.request
@ -108,8 +105,6 @@ test('creates new admin token with expiry', async () => {
}); });
test('update admin token with expiry', async () => { test('update admin token with expiry', async () => {
expect.assertions(2);
const tokenSecret = 'random-secret-update'; const tokenSecret = 'random-secret-update';
await db.stores.apiTokenStore.insert({ await db.stores.apiTokenStore.insert({
@ -137,8 +132,6 @@ test('update admin token with expiry', async () => {
}); });
test('creates a lot of client tokens', async () => { test('creates a lot of client tokens', async () => {
expect.assertions(4);
const requests = []; const requests = [];
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
@ -166,8 +159,6 @@ test('creates a lot of client tokens', async () => {
}); });
test('removes api token', async () => { test('removes api token', async () => {
expect.assertions(1);
const tokenSecret = 'random-secret'; const tokenSecret = 'random-secret';
await db.stores.apiTokenStore.insert({ await db.stores.apiTokenStore.insert({
@ -202,7 +193,7 @@ test('creates new client token: project & environment defaults to "*"', async ()
.expect((res) => { .expect((res) => {
expect(res.body.type).toBe('client'); expect(res.body.type).toBe('client');
expect(res.body.secret.length > 16).toBe(true); expect(res.body.secret.length > 16).toBe(true);
expect(res.body.environment).toBe(ALL); expect(res.body.environment).toBe(DEFAULT_ENV);
expect(res.body.project).toBe(ALL); expect(res.body.project).toBe(ALL);
}); });
}); });
@ -214,14 +205,14 @@ test('creates new client token with project & environment set', async () => {
username: 'default-client', username: 'default-client',
type: 'client', type: 'client',
project: 'default', project: 'default',
environment: ':global:', environment: DEFAULT_ENV,
}) })
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(201) .expect(201)
.expect((res) => { .expect((res) => {
expect(res.body.type).toBe('client'); expect(res.body.type).toBe('client');
expect(res.body.secret.length > 16).toBe(true); expect(res.body.secret.length > 16).toBe(true);
expect(res.body.environment).toBe(':global:'); expect(res.body.environment).toBe(DEFAULT_ENV);
expect(res.body.project).toBe('default'); expect(res.body.project).toBe('default');
}); });
}); });
@ -236,7 +227,7 @@ test('should prefix default token with "*:*."', async () => {
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(201) .expect(201)
.expect((res) => { .expect((res) => {
expect(res.body.secret).toMatch(/\*:\*\..*/); expect(res.body.secret).toMatch(/\*:default\..*/);
}); });
}); });
@ -247,12 +238,12 @@ test('should prefix token with "project:environment."', async () => {
username: 'default-client', username: 'default-client',
type: 'client', type: 'client',
project: 'default', project: 'default',
environment: ':global:', environment: DEFAULT_ENV,
}) })
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(201) .expect(201)
.expect((res) => { .expect((res) => {
expect(res.body.secret).toMatch(/default::global:\..*/); expect(res.body.secret).toMatch(/default:default\..*/);
}); });
}); });
@ -323,7 +314,19 @@ test('admin token only supports ALL environments', async () => {
username: 'default-admin', username: 'default-admin',
type: 'admin', type: 'admin',
project: '*', project: '*',
environment: ':global:', environment: DEFAULT_ENV,
})
.set('Content-Type', 'application/json')
.expect(400);
});
test('client tokens cannot span all environments', async () => {
return app.request
.post('/api/admin/api-tokens')
.send({
username: 'default-client',
type: 'client',
environment: ALL,
}) })
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(400); .expect(400);

View File

@ -1,6 +1,7 @@
import dbInit, { ITestDb } from '../../helpers/database-init'; import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import { IUnleashTest, setupApp } from '../../helpers/test-helper'; import { IUnleashTest, setupApp } from '../../helpers/test-helper';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
@ -23,8 +24,8 @@ 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: 'Across all environments', displayName: 'Default Environment',
name: ':global:', name: DEFAULT_ENV,
enabled: true, enabled: true,
sortOrder: 1, sortOrder: 1,
type: 'production', type: 'production',
@ -43,7 +44,7 @@ test('Can update sort order', async () => {
await app.request await app.request
.put('/api/admin/environments/sort-order') .put('/api/admin/environments/sort-order')
.send({ .send({
':global:': 2, [DEFAULT_ENV]: 2,
[envName]: 1, [envName]: 1,
}) })
.expect(200); .expect(200);
@ -56,11 +57,11 @@ test('Can update sort order', async () => {
const updatedSort = res.body.environments.find( const updatedSort = res.body.environments.find(
(t) => t.name === envName, (t) => t.name === envName,
); );
const global = res.body.environments.find( const defaultEnv = res.body.environments.find(
(t) => t.name === ':global:', (t) => t.name === DEFAULT_ENV,
); );
expect(updatedSort.sortOrder).toBe(1); expect(updatedSort.sortOrder).toBe(1);
expect(global.sortOrder).toBe(2); expect(defaultEnv.sortOrder).toBe(2);
}); });
}); });
@ -70,7 +71,7 @@ test('Sort order will fail on wrong data format', async () => {
await app.request await app.request
.put('/api/admin/environments/sort-order') .put('/api/admin/environments/sort-order')
.send({ .send({
':global:': 'test', [DEFAULT_ENV]: 'test',
[envName]: 1, [envName]: 1,
}) })
.expect(400); .expect(400);

View File

@ -1,6 +1,7 @@
import dbInit, { ITestDb } from '../../../helpers/database-init'; import dbInit, { ITestDb } from '../../../helpers/database-init';
import { IUnleashTest, setupApp } from '../../../helpers/test-helper'; import { IUnleashTest, setupApp } from '../../../helpers/test-helper';
import getLogger from '../../../../fixtures/no-logger'; import getLogger from '../../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../../lib/util/constants';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
@ -16,7 +17,7 @@ afterEach(async () => {
); );
await Promise.all( await Promise.all(
all all
.filter((env) => env !== ':global:') .filter((env) => env !== DEFAULT_ENV)
.map(async (env) => .map(async (env) =>
db.stores.projectStore.deleteEnvironmentForProject( db.stores.projectStore.deleteEnvironmentForProject(
'default', 'default',

View File

@ -1,7 +1,7 @@
import dbInit, { ITestDb } from '../../../helpers/database-init'; import dbInit, { ITestDb } from '../../../helpers/database-init';
import { IUnleashTest, setupApp } from '../../../helpers/test-helper'; import { IUnleashTest, setupApp } from '../../../helpers/test-helper';
import getLogger from '../../../../fixtures/no-logger'; import getLogger from '../../../../fixtures/no-logger';
import { GLOBAL_ENV } from '../../../../../lib/types/environment'; import { DEFAULT_ENV } from '../../../../../lib/util/constants';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
@ -20,7 +20,7 @@ afterEach(async () => {
); );
await Promise.all( await Promise.all(
all all
.filter((env) => env !== ':global:') .filter((env) => env !== DEFAULT_ENV)
.map(async (env) => .map(async (env) =>
db.stores.projectStore.deleteEnvironmentForProject( db.stores.projectStore.deleteEnvironmentForProject(
'default', 'default',
@ -181,7 +181,7 @@ test('Project overview includes environment connected to feature', async () => {
.get('/api/admin/projects/default') .get('/api/admin/projects/default')
.expect(200) .expect(200)
.expect((r) => { .expect((r) => {
expect(r.body.features[0].environments[0].name).toBe(':global:'); expect(r.body.features[0].environments[0].name).toBe(DEFAULT_ENV);
expect(r.body.features[0].environments[1].name).toBe( expect(r.body.features[0].environments[1].name).toBe(
'project-overview', 'project-overview',
); );
@ -540,8 +540,8 @@ test('Should archive feature toggle', async () => {
expect(toggle).toBeDefined(); expect(toggle).toBeDefined();
}); });
test('Can add strategy to feature toggle to default env', async () => { test('Can add strategy to feature toggle to a "some-env-2"', async () => {
const envName = 'default'; const envName = 'some-env-2';
const featureName = 'feature.strategy.toggle'; const featureName = 'feature.strategy.toggle';
// Create environment // Create environment
await db.stores.environmentStore.create({ await db.stores.environmentStore.create({
@ -644,7 +644,7 @@ test('Environments are returned in sortOrder', async () => {
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
expect(res.body.environments).toHaveLength(3); expect(res.body.environments).toHaveLength(3);
expect(res.body.environments[0].name).toBe(GLOBAL_ENV); expect(res.body.environments[0].name).toBe(DEFAULT_ENV);
expect(res.body.environments[1].name).toBe(sortedSecond); expect(res.body.environments[1].name).toBe(sortedSecond);
expect(res.body.environments[2].name).toBe(sortedLast); expect(res.body.environments[2].name).toBe(sortedLast);
}); });

View File

@ -1,7 +1,7 @@
import dbInit, { ITestDb } from '../../helpers/database-init'; import dbInit, { ITestDb } from '../../helpers/database-init';
import { IUnleashTest, setupApp } from '../../helpers/test-helper'; import { IUnleashTest, setupApp } from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import { GLOBAL_ENV } from '../../../../lib/types/environment'; import { DEFAULT_ENV } from '../../../../lib/util/constants';
const importData = require('../../../examples/import.json'); const importData = require('../../../examples/import.json');
@ -265,7 +265,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
projectId, projectId,
); );
await app.services.environmentService.addEnvironmentToProject( await app.services.environmentService.addEnvironmentToProject(
GLOBAL_ENV, DEFAULT_ENV,
projectId, projectId,
); );
await app.services.featureToggleServiceV2.createFeatureToggle( await app.services.featureToggleServiceV2.createFeatureToggle(
@ -299,7 +299,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
}, },
projectId, projectId,
featureName, featureName,
GLOBAL_ENV, DEFAULT_ENV,
); );
const data = await app.services.stateService.export({}); const data = await app.services.stateService.export({});
await app.services.stateService.import({ await app.services.stateService.import({
@ -311,3 +311,17 @@ test('Roundtrip with strategies in multiple environments works', async () => {
const f = await app.services.featureToggleServiceV2.getFeature(featureName); const f = await app.services.featureToggleServiceV2.getFeature(featureName);
expect(f.environments).toHaveLength(2); expect(f.environments).toHaveLength(2);
}); });
test(`Importing version 2 replaces :global: environment with 'default'`, async () => {
await app.request
.post('/api/admin/state/import')
.attach('file', 'src/test/examples/exported412-version2.json')
.expect(202);
const env = await app.services.environmentService.get(DEFAULT_ENV);
expect(env).toBeTruthy();
const feature = await app.services.featureToggleServiceV2.getFeatureToggle(
'this-is-fun',
);
expect(feature.environments).toHaveLength(1);
expect(feature.environments[0].name).toBe(DEFAULT_ENV);
});

View File

@ -1,6 +1,7 @@
import { IUnleashTest, setupApp } from '../../helpers/test-helper'; import { IUnleashTest, setupApp } from '../../helpers/test-helper';
import dbInit, { ITestDb } from '../../helpers/database-init'; import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
@ -153,6 +154,7 @@ test('Can filter features by namePrefix', async () => {
test('Can get strategies for specific environment', async () => { test('Can get strategies for specific environment', async () => {
const featureName = 'test.feature.with.env'; const featureName = 'test.feature.with.env';
const env = DEFAULT_ENV;
// Create feature toggle // Create feature toggle
await app.request.post('/api/admin/projects/default/features').send({ await app.request.post('/api/admin/projects/default/features').send({
@ -163,7 +165,7 @@ test('Can get strategies for specific environment', async () => {
// Add global strategy // Add global strategy
await app.request await app.request
.post( .post(
`/api/admin/projects/default/features/${featureName}/environments/:global:/strategies`, `/api/admin/projects/default/features/${featureName}/environments/${env}/strategies`,
) )
.send({ .send({
name: 'default', name: 'default',
@ -198,7 +200,7 @@ test('Can get strategies for specific environment', async () => {
.expect(200) .expect(200)
.expect((res) => { .expect((res) => {
expect(res.body.name).toBe(featureName); expect(res.body.name).toBe(featureName);
expect(res.body.strategies).toHaveLength(2); expect(res.body.strategies).toHaveLength(1);
expect( expect(
res.body.strategies.find((s) => s.name === 'custom1'), res.body.strategies.find((s) => s.name === 'custom1'),
).toBeDefined(); ).toBeDefined();

View File

@ -3,6 +3,7 @@ import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import { ApiTokenService } from '../../../../lib/services/api-token-service'; import { ApiTokenService } from '../../../../lib/services/api-token-service';
import { ApiTokenType } from '../../../../lib/types/models/api-token'; import { ApiTokenType } from '../../../../lib/types/models/api-token';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
let app: IUnleashTest; let app: IUnleashTest;
let db: ITestDb; let db: ITestDb;
@ -117,11 +118,11 @@ afterAll(async () => {
await db.destroy(); await db.destroy();
}); });
test('returns feature toggle with :global: config', async () => { test('returns feature toggle with "default" config', async () => {
const token = await apiTokenService.createApiToken({ const token = await apiTokenService.createApiToken({
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
username, username,
environment: ':global:', environment: DEFAULT_ENV,
project, project,
}); });
await app.request await app.request
@ -157,7 +158,8 @@ test('returns feature toggle with testing environment config', async () => {
const f2 = features.find((f) => f.name === feature2); const f2 = features.find((f) => f.name === feature2);
expect(features).toHaveLength(2); expect(features).toHaveLength(2);
expect(f1.strategies).toHaveLength(2); expect(f1.strategies).toHaveLength(1);
expect(f1.strategies[0].name).toBe('custom-testing');
expect(f2.strategies).toHaveLength(1); expect(f2.strategies).toHaveLength(1);
expect(query.project[0]).toBe(project); expect(query.project[0]).toBe(project);
expect(query.environment).toBe(environment); expect(query.environment).toBe(environment);

View File

@ -10,6 +10,7 @@ import noLoggerProvider from '../../fixtures/no-logger';
import EnvironmentStore from '../../../lib/db/environment-store'; import EnvironmentStore from '../../../lib/db/environment-store';
import { IUnleashStores } from '../../../lib/types'; import { IUnleashStores } from '../../../lib/types';
import { IFeatureEnvironmentStore } from '../../../lib/types/stores/feature-environment-store'; import { IFeatureEnvironmentStore } from '../../../lib/types/stores/feature-environment-store';
import { DEFAULT_ENV } from '../../../lib/util/constants';
// require('db-migrate-shared').log.silence(false); // require('db-migrate-shared').log.silence(false);
@ -54,7 +55,7 @@ function createTagTypes(store) {
} }
async function connectProject(store: IFeatureEnvironmentStore): Promise<void> { async function connectProject(store: IFeatureEnvironmentStore): Promise<void> {
await store.connectProject(':global:', 'default'); await store.connectProject(DEFAULT_ENV, 'default');
} }
async function createEnvironments(store: EnvironmentStore): Promise<void> { async function createEnvironments(store: EnvironmentStore): Promise<void> {

View File

@ -29,8 +29,8 @@
], ],
"environments": [ "environments": [
{ {
"name": ":global:", "name": "default",
"displayName": "Across all environments", "displayName": "Default Environment",
"type": "production", "type": "production",
"sortOrder": 1, "sortOrder": 1,
"enabled": true, "enabled": true,

View File

@ -3,6 +3,7 @@ import getLogger from '../../fixtures/no-logger';
import { ApiTokenService } from '../../../lib/services/api-token-service'; import { ApiTokenService } from '../../../lib/services/api-token-service';
import { createTestConfig } from '../../config/test-config'; import { createTestConfig } from '../../config/test-config';
import { ApiTokenType, IApiToken } from '../../../lib/types/models/api-token'; import { ApiTokenType, IApiToken } from '../../../lib/types/models/api-token';
import { DEFAULT_ENV } from '../../../lib/util/constants';
let db; let db;
let stores; let stores;
@ -43,7 +44,7 @@ test('should create client token', async () => {
username: 'default-client', username: 'default-client',
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
project: '*', project: '*',
environment: '*', environment: DEFAULT_ENV,
}); });
const allTokens = await apiTokenService.getAllTokens(); const allTokens = await apiTokenService.getAllTokens();
@ -73,7 +74,7 @@ test('should set expiry of token', async () => {
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
expiresAt: time, expiresAt: time,
project: '*', project: '*',
environment: '*', environment: DEFAULT_ENV,
}); });
const [token] = await apiTokenService.getAllTokens(); const [token] = await apiTokenService.getAllTokens();
@ -90,7 +91,7 @@ test('should update expiry of token', async () => {
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
expiresAt: time, expiresAt: time,
project: '*', project: '*',
environment: '*', environment: DEFAULT_ENV,
}); });
await apiTokenService.updateExpiry(token.secret, newTime); await apiTokenService.updateExpiry(token.secret, newTime);
@ -109,7 +110,7 @@ test('should only return valid tokens', async () => {
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
expiresAt: new Date('2021-01-01'), expiresAt: new Date('2021-01-01'),
project: '*', project: '*',
environment: '*', environment: DEFAULT_ENV,
}); });
const activeToken = await apiTokenService.createApiToken({ const activeToken = await apiTokenService.createApiToken({
@ -117,7 +118,7 @@ test('should only return valid tokens', async () => {
type: ApiTokenType.CLIENT, type: ApiTokenType.CLIENT,
expiresAt: tomorrow, expiresAt: tomorrow,
project: '*', project: '*',
environment: '*', environment: DEFAULT_ENV,
}); });
const tokens = await apiTokenService.getAllActiveTokens(); const tokens = await apiTokenService.getAllActiveTokens();

View File

@ -38,7 +38,7 @@ test('Can get all', async () => {
}); });
const environments = await service.getAll(); const environments = await service.getAll();
expect(environments).toHaveLength(3); // the one we created plus ':global:' expect(environments).toHaveLength(3); // the one we created plus 'default'
}); });
test('Can connect environment to project', async () => { test('Can connect environment to project', async () => {

View File

@ -4,7 +4,7 @@ import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service
import { IStrategyConfig } from '../../../lib/types/model'; import { IStrategyConfig } from '../../../lib/types/model';
import { createTestConfig } from '../../config/test-config'; import { createTestConfig } from '../../config/test-config';
import dbInit from '../helpers/database-init'; import dbInit from '../helpers/database-init';
import { GLOBAL_ENV } from '../../../lib/types/environment'; import { DEFAULT_ENV } from '../../../lib/util/constants';
let stores; let stores;
let db; let db;
@ -80,7 +80,7 @@ test('Should be able to update existing strategy configuration', async () => {
expect(createdConfig.name).toEqual('default'); expect(createdConfig.name).toEqual('default');
const updatedConfig = await service.updateStrategy( const updatedConfig = await service.updateStrategy(
createdConfig.id, createdConfig.id,
GLOBAL_ENV, DEFAULT_ENV,
projectId, projectId,
username, username,
{ {
@ -112,7 +112,7 @@ test('Should include legacy props in event log when updating strategy configurat
await service.updateEnabled( await service.updateEnabled(
'default', 'default',
featureName, featureName,
GLOBAL_ENV, DEFAULT_ENV,
true, true,
userName, userName,
); );

View File

@ -1 +1,126 @@
{"version":1,"features":[{"name":"in-another-project","description":"","type":"release","project":"someother","enabled":true,"stale":false,"strategies":[{"name":"gradualRolloutRandom","parameters":{"percentage":"29"},"constraints":[{"contextName":"environment","operator":"IN","values":["dev"]},{"contextName":"environment","operator":"IN","values":["prod"]}]}],"variants":[],"createdAt":"2021-09-17T07:14:03.718Z","lastSeenAt":null},{"name":"this-is-fun","description":"","type":"release","project":"default","enabled":true,"stale":false,"strategies":[{"name":"gradualRolloutRandom","parameters":{"percentage":"100"}}],"variants":[],"createdAt":"2021-09-17T07:06:40.925Z","lastSeenAt":null},{"name":"version.three.seventeen","description":"","type":"operational","project":"default","enabled":true,"stale":false,"strategies":[{"name":"default","parameters":{}}],"variants":[],"createdAt":"2021-09-17T07:06:56.421Z","lastSeenAt":null},{"name":"with-constraints","description":"","type":"release","project":"default","enabled":true,"stale":false,"strategies":[{"name":"default","parameters":{},"constraints":[{"contextName":"userId","operator":"IN","values":["123456"]}]}],"variants":[],"createdAt":"2021-09-17T07:14:39.509Z","lastSeenAt":null}],"strategies":[],"projects":[{"id":"default","name":"Default","description":"Default project","createdAt":"2021-09-17T05:06:16.299Z"},{"id":"someother","name":"Some other project","description":"","createdAt":"2021-09-17T05:13:45.011Z"}],"tagTypes":[{"name":"simple","description":"Used to simplify filtering of features","icon":"#"}],"tags":[],"featureTags":[]} {
"version": 1,
"features": [
{
"name": "in-another-project",
"description": "",
"type": "release",
"project": "someother",
"enabled": true,
"stale": false,
"strategies": [
{
"name": "gradualRolloutRandom",
"parameters": {
"percentage": "29"
},
"constraints": [
{
"contextName": "environment",
"operator": "IN",
"values": [
"dev"
]
},
{
"contextName": "environment",
"operator": "IN",
"values": [
"prod"
]
}
]
}
],
"variants": [],
"createdAt": "2021-09-17T07:14:03.718Z",
"lastSeenAt": null
},
{
"name": "this-is-fun",
"description": "",
"type": "release",
"project": "default",
"enabled": true,
"stale": false,
"strategies": [
{
"name": "gradualRolloutRandom",
"parameters": {
"percentage": "100"
}
}
],
"variants": [],
"createdAt": "2021-09-17T07:06:40.925Z",
"lastSeenAt": null
},
{
"name": "version.three.seventeen",
"description": "",
"type": "operational",
"project": "default",
"enabled": true,
"stale": false,
"strategies": [
{
"name": "default",
"parameters": {}
}
],
"variants": [],
"createdAt": "2021-09-17T07:06:56.421Z",
"lastSeenAt": null
},
{
"name": "with-constraints",
"description": "",
"type": "release",
"project": "default",
"enabled": true,
"stale": false,
"strategies": [
{
"name": "default",
"parameters": {},
"constraints": [
{
"contextName": "userId",
"operator": "IN",
"values": [
"123456"
]
}
]
}
],
"variants": [],
"createdAt": "2021-09-17T07:14:39.509Z",
"lastSeenAt": null
}
],
"strategies": [],
"projects": [
{
"id": "default",
"name": "Default",
"description": "Default project",
"createdAt": "2021-09-17T05:06:16.299Z"
},
{
"id": "someother",
"name": "Some other project",
"description": "",
"createdAt": "2021-09-17T05:13:45.011Z"
}
],
"tagTypes": [
{
"name": "simple",
"description": "Used to simplify filtering of features",
"icon": "#"
}
],
"tags": [],
"featureTags": []
}

View File

@ -1 +1,198 @@
{"version":1,"features":[{"name":"another-toggle","description":"","type":"release","project":"someother","enabled":true,"stale":false,"strategies":[{"name":"userWithId","parameters":{"userIds":"12541,123"},"constraints":[]}],"variants":[],"createdAt":"2021-09-17T07:22:16.404Z","lastSeenAt":null},{"name":"in-another-project","description":"","type":"release","project":"someother","enabled":true,"stale":false,"strategies":[{"name":"gradualRolloutRandom","parameters":{"percentage":"29"},"constraints":[{"contextName":"environment","operator":"IN","values":["dev"]},{"contextName":"environment","operator":"IN","values":["prod"]}]}],"variants":[],"createdAt":"2021-09-17T07:14:03.718Z","lastSeenAt":null},{"name":"this-is-fun","description":"","type":"release","project":"default","enabled":true,"stale":false,"strategies":[{"name":"gradualRolloutRandom","parameters":{"percentage":"100"}}],"variants":[],"createdAt":"2021-09-17T07:06:40.925Z","lastSeenAt":null},{"name":"version.three.seventeen","description":"","type":"operational","project":"default","enabled":true,"stale":false,"strategies":[{"name":"default","parameters":{}}],"variants":[],"createdAt":"2021-09-17T07:06:56.421Z","lastSeenAt":null},{"name":"with-constraints","description":"","type":"release","project":"default","enabled":true,"stale":false,"strategies":[{"name":"default","parameters":{},"constraints":[{"contextName":"userId","operator":"IN","values":["123456"]}]}],"variants":[],"createdAt":"2021-09-17T07:14:39.509Z","lastSeenAt":null}],"strategies":[{"name":"gradualRolloutRandom","description":"Randomly activate the feature toggle. No stickiness.","parameters":[{"name":"percentage","type":"percentage","description":"","required":false}],"deprecated":true},{"name":"gradualRolloutSessionId","description":"Gradually activate feature toggle. Stickiness based on session id.","parameters":[{"name":"percentage","type":"percentage","description":"","required":false},{"name":"groupId","type":"string","description":"Used to define a activation groups, which allows you to correlate across feature toggles.","required":true}],"deprecated":true},{"name":"gradualRolloutUserId","description":"Gradually activate feature toggle for logged in users. Stickiness based on user id.","parameters":[{"name":"percentage","type":"percentage","description":"","required":false},{"name":"groupId","type":"string","description":"Used to define a activation groups, which allows you to correlate across feature toggles.","required":true}],"deprecated":true}],"projects":[{"id":"default","name":"Default","description":"Default project","createdAt":"2021-09-17T05:06:16.299Z"},{"id":"someother","name":"Some other project","description":"","createdAt":"2021-09-17T05:13:45.011Z"}],"tagTypes":[{"name":"simple","description":"Used to simplify filtering of features","icon":"#"}],"tags":[],"featureTags":[]} {
"version": 1,
"features": [
{
"name": "another-toggle",
"description": "",
"type": "release",
"project": "someother",
"enabled": true,
"stale": false,
"strategies": [
{
"name": "userWithId",
"parameters": {
"userIds": "12541,123"
},
"constraints": []
}
],
"variants": [],
"createdAt": "2021-09-17T07:22:16.404Z",
"lastSeenAt": null
},
{
"name": "in-another-project",
"description": "",
"type": "release",
"project": "someother",
"enabled": true,
"stale": false,
"strategies": [
{
"name": "gradualRolloutRandom",
"parameters": {
"percentage": "29"
},
"constraints": [
{
"contextName": "environment",
"operator": "IN",
"values": [
"dev"
]
},
{
"contextName": "environment",
"operator": "IN",
"values": [
"prod"
]
}
]
}
],
"variants": [],
"createdAt": "2021-09-17T07:14:03.718Z",
"lastSeenAt": null
},
{
"name": "this-is-fun",
"description": "",
"type": "release",
"project": "default",
"enabled": true,
"stale": false,
"strategies": [
{
"name": "gradualRolloutRandom",
"parameters": {
"percentage": "100"
}
}
],
"variants": [],
"createdAt": "2021-09-17T07:06:40.925Z",
"lastSeenAt": null
},
{
"name": "version.three.seventeen",
"description": "",
"type": "operational",
"project": "default",
"enabled": true,
"stale": false,
"strategies": [
{
"name": "default",
"parameters": {}
}
],
"variants": [],
"createdAt": "2021-09-17T07:06:56.421Z",
"lastSeenAt": null
},
{
"name": "with-constraints",
"description": "",
"type": "release",
"project": "default",
"enabled": true,
"stale": false,
"strategies": [
{
"name": "default",
"parameters": {},
"constraints": [
{
"contextName": "userId",
"operator": "IN",
"values": [
"123456"
]
}
]
}
],
"variants": [],
"createdAt": "2021-09-17T07:14:39.509Z",
"lastSeenAt": null
}
],
"strategies": [
{
"name": "gradualRolloutRandom",
"description": "Randomly activate the feature toggle. No stickiness.",
"parameters": [
{
"name": "percentage",
"type": "percentage",
"description": "",
"required": false
}
],
"deprecated": true
},
{
"name": "gradualRolloutSessionId",
"description": "Gradually activate feature toggle. Stickiness based on session id.",
"parameters": [
{
"name": "percentage",
"type": "percentage",
"description": "",
"required": false
},
{
"name": "groupId",
"type": "string",
"description": "Used to define a activation groups, which allows you to correlate across feature toggles.",
"required": true
}
],
"deprecated": true
},
{
"name": "gradualRolloutUserId",
"description": "Gradually activate feature toggle for logged in users. Stickiness based on user id.",
"parameters": [
{
"name": "percentage",
"type": "percentage",
"description": "",
"required": false
},
{
"name": "groupId",
"type": "string",
"description": "Used to define a activation groups, which allows you to correlate across feature toggles.",
"required": true
}
],
"deprecated": true
}
],
"projects": [
{
"id": "default",
"name": "Default",
"description": "Default project",
"createdAt": "2021-09-17T05:06:16.299Z"
},
{
"id": "someother",
"name": "Some other project",
"description": "",
"createdAt": "2021-09-17T05:13:45.011Z"
}
],
"tagTypes": [
{
"name": "simple",
"description": "Used to simplify filtering of features",
"icon": "#"
}
],
"tags": [],
"featureTags": []
}

View File

@ -145,7 +145,7 @@
"id": "2ea91298-4565-4db2-8a23-50757001a076", "id": "2ea91298-4565-4db2-8a23-50757001a076",
"featureName": "this-is-fun", "featureName": "this-is-fun",
"projectId": "default", "projectId": "default",
"environment": ":global:", "environment": "default",
"strategyName": "gradualRolloutRandom", "strategyName": "gradualRolloutRandom",
"parameters": { "parameters": {
"percentage": "100" "percentage": "100"
@ -157,7 +157,7 @@
"id": "edaffaee-cf6e-473f-b137-ae15fb88ff53", "id": "edaffaee-cf6e-473f-b137-ae15fb88ff53",
"featureName": "version.three.seventeen", "featureName": "version.three.seventeen",
"projectId": "default", "projectId": "default",
"environment": ":global:", "environment": "default",
"strategyName": "default", "strategyName": "default",
"parameters": {}, "parameters": {},
"constraints": [], "constraints": [],
@ -167,7 +167,7 @@
"id": "e6eaede4-027a-41a9-8e80-0e0fc0a5d7af", "id": "e6eaede4-027a-41a9-8e80-0e0fc0a5d7af",
"featureName": "in-another-project", "featureName": "in-another-project",
"projectId": "someother", "projectId": "someother",
"environment": ":global:", "environment": "default",
"strategyName": "gradualRolloutRandom", "strategyName": "gradualRolloutRandom",
"parameters": { "parameters": {
"percentage": "29" "percentage": "29"
@ -194,7 +194,7 @@
"id": "da60e934-246c-4b3e-b314-f2fd1828dd51", "id": "da60e934-246c-4b3e-b314-f2fd1828dd51",
"featureName": "with-constraints", "featureName": "with-constraints",
"projectId": "default", "projectId": "default",
"environment": ":global:", "environment": "default",
"strategyName": "default", "strategyName": "default",
"parameters": {}, "parameters": {},
"constraints": [ "constraints": [
@ -212,7 +212,7 @@
"id": "162058f5-3600-4299-97df-d543a0301bdd", "id": "162058f5-3600-4299-97df-d543a0301bdd",
"featureName": "another-toggle", "featureName": "another-toggle",
"projectId": "someother", "projectId": "someother",
"environment": ":global:", "environment": "default",
"strategyName": "userWithId", "strategyName": "userWithId",
"parameters": { "parameters": {
"userIds": "12541,123" "userIds": "12541,123"
@ -224,7 +224,7 @@
"id": "5630e0fb-ebc1-4313-b6df-06b0a563c7b4", "id": "5630e0fb-ebc1-4313-b6df-06b0a563c7b4",
"featureName": "toggle-created-in-4-1", "featureName": "toggle-created-in-4-1",
"projectId": "default", "projectId": "default",
"environment": ":global:", "environment": "default",
"strategyName": "applicationHostname", "strategyName": "applicationHostname",
"parameters": { "parameters": {
"hostNames": "vg.no" "hostNames": "vg.no"
@ -235,7 +235,7 @@
], ],
"environments": [ "environments": [
{ {
"name": ":global:", "name": "default",
"displayName": "Across all environments", "displayName": "Across all environments",
"type": "production" "type": "production"
} }
@ -244,32 +244,32 @@
{ {
"enabled": true, "enabled": true,
"featureName": "this-is-fun", "featureName": "this-is-fun",
"environment": ":global:" "environment": "default"
}, },
{ {
"enabled": true, "enabled": true,
"featureName": "version.three.seventeen", "featureName": "version.three.seventeen",
"environment": ":global:" "environment": "default"
}, },
{ {
"enabled": true, "enabled": true,
"featureName": "in-another-project", "featureName": "in-another-project",
"environment": ":global:" "environment": "default"
}, },
{ {
"enabled": true, "enabled": true,
"featureName": "with-constraints", "featureName": "with-constraints",
"environment": ":global:" "environment": "default"
}, },
{ {
"enabled": true, "enabled": true,
"featureName": "another-toggle", "featureName": "another-toggle",
"environment": ":global:" "environment": "default"
}, },
{ {
"enabled": true, "enabled": true,
"featureName": "toggle-created-in-4-1", "featureName": "toggle-created-in-4-1",
"environment": ":global:" "environment": "default"
} }
] ]
} }

View File

@ -1,4 +1,4 @@
exported412-enterprise.json{ {
"version": 2, "version": 2,
"features": [ "features": [
{ {

View File

@ -0,0 +1,275 @@
{
"version": 2,
"features": [
{
"name": "this-is-fun",
"description": "",
"type": "release",
"project": "default",
"stale": false,
"variants": [],
"createdAt": "2021-09-17T07:06:40.925Z",
"lastSeenAt": null
},
{
"name": "version.three.seventeen",
"description": "",
"type": "operational",
"project": "default",
"stale": false,
"variants": [],
"createdAt": "2021-09-17T07:06:56.421Z",
"lastSeenAt": null
},
{
"name": "in-another-project",
"description": "",
"type": "release",
"project": "someother",
"stale": false,
"variants": [],
"createdAt": "2021-09-17T07:14:03.718Z",
"lastSeenAt": null
},
{
"name": "with-constraints",
"description": "",
"type": "release",
"project": "default",
"stale": false,
"variants": [],
"createdAt": "2021-09-17T07:14:39.509Z",
"lastSeenAt": null
},
{
"name": "another-toggle",
"description": "",
"type": "release",
"project": "someother",
"stale": false,
"variants": [],
"createdAt": "2021-09-17T07:22:16.404Z",
"lastSeenAt": null
},
{
"name": "toggle-created-in-4-1",
"description": "",
"type": "operational",
"project": "default",
"stale": false,
"variants": [],
"createdAt": "2021-09-17T07:24:13.897Z",
"lastSeenAt": null
}
],
"strategies": [
{
"name": "gradualRolloutRandom",
"description": "Randomly activate the feature toggle. No stickiness.",
"parameters": [
{
"name": "percentage",
"type": "percentage",
"description": "",
"required": false
}
],
"deprecated": true
},
{
"name": "gradualRolloutSessionId",
"description": "Gradually activate feature toggle. Stickiness based on session id.",
"parameters": [
{
"name": "percentage",
"type": "percentage",
"description": "",
"required": false
},
{
"name": "groupId",
"type": "string",
"description": "Used to define a activation groups, which allows you to correlate across feature toggles.",
"required": true
}
],
"deprecated": true
},
{
"name": "gradualRolloutUserId",
"description": "Gradually activate feature toggle for logged in users. Stickiness based on user id.",
"parameters": [
{
"name": "percentage",
"type": "percentage",
"description": "",
"required": false
},
{
"name": "groupId",
"type": "string",
"description": "Used to define a activation groups, which allows you to correlate across feature toggles.",
"required": true
}
],
"deprecated": true
}
],
"projects": [
{
"id": "default",
"name": "Default",
"description": "Default project",
"createdAt": "2021-09-17T05:06:16.299Z",
"health": 100
},
{
"id": "someother",
"name": "Some other project",
"description": "",
"createdAt": "2021-09-17T05:13:45.011Z",
"health": 100
}
],
"tagTypes": [
{
"name": "simple",
"description": "Used to simplify filtering of features",
"icon": "#"
}
],
"tags": [],
"featureTags": [],
"featureStrategies": [
{
"id": "2ea91298-4565-4db2-8a23-50757001a076",
"featureName": "this-is-fun",
"projectId": "default",
"environment": ":global:",
"strategyName": "gradualRolloutRandom",
"parameters": {
"percentage": "100"
},
"constraints": [],
"createdAt": "2021-09-17T07:23:39.374Z"
},
{
"id": "edaffaee-cf6e-473f-b137-ae15fb88ff53",
"featureName": "version.three.seventeen",
"projectId": "default",
"environment": ":global:",
"strategyName": "default",
"parameters": {},
"constraints": [],
"createdAt": "2021-09-17T07:23:39.374Z"
},
{
"id": "e6eaede4-027a-41a9-8e80-0e0fc0a5d7af",
"featureName": "in-another-project",
"projectId": "someother",
"environment": ":global:",
"strategyName": "gradualRolloutRandom",
"parameters": {
"percentage": "29"
},
"constraints": [
{
"values": [
"dev"
],
"operator": "IN",
"contextName": "environment"
},
{
"values": [
"prod"
],
"operator": "IN",
"contextName": "environment"
}
],
"createdAt": "2021-09-17T07:23:39.374Z"
},
{
"id": "da60e934-246c-4b3e-b314-f2fd1828dd51",
"featureName": "with-constraints",
"projectId": "default",
"environment": ":global:",
"strategyName": "default",
"parameters": {},
"constraints": [
{
"values": [
"123456"
],
"operator": "IN",
"contextName": "userId"
}
],
"createdAt": "2021-09-17T07:23:39.374Z"
},
{
"id": "162058f5-3600-4299-97df-d543a0301bdd",
"featureName": "another-toggle",
"projectId": "someother",
"environment": ":global:",
"strategyName": "userWithId",
"parameters": {
"userIds": "12541,123"
},
"constraints": [],
"createdAt": "2021-09-17T07:23:39.374Z"
},
{
"id": "5630e0fb-ebc1-4313-b6df-06b0a563c7b4",
"featureName": "toggle-created-in-4-1",
"projectId": "default",
"environment": ":global:",
"strategyName": "applicationHostname",
"parameters": {
"hostNames": "vg.no"
},
"constraints": [],
"createdAt": "2021-09-17T07:24:13.904Z"
}
],
"environments": [
{
"name": ":global:",
"displayName": "Across all environments",
"type": "production"
}
],
"featureEnvironments": [
{
"enabled": true,
"featureName": "this-is-fun",
"environment": ":global:"
},
{
"enabled": true,
"featureName": "version.three.seventeen",
"environment": ":global:"
},
{
"enabled": true,
"featureName": "in-another-project",
"environment": ":global:"
},
{
"enabled": true,
"featureName": "with-constraints",
"environment": ":global:"
},
{
"enabled": true,
"featureName": "another-toggle",
"environment": ":global:"
},
{
"enabled": true,
"featureName": "toggle-created-in-4-1",
"environment": ":global:"
}
]
}

View File

@ -3,6 +3,11 @@ import NotFoundError from '../../lib/error/notfound-error';
import { IEnvironmentStore } from '../../lib/types/stores/environment-store'; import { IEnvironmentStore } from '../../lib/types/stores/environment-store';
export default class FakeEnvironmentStore implements IEnvironmentStore { export default class FakeEnvironmentStore implements IEnvironmentStore {
importEnvironments(envs: IEnvironment[]): Promise<IEnvironment[]> {
this.environments = envs;
return Promise.resolve(envs);
}
environments: IEnvironment[] = []; environments: IEnvironment[] = [];
async getAll(): Promise<IEnvironment[]> { async getAll(): Promise<IEnvironment[]> {

View File

@ -13,7 +13,7 @@ In this document we will guide you on how you can work with feature toggles and
- A feature toggle can take different configuration, activation strategies, per environment. - A feature toggle can take different configuration, activation strategies, per environment.
TODO: Need to explain the following in a bit more details: TODO: Need to explain the following in a bit more details:
- The _:global:: environment - The _default_ environment
> We will in this guide use [HTTPie](https://httpie.io) commands to show examples on how to interact with the API. > We will in this guide use [HTTPie](https://httpie.io) commands to show examples on how to interact with the API.