mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
Feat: project default strategy (#3644)
<!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ --> Adds default strategy to project environment link table ## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> <!-- Does it close an issue? Multiple? --> Closes # [1-876](https://linear.app/unleash/issue/1-876/default-strategy-backend) <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> <!-- Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: # --> ### Important files <!-- PRs can contain a lot of changes, but not all changes are equally important. Where should a reviewer start looking to get an overview of the changes? Are any files particularly important? --> ## Discussion points <!-- Anything about the PR you'd like to discuss before it gets merged? Got any questions or doubts? --> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
e378793de5
commit
1ccbbfeb57
@ -48,7 +48,10 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||
import { useEnvironmentsRef } from './hooks/useEnvironmentsRef';
|
||||
import {
|
||||
ProjectEnvironmentType,
|
||||
useEnvironmentsRef,
|
||||
} from './hooks/useEnvironmentsRef';
|
||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import { FeatureToggleSwitch } from './FeatureToggleSwitch/FeatureToggleSwitch';
|
||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
||||
@ -321,46 +324,53 @@ export const ProjectFeatureToggles = ({
|
||||
sortType: 'date',
|
||||
minWidth: 120,
|
||||
},
|
||||
...environments.map((name: string) => ({
|
||||
Header: loading ? () => '' : name,
|
||||
maxWidth: 90,
|
||||
id: `environments.${name}`,
|
||||
accessor: (row: ListItemType) =>
|
||||
row.environments[name]?.enabled,
|
||||
align: 'center',
|
||||
Cell: ({
|
||||
value,
|
||||
row: { original: feature },
|
||||
}: {
|
||||
value: boolean;
|
||||
row: { original: ListItemType };
|
||||
}) => {
|
||||
const hasWarning =
|
||||
feature.someEnabledEnvironmentHasVariants &&
|
||||
feature.environments[name].variantCount === 0 &&
|
||||
feature.environments[name].enabled;
|
||||
...environments.map((value: ProjectEnvironmentType | string) => {
|
||||
const name =
|
||||
typeof value === 'string'
|
||||
? value
|
||||
: (value as ProjectEnvironmentType).environment;
|
||||
return {
|
||||
Header: loading ? () => '' : name,
|
||||
maxWidth: 90,
|
||||
id: `environments.${name}`,
|
||||
accessor: (row: ListItemType) =>
|
||||
row.environments[name]?.enabled,
|
||||
align: 'center',
|
||||
Cell: ({
|
||||
value,
|
||||
row: { original: feature },
|
||||
}: {
|
||||
value: boolean;
|
||||
row: { original: ListItemType };
|
||||
}) => {
|
||||
const hasWarning =
|
||||
feature.someEnabledEnvironmentHasVariants &&
|
||||
feature.environments[name].variantCount === 0 &&
|
||||
feature.environments[name].enabled;
|
||||
|
||||
return (
|
||||
<StyledSwitchContainer hasWarning={hasWarning}>
|
||||
<FeatureToggleSwitch
|
||||
value={value}
|
||||
projectId={projectId}
|
||||
featureName={feature?.name}
|
||||
environmentName={name}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasWarning}
|
||||
show={<VariantsWarningTooltip />}
|
||||
/>
|
||||
</StyledSwitchContainer>
|
||||
);
|
||||
},
|
||||
sortType: 'boolean',
|
||||
filterName: name,
|
||||
filterParsing: (value: boolean) =>
|
||||
value ? 'enabled' : 'disabled',
|
||||
};
|
||||
}),
|
||||
|
||||
return (
|
||||
<StyledSwitchContainer hasWarning={hasWarning}>
|
||||
<FeatureToggleSwitch
|
||||
value={value}
|
||||
projectId={projectId}
|
||||
featureName={feature?.name}
|
||||
environmentName={name}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={hasWarning}
|
||||
show={<VariantsWarningTooltip />}
|
||||
/>
|
||||
</StyledSwitchContainer>
|
||||
);
|
||||
},
|
||||
sortType: 'boolean',
|
||||
filterName: name,
|
||||
filterParsing: (value: boolean) =>
|
||||
value ? 'enabled' : 'disabled',
|
||||
})),
|
||||
{
|
||||
id: 'Actions',
|
||||
maxWidth: 56,
|
||||
@ -477,7 +487,6 @@ export const ProjectFeatureToggles = ({
|
||||
);
|
||||
|
||||
const getRowId = useCallback((row: any) => row.name, []);
|
||||
|
||||
const {
|
||||
allColumns,
|
||||
headerGroups,
|
||||
|
@ -1,14 +1,32 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { CreateFeatureStrategySchema } from 'openapi';
|
||||
/**
|
||||
* Don't revalidate if array content didn't change.
|
||||
* Needed for `columns` memo optimization.
|
||||
*/
|
||||
export const useEnvironmentsRef = (environments: string[] = []) => {
|
||||
const ref = useRef<string[]>(environments);
|
||||
|
||||
if (environments?.join('') !== ref.current?.join('')) {
|
||||
ref.current = environments;
|
||||
export type ProjectEnvironmentType = {
|
||||
environment: string;
|
||||
defaultStrategy: CreateFeatureStrategySchema | null;
|
||||
};
|
||||
export const useEnvironmentsRef = (
|
||||
environments: Array<string | ProjectEnvironmentType> = []
|
||||
): string[] => {
|
||||
let names: string[];
|
||||
if (
|
||||
environments &&
|
||||
environments.length > 0 &&
|
||||
typeof environments[0] !== 'string'
|
||||
) {
|
||||
names = environments.map(
|
||||
env => (env as ProjectEnvironmentType).environment
|
||||
);
|
||||
} else {
|
||||
names = environments as string[];
|
||||
}
|
||||
const ref = useRef<Array<string>>(names);
|
||||
if (names.join('') !== ref.current?.join('')) {
|
||||
ref.current = names;
|
||||
}
|
||||
|
||||
return ref.current;
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
IProjectSettings,
|
||||
IProjectSettingsRow,
|
||||
IProjectStore,
|
||||
ProjectEnvironment,
|
||||
} from '../types/stores/project-store';
|
||||
import { DEFAULT_ENV } from '../util';
|
||||
import metricsHelper from '../util/metrics-helper';
|
||||
@ -23,6 +24,7 @@ import { DB_TIME } from '../metric-events';
|
||||
import EventEmitter from 'events';
|
||||
import { Db } from './db';
|
||||
import Raw = Knex.Raw;
|
||||
import { CreateFeatureStrategySchema } from '../openapi';
|
||||
|
||||
const COLUMNS = [
|
||||
'id',
|
||||
@ -35,6 +37,7 @@ const COLUMNS = [
|
||||
const TABLE = 'projects';
|
||||
const SETTINGS_COLUMNS = ['project_mode', 'default_stickiness'];
|
||||
const SETTINGS_TABLE = 'project_settings';
|
||||
const PROJECT_ENVIRONMENTS = 'project_environments';
|
||||
|
||||
export interface IEnvironmentProjectLink {
|
||||
environmentName: string;
|
||||
@ -350,8 +353,8 @@ class ProjectStore implements IProjectStore {
|
||||
.ignore();
|
||||
}
|
||||
|
||||
async getEnvironmentsForProject(id: string): Promise<string[]> {
|
||||
return this.db('project_environments')
|
||||
async getEnvironmentsForProject(id: string): Promise<ProjectEnvironment[]> {
|
||||
const rows = await this.db(PROJECT_ENVIRONMENTS)
|
||||
.where({
|
||||
project_id: id,
|
||||
})
|
||||
@ -362,7 +365,12 @@ class ProjectStore implements IProjectStore {
|
||||
)
|
||||
.orderBy('environments.sort_order', 'asc')
|
||||
.orderBy('project_environments.environment_name', 'asc')
|
||||
.pluck('project_environments.environment_name');
|
||||
.returning([
|
||||
'project_environments.environment_name',
|
||||
'project_environments.default_strategy',
|
||||
]);
|
||||
|
||||
return rows.map(this.mapProjectEnvironmentRow);
|
||||
}
|
||||
|
||||
async getMembersCount(): Promise<IProjectMembersCount[]> {
|
||||
@ -495,6 +503,32 @@ class ProjectStore implements IProjectStore {
|
||||
.where({ project: projectId });
|
||||
}
|
||||
|
||||
async getDefaultStrategy(
|
||||
projectId: string,
|
||||
environment: string,
|
||||
): Promise<CreateFeatureStrategySchema | null> {
|
||||
const rows = await this.db(PROJECT_ENVIRONMENTS)
|
||||
.select('default_strategy')
|
||||
.where({ project_id: projectId, environment_name: environment });
|
||||
|
||||
return rows.length > 0 ? rows[0].default_strategy : null;
|
||||
}
|
||||
|
||||
async updateDefaultStrategy(
|
||||
projectId: string,
|
||||
environment: string,
|
||||
strategy: CreateFeatureStrategySchema,
|
||||
): Promise<CreateFeatureStrategySchema> {
|
||||
const rows = await this.db(PROJECT_ENVIRONMENTS)
|
||||
.update({
|
||||
default_strategy: strategy,
|
||||
})
|
||||
.where({ project_id: projectId, environment_name: environment })
|
||||
.returning('default_strategy');
|
||||
|
||||
return rows[0].default_strategy;
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
return this.db
|
||||
.from(TABLE)
|
||||
@ -534,6 +568,19 @@ class ProjectStore implements IProjectStore {
|
||||
defaultStickiness: row.default_stickiness || 'default',
|
||||
};
|
||||
}
|
||||
|
||||
mapProjectEnvironmentRow(row: {
|
||||
environment_name: string;
|
||||
default_strategy: CreateFeatureStrategySchema;
|
||||
}): ProjectEnvironment {
|
||||
return {
|
||||
environment: row.environment_name,
|
||||
defaultStrategy:
|
||||
row.default_strategy === null
|
||||
? undefined
|
||||
: row.default_strategy,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ProjectStore;
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import {
|
||||
adminFeaturesQuerySchema,
|
||||
addonCreateUpdateSchema,
|
||||
addonParameterSchema,
|
||||
addonSchema,
|
||||
addonCreateUpdateSchema,
|
||||
addonsSchema,
|
||||
addonTypeSchema,
|
||||
adminFeaturesQuerySchema,
|
||||
apiTokenSchema,
|
||||
apiTokensSchema,
|
||||
applicationSchema,
|
||||
@ -35,8 +35,8 @@ import {
|
||||
environmentsSchema,
|
||||
eventSchema,
|
||||
eventsSchema,
|
||||
exportResultSchema,
|
||||
exportQuerySchema,
|
||||
exportResultSchema,
|
||||
featureEnvironmentMetricsSchema,
|
||||
featureEnvironmentSchema,
|
||||
featureEventsSchema,
|
||||
@ -58,6 +58,9 @@ import {
|
||||
healthOverviewSchema,
|
||||
healthReportSchema,
|
||||
idSchema,
|
||||
importTogglesSchema,
|
||||
importTogglesValidateItemSchema,
|
||||
importTogglesValidateSchema,
|
||||
instanceAdminStatsSchema,
|
||||
legalValueSchema,
|
||||
loginSchema,
|
||||
@ -79,20 +82,21 @@ import {
|
||||
playgroundStrategySchema,
|
||||
profileSchema,
|
||||
projectEnvironmentSchema,
|
||||
projectOverviewSchema,
|
||||
projectSchema,
|
||||
projectsSchema,
|
||||
projectStatsSchema,
|
||||
proxyClientSchema,
|
||||
proxyFeatureSchema,
|
||||
proxyFeaturesSchema,
|
||||
publicSignupTokenCreateSchema,
|
||||
projectStatsSchema,
|
||||
publicSignupTokenSchema,
|
||||
publicSignupTokensSchema,
|
||||
publicSignupTokenUpdateSchema,
|
||||
pushVariantsSchema,
|
||||
resetPasswordSchema,
|
||||
requestsPerSecondSchema,
|
||||
requestsPerSecondSegmentedSchema,
|
||||
resetPasswordSchema,
|
||||
roleSchema,
|
||||
sdkContextSchema,
|
||||
searchEventsSchema,
|
||||
@ -131,10 +135,6 @@ import {
|
||||
variantSchema,
|
||||
variantsSchema,
|
||||
versionSchema,
|
||||
projectOverviewSchema,
|
||||
importTogglesSchema,
|
||||
importTogglesValidateSchema,
|
||||
importTogglesValidateItemSchema,
|
||||
} from './spec';
|
||||
import { IServerOption } from '../types';
|
||||
import { mapValues, omitKeys } from '../util';
|
||||
|
@ -8,6 +8,8 @@ import { constraintSchema } from './constraint-schema';
|
||||
import { environmentSchema } from './environment-schema';
|
||||
import { featureEnvironmentSchema } from './feature-environment-schema';
|
||||
import { projectStatsSchema } from './project-stats-schema';
|
||||
import { createFeatureStrategySchema } from './create-feature-strategy-schema';
|
||||
import { projectEnvironmentSchema } from './project-environment-schema';
|
||||
|
||||
export const healthOverviewSchema = {
|
||||
$id: '#/components/schemas/healthOverviewSchema',
|
||||
@ -47,7 +49,7 @@ export const healthOverviewSchema = {
|
||||
environments: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
$ref: '#/components/schemas/projectEnvironmentSchema',
|
||||
},
|
||||
},
|
||||
features: {
|
||||
@ -71,8 +73,10 @@ export const healthOverviewSchema = {
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
constraintSchema,
|
||||
environmentSchema,
|
||||
projectEnvironmentSchema,
|
||||
createFeatureStrategySchema,
|
||||
constraintSchema,
|
||||
featureSchema,
|
||||
featureEnvironmentSchema,
|
||||
overrideSchema,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { createFeatureStrategySchema } from './create-feature-strategy-schema';
|
||||
|
||||
export const projectEnvironmentSchema = {
|
||||
$id: '#/components/schemas/projectEnvironmentSchema',
|
||||
@ -12,8 +13,15 @@ export const projectEnvironmentSchema = {
|
||||
changeRequestsEnabled: {
|
||||
type: 'boolean',
|
||||
},
|
||||
defaultStrategy: {
|
||||
$ref: '#/components/schemas/createFeatureStrategySchema',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
createFeatureStrategySchema,
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type ProjectEnvironmentSchema = FromSchema<
|
||||
|
@ -8,6 +8,8 @@ import { constraintSchema } from './constraint-schema';
|
||||
import { environmentSchema } from './environment-schema';
|
||||
import { featureEnvironmentSchema } from './feature-environment-schema';
|
||||
import { projectStatsSchema } from './project-stats-schema';
|
||||
import { createFeatureStrategySchema } from './create-feature-strategy-schema';
|
||||
import { projectEnvironmentSchema } from './project-environment-schema';
|
||||
|
||||
export const projectOverviewSchema = {
|
||||
$id: '#/components/schemas/projectOverviewSchema',
|
||||
@ -63,9 +65,23 @@ export const projectOverviewSchema = {
|
||||
environments: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
$ref: '#/components/schemas/projectEnvironmentSchema',
|
||||
},
|
||||
example: ['development', 'production'],
|
||||
example: [
|
||||
{ environment: 'development' },
|
||||
{
|
||||
environment: 'production',
|
||||
defaultStrategy: {
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
parameters: {
|
||||
rollout: '50',
|
||||
stickiness: 'customAppName',
|
||||
groupId: 'stickytoggle',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
description: 'The environments that are enabled for this project',
|
||||
},
|
||||
features: {
|
||||
@ -91,8 +107,10 @@ export const projectOverviewSchema = {
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
constraintSchema,
|
||||
environmentSchema,
|
||||
projectEnvironmentSchema,
|
||||
createFeatureStrategySchema,
|
||||
constraintSchema,
|
||||
featureSchema,
|
||||
featureEnvironmentSchema,
|
||||
overrideSchema,
|
||||
|
@ -1,13 +1,23 @@
|
||||
import { Request, Response } from 'express';
|
||||
import Controller from '../../controller';
|
||||
import { IUnleashConfig } from '../../../types/option';
|
||||
import { IUnleashServices } from '../../../types/services';
|
||||
import {
|
||||
IUnleashConfig,
|
||||
IUnleashServices,
|
||||
serializeDates,
|
||||
UPDATE_PROJECT,
|
||||
} from '../../../types';
|
||||
import { Logger } from '../../../logger';
|
||||
import EnvironmentService from '../../../services/environment-service';
|
||||
import { UPDATE_PROJECT } from '../../../types/permissions';
|
||||
import { createRequestSchema } from '../../../openapi/util/create-request-schema';
|
||||
import { ProjectEnvironmentSchema } from '../../../openapi/spec/project-environment-schema';
|
||||
import { emptyResponse } from '../../../openapi/util/standard-responses';
|
||||
import {
|
||||
createFeatureStrategySchema,
|
||||
CreateFeatureStrategySchema,
|
||||
createRequestSchema,
|
||||
createResponseSchema,
|
||||
emptyResponse,
|
||||
getStandardResponses,
|
||||
ProjectEnvironmentSchema,
|
||||
} from '../../../openapi';
|
||||
import { OpenApiService } from '../../../services';
|
||||
|
||||
const PREFIX = '/:projectId/environments';
|
||||
|
||||
@ -21,6 +31,8 @@ export default class EnvironmentsController extends Controller {
|
||||
|
||||
private environmentService: EnvironmentService;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
@ -32,6 +44,7 @@ export default class EnvironmentsController extends Controller {
|
||||
|
||||
this.logger = config.getLogger('admin-api/project/environments.ts');
|
||||
this.environmentService = environmentService;
|
||||
this.openApiService = openApiService;
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
@ -64,6 +77,30 @@ export default class EnvironmentsController extends Controller {
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: `${PREFIX}/:environment/default-strategy`,
|
||||
handler: this.addDefaultStrategyToProjectEnvironment,
|
||||
permission: UPDATE_PROJECT,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Projects'],
|
||||
operationId: 'addDefaultStrategyToProjectEnvironment',
|
||||
description:
|
||||
'Adds a default strategy for this environment. Unleash will use this strategy by default when enabling a toggle. Use the wild card "*" for `:environment` to add to all environments. ',
|
||||
requestBody: createRequestSchema(
|
||||
'createFeatureStrategySchema',
|
||||
),
|
||||
responses: {
|
||||
200: createResponseSchema(
|
||||
'createFeatureStrategySchema',
|
||||
),
|
||||
...getStandardResponses(400),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async addEnvironmentToProject(
|
||||
@ -98,4 +135,25 @@ export default class EnvironmentsController extends Controller {
|
||||
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async addDefaultStrategyToProjectEnvironment(
|
||||
req: Request<IProjectEnvironmentParams, CreateFeatureStrategySchema>,
|
||||
res: Response<CreateFeatureStrategySchema>,
|
||||
): Promise<void> {
|
||||
const { projectId, environment } = req.params;
|
||||
const strategy = req.body;
|
||||
|
||||
const saved = await this.environmentService.addDefaultStrategy(
|
||||
environment,
|
||||
projectId,
|
||||
strategy,
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
createFeatureStrategySchema.$id,
|
||||
serializeDates(saved),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -110,6 +110,7 @@ export default class ProjectApi extends Controller {
|
||||
archived,
|
||||
user.id,
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
|
@ -611,11 +611,12 @@ export default class ProjectFeaturesController extends Controller {
|
||||
res: Response<FeatureEnvironmentSchema>,
|
||||
): Promise<void> {
|
||||
const { environment, featureName, projectId } = req.params;
|
||||
const environmentInfo = await this.featureService.getEnvironmentInfo(
|
||||
projectId,
|
||||
environment,
|
||||
featureName,
|
||||
);
|
||||
const { defaultStrategy, ...environmentInfo } =
|
||||
await this.featureService.getEnvironmentInfo(
|
||||
projectId,
|
||||
environment,
|
||||
featureName,
|
||||
);
|
||||
|
||||
const result = {
|
||||
...environmentInfo,
|
||||
|
@ -95,7 +95,10 @@ async function createApp(
|
||||
});
|
||||
}
|
||||
|
||||
if (config.environmentEnableOverrides?.length > 0) {
|
||||
if (
|
||||
config.environmentEnableOverrides &&
|
||||
config.environmentEnableOverrides?.length > 0
|
||||
) {
|
||||
await services.environmentService.overrideEnabledProjects(
|
||||
config.environmentEnableOverrides,
|
||||
);
|
||||
|
@ -1,17 +1,22 @@
|
||||
import { IUnleashStores } from '../types/stores';
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
import {
|
||||
IEnvironment,
|
||||
IEnvironmentStore,
|
||||
IFeatureEnvironmentStore,
|
||||
IFeatureStrategiesStore,
|
||||
IProjectEnvironment,
|
||||
ISortOrder,
|
||||
IUnleashConfig,
|
||||
IUnleashStores,
|
||||
} from '../types';
|
||||
import { Logger } from '../logger';
|
||||
import { IEnvironment, IProjectEnvironment, ISortOrder } from '../types/model';
|
||||
import { UNIQUE_CONSTRAINT_VIOLATION } from '../error/db-error';
|
||||
import { BadDataError, UNIQUE_CONSTRAINT_VIOLATION } from '../error';
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
import { sortOrderSchema } from './state-schema';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import { IEnvironmentStore } from '../types/stores/environment-store';
|
||||
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
||||
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
||||
import { IProjectStore } from 'lib/types/stores/project-store';
|
||||
import MinimumOneEnvironmentError from '../error/minimum-one-environment-error';
|
||||
import { IFlagResolver } from 'lib/types/experimental';
|
||||
import { CreateFeatureStrategySchema } from '../openapi';
|
||||
|
||||
export default class EnvironmentService {
|
||||
private logger: Logger;
|
||||
@ -107,6 +112,23 @@ export default class EnvironmentService {
|
||||
}
|
||||
}
|
||||
|
||||
async addDefaultStrategy(
|
||||
environment: string,
|
||||
projectId: string,
|
||||
strategy: CreateFeatureStrategySchema,
|
||||
): Promise<CreateFeatureStrategySchema> {
|
||||
if (strategy.name !== 'flexibleRollout') {
|
||||
throw new BadDataError(
|
||||
'Only "flexibleRollout" strategy can be used as a default strategy for an environment',
|
||||
);
|
||||
}
|
||||
return this.projectStore.updateDefaultStrategy(
|
||||
projectId,
|
||||
environment,
|
||||
strategy,
|
||||
);
|
||||
}
|
||||
|
||||
async overrideEnabledProjects(
|
||||
environmentNamesToEnable: string[],
|
||||
): Promise<void> {
|
||||
|
@ -1062,11 +1062,16 @@ class FeatureToggleService {
|
||||
featureName,
|
||||
environment,
|
||||
);
|
||||
const defaultStrategy = await this.projectStore.getDefaultStrategy(
|
||||
project,
|
||||
environment,
|
||||
);
|
||||
return {
|
||||
name: featureName,
|
||||
environment,
|
||||
enabled: envMetadata.enabled,
|
||||
strategies,
|
||||
defaultStrategy,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -238,7 +238,7 @@ export default class ProjectService {
|
||||
);
|
||||
return arraysHaveSameItems(
|
||||
featureEnvs.map((env) => env.environment),
|
||||
newEnvs,
|
||||
newEnvs.map((projectEnv) => projectEnv.environment),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ import { IRole } from './stores/access-store';
|
||||
import { IUser } from './user';
|
||||
import { ALL_OPERATORS } from '../util';
|
||||
import { IProjectStats } from 'lib/services/project-service';
|
||||
import { CreateFeatureStrategySchema } from '../openapi';
|
||||
import { ProjectEnvironment } from './stores/project-store';
|
||||
|
||||
export type Operator = typeof ALL_OPERATORS[number];
|
||||
|
||||
@ -84,6 +86,7 @@ export interface IFeatureEnvironmentInfo {
|
||||
environment: string;
|
||||
enabled: boolean;
|
||||
strategies: IFeatureStrategy[];
|
||||
defaultStrategy?: CreateFeatureStrategySchema;
|
||||
}
|
||||
|
||||
export interface FeatureToggleWithEnvironment extends FeatureToggle {
|
||||
@ -141,6 +144,7 @@ export interface IEnvironment {
|
||||
export interface IProjectEnvironment extends IEnvironment {
|
||||
projectApiTokenCount?: number;
|
||||
projectEnabledToggleCount?: number;
|
||||
defaultStrategy?: CreateFeatureStrategySchema;
|
||||
}
|
||||
|
||||
export interface IEnvironmentCreate {
|
||||
@ -182,7 +186,7 @@ export type ProjectMode = 'open' | 'protected';
|
||||
export interface IProjectOverview {
|
||||
name: string;
|
||||
description: string;
|
||||
environments: string[];
|
||||
environments: ProjectEnvironment[];
|
||||
features: IFeatureOverview[];
|
||||
members: number;
|
||||
version: number;
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
ProjectMode,
|
||||
} from '../model';
|
||||
import { Store } from './store';
|
||||
import { CreateFeatureStrategySchema } from '../../openapi';
|
||||
|
||||
export interface IProjectInsert {
|
||||
id: string;
|
||||
@ -29,6 +30,11 @@ export interface IProjectSettingsRow {
|
||||
default_stickiness: string;
|
||||
}
|
||||
|
||||
export interface IProjectEnvironmenDefaultStrategyRow {
|
||||
environment: string;
|
||||
default_strategy: any;
|
||||
}
|
||||
|
||||
export interface IProjectArchived {
|
||||
id: string;
|
||||
archived: boolean;
|
||||
@ -43,6 +49,12 @@ export interface IProjectQuery {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export type ProjectEnvironment = {
|
||||
environment: string;
|
||||
changeRequestEnabled?: boolean;
|
||||
defaultStrategy?: CreateFeatureStrategySchema;
|
||||
};
|
||||
|
||||
export interface IProjectEnvironmentWithChangeRequests {
|
||||
environment: string;
|
||||
changeRequestsEnabled: boolean;
|
||||
@ -66,7 +78,7 @@ export interface IProjectStore extends Store<IProject, string> {
|
||||
|
||||
deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
|
||||
|
||||
getEnvironmentsForProject(id: string): Promise<string[]>;
|
||||
getEnvironmentsForProject(id: string): Promise<ProjectEnvironment[]>;
|
||||
|
||||
getMembersCountByProject(projectId: string): Promise<number>;
|
||||
|
||||
@ -103,4 +115,14 @@ export interface IProjectStore extends Store<IProject, string> {
|
||||
defaultStickiness: string,
|
||||
mode: ProjectMode,
|
||||
): Promise<void>;
|
||||
|
||||
getDefaultStrategy(
|
||||
projectId: string,
|
||||
environment: string,
|
||||
): Promise<CreateFeatureStrategySchema | null>;
|
||||
updateDefaultStrategy(
|
||||
projectId: string,
|
||||
environment: string,
|
||||
strategy: CreateFeatureStrategySchema,
|
||||
): Promise<CreateFeatureStrategySchema>;
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function (db, callback) {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE project_environments
|
||||
ADD COLUMN IF NOT EXISTS default_strategy jsonb;
|
||||
`,
|
||||
callback,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE project_environments
|
||||
DROP COLUMN IF EXISTS default_strategy;
|
||||
`,
|
||||
callback,
|
||||
);
|
||||
};
|
@ -132,7 +132,7 @@ test('update admin token with expiry', async () => {
|
||||
});
|
||||
|
||||
test('creates a lot of client tokens', async () => {
|
||||
const requests = [];
|
||||
const requests: any[] = [];
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
requests.push(
|
||||
|
@ -92,11 +92,11 @@ describe('Playground API E2E', () => {
|
||||
features.map(async (feature) => {
|
||||
// create feature
|
||||
const toggle = await database.stores.featureToggleStore.create(
|
||||
feature.project,
|
||||
feature.project!,
|
||||
{
|
||||
...feature,
|
||||
...(feature as any),
|
||||
createdAt: undefined,
|
||||
variants: null,
|
||||
variants: null as any,
|
||||
},
|
||||
);
|
||||
|
||||
@ -108,7 +108,7 @@ describe('Playground API E2E', () => {
|
||||
);
|
||||
|
||||
await database.stores.featureToggleStore.saveVariants(
|
||||
feature.project,
|
||||
feature.project!,
|
||||
feature.name,
|
||||
[
|
||||
...(feature.variants ?? []).map((variant) => ({
|
||||
@ -131,7 +131,7 @@ describe('Playground API E2E', () => {
|
||||
environment,
|
||||
strategyName: strategy.name,
|
||||
disabled: !!(index % 2),
|
||||
projectId: feature.project,
|
||||
projectId: feature.project!,
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -194,7 +194,7 @@ describe('Playground API E2E', () => {
|
||||
),
|
||||
);
|
||||
|
||||
request.projects = projects;
|
||||
request.projects = projects as any;
|
||||
|
||||
// create a list of features that can be filtered
|
||||
// pass in args that should filter the list
|
||||
@ -388,6 +388,7 @@ describe('Playground API E2E', () => {
|
||||
(acc, next) => ({
|
||||
...acc,
|
||||
[next.name]:
|
||||
// @ts-ignore
|
||||
next.strategies[0].constraints[0]
|
||||
.values[0] === req.context.appName,
|
||||
}),
|
||||
@ -485,8 +486,8 @@ describe('Playground API E2E', () => {
|
||||
(acc, next) => ({
|
||||
...acc,
|
||||
[next.name]:
|
||||
next.strategies[0].constraints[0]
|
||||
.values[0] === contextField,
|
||||
next.strategies![0].constraints![0]
|
||||
.values![0] === contextField,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
@ -599,14 +600,14 @@ describe('Playground API E2E', () => {
|
||||
const shouldBeEnabled = features.reduce(
|
||||
(acc, next) => {
|
||||
const constraint =
|
||||
next.strategies[0].constraints[0];
|
||||
next.strategies![0].constraints![0];
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[next.name]:
|
||||
constraint.contextName ===
|
||||
generatedContextValue.name &&
|
||||
constraint.values[0] ===
|
||||
constraint.values![0] ===
|
||||
generatedContextValue.value,
|
||||
};
|
||||
},
|
||||
@ -684,7 +685,7 @@ describe('Playground API E2E', () => {
|
||||
const body = await playgroundRequest(app, token.secret, request);
|
||||
|
||||
// when enabled, this toggle should have one of the variants
|
||||
expect(body.features[0].variant.name).toBe('a');
|
||||
expect(body.features[0].variant!.name).toBe('a');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
setupAppWithCustomConfig,
|
||||
} from '../../../helpers/test-helper';
|
||||
import getLogger from '../../../../fixtures/no-logger';
|
||||
import { DEFAULT_ENV } from '../../../../../lib/util/constants';
|
||||
import { DEFAULT_ENV } from '../../../../../lib/util';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
@ -26,11 +26,11 @@ afterEach(async () => {
|
||||
);
|
||||
await Promise.all(
|
||||
all
|
||||
.filter((env) => env !== DEFAULT_ENV)
|
||||
.filter((env) => env.environment !== DEFAULT_ENV)
|
||||
.map(async (env) =>
|
||||
db.stores.projectStore.deleteEnvironmentForProject(
|
||||
'default',
|
||||
env,
|
||||
env.environment,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -56,7 +56,7 @@ test('Should add environment to project', async () => {
|
||||
'default',
|
||||
);
|
||||
|
||||
const environment = envs.find((env) => env === 'test');
|
||||
const environment = envs.find((env) => env.environment === 'test');
|
||||
|
||||
expect(environment).toBeDefined();
|
||||
expect(envs).toHaveLength(2);
|
||||
@ -111,3 +111,51 @@ test('Should not remove environment from project if project only has one environ
|
||||
|
||||
expect(envs).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Should add default strategy to environment', async () => {
|
||||
await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/environments/default/default-strategy`,
|
||||
)
|
||||
.send({
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
parameters: {
|
||||
rollout: '50',
|
||||
stickiness: 'customAppName',
|
||||
groupId: 'stickytoggle',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const envs = await db.stores.projectStore.getEnvironmentsForProject(
|
||||
'default',
|
||||
);
|
||||
|
||||
expect(envs).toHaveLength(1);
|
||||
expect(envs[0]).toStrictEqual({
|
||||
environment: 'default',
|
||||
defaultStrategy: {
|
||||
name: 'flexibleRollout',
|
||||
constraints: [],
|
||||
parameters: {
|
||||
rollout: '50',
|
||||
stickiness: 'customAppName',
|
||||
groupId: 'stickytoggle',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Should throw an error if you try to set defaultStrategy other than flexibleRollout', async () => {
|
||||
await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/environments/default/default-strategy`,
|
||||
)
|
||||
.send({
|
||||
name: 'default',
|
||||
constraints: [],
|
||||
parameters: {},
|
||||
})
|
||||
.expect(400);
|
||||
});
|
||||
|
@ -18,11 +18,11 @@ afterEach(async () => {
|
||||
);
|
||||
await Promise.all(
|
||||
all
|
||||
.filter((env) => env !== DEFAULT_ENV)
|
||||
.filter((env) => env.environment !== DEFAULT_ENV)
|
||||
.map(async (env) =>
|
||||
db.stores.projectStore.deleteEnvironmentForProject(
|
||||
'default',
|
||||
env,
|
||||
env.environment,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -102,11 +102,11 @@ afterEach(async () => {
|
||||
);
|
||||
await Promise.all(
|
||||
all
|
||||
.filter((env) => env !== DEFAULT_ENV)
|
||||
.filter((env) => env.environment !== DEFAULT_ENV)
|
||||
.map(async (env) =>
|
||||
db.stores.projectStore.deleteEnvironmentForProject(
|
||||
'default',
|
||||
env,
|
||||
env.environment,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -2693,7 +2693,7 @@ test('should add multiple segments to a strategy', async () => {
|
||||
const defaultEnv = res.body.environments.find(
|
||||
(env) => env.name === 'default',
|
||||
);
|
||||
const strategy = defaultEnv.strategies.find(
|
||||
const strategy = defaultEnv?.strategies.find(
|
||||
(strat) => strat.id === strategyOne.id,
|
||||
);
|
||||
|
||||
|
@ -56,7 +56,9 @@ test('Project with no stale toggles should have 100% health rating', async () =>
|
||||
.expect((res) => {
|
||||
expect(res.body.health).toBe(100);
|
||||
expect(res.body.environments).toHaveLength(1);
|
||||
expect(res.body.environments).toStrictEqual(['default']);
|
||||
expect(res.body.environments).toStrictEqual([
|
||||
{ environment: 'default' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -73,13 +73,13 @@ beforeAll(async () => {
|
||||
settingService,
|
||||
});
|
||||
resetTokenService = new ResetTokenService(stores, config);
|
||||
const adminRole = await accessService.getRootRole(RoleName.ADMIN);
|
||||
const adminRole = (await accessService.getRootRole(RoleName.ADMIN))!;
|
||||
adminUser = await userService.createUser({
|
||||
username: 'admin@test.com',
|
||||
rootRole: adminRole.id,
|
||||
});
|
||||
})!;
|
||||
|
||||
const userRole = await accessService.getRootRole(RoleName.EDITOR);
|
||||
const userRole = (await accessService.getRootRole(RoleName.EDITOR))!;
|
||||
user = await userService.createUser({
|
||||
username: 'test@test.com',
|
||||
email: 'test@test.com',
|
||||
@ -99,7 +99,7 @@ afterAll(async () => {
|
||||
test('Can validate token for password reset', async () => {
|
||||
const url = await resetTokenService.createResetPasswordUrl(
|
||||
user.id,
|
||||
adminUser.username,
|
||||
adminUser.username!,
|
||||
);
|
||||
const relative = getBackendResetUrl(url);
|
||||
return app.request
|
||||
@ -114,12 +114,12 @@ test('Can validate token for password reset', async () => {
|
||||
test('Can use token to reset password', async () => {
|
||||
const url = await resetTokenService.createResetPasswordUrl(
|
||||
user.id,
|
||||
adminUser.username,
|
||||
adminUser.username!,
|
||||
);
|
||||
const relative = getBackendResetUrl(url);
|
||||
// Can't login before reset
|
||||
await expect(async () =>
|
||||
userService.loginUser(user.email, password),
|
||||
userService.loginUser(user.email!, password),
|
||||
).rejects.toThrow(Error);
|
||||
|
||||
let token;
|
||||
@ -137,14 +137,14 @@ test('Can use token to reset password', async () => {
|
||||
password,
|
||||
})
|
||||
.expect(200);
|
||||
const loggedInUser = await userService.loginUser(user.email, password);
|
||||
const loggedInUser = await userService.loginUser(user.email!, password);
|
||||
expect(user.email).toBe(loggedInUser.email);
|
||||
});
|
||||
|
||||
test('Trying to reset password with same token twice does not work', async () => {
|
||||
const url = await resetTokenService.createResetPasswordUrl(
|
||||
user.id,
|
||||
adminUser.username,
|
||||
adminUser.username!,
|
||||
);
|
||||
const relative = getBackendResetUrl(url);
|
||||
let token;
|
||||
@ -205,7 +205,7 @@ test('Calling reset endpoint with already existing session should logout/destroy
|
||||
const { request, destroy } = await setupAppWithAuth(stores);
|
||||
const url = await resetTokenService.createResetPasswordUrl(
|
||||
user.id,
|
||||
adminUser.username,
|
||||
adminUser.username!,
|
||||
);
|
||||
const relative = getBackendResetUrl(url);
|
||||
let token;
|
||||
@ -248,7 +248,7 @@ test('Trying to change password to undefined should yield 400 without crashing t
|
||||
|
||||
const url = await resetTokenService.createResetPasswordUrl(
|
||||
user.id,
|
||||
adminUser.username,
|
||||
adminUser.username!,
|
||||
);
|
||||
const relative = getBackendResetUrl(url);
|
||||
let token;
|
||||
@ -271,7 +271,7 @@ test('Trying to change password to undefined should yield 400 without crashing t
|
||||
test('changing password should expire all active tokens', async () => {
|
||||
const url = await resetTokenService.createResetPasswordUrl(
|
||||
user.id,
|
||||
adminUser.username,
|
||||
adminUser.username!,
|
||||
);
|
||||
const relative = getBackendResetUrl(url);
|
||||
|
||||
|
@ -37,6 +37,7 @@ beforeEach(async () => {
|
||||
const groupService = new GroupService(stores, config);
|
||||
const accessService = new AccessService(stores, config, groupService);
|
||||
const resetTokenService = new ResetTokenService(stores, config);
|
||||
// @ts-ignore
|
||||
const emailService = new EmailService(undefined, config.getLogger);
|
||||
const sessionService = new SessionService(stores, config);
|
||||
const settingService = new SettingService(stores, config);
|
||||
@ -52,7 +53,7 @@ beforeEach(async () => {
|
||||
adminUser = await userService.createUser({
|
||||
username: 'admin@test.com',
|
||||
email: 'admin@test.com',
|
||||
rootRole: adminRole.id,
|
||||
rootRole: adminRole!.id,
|
||||
password: password,
|
||||
});
|
||||
});
|
||||
|
@ -2593,7 +2593,7 @@ The provider you choose for your addon dictates what properties the \`parameters
|
||||
},
|
||||
"environments": {
|
||||
"items": {
|
||||
"type": "string",
|
||||
"$ref": "#/components/schemas/projectEnvironmentSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
@ -2660,7 +2660,7 @@ The provider you choose for your addon dictates what properties the \`parameters
|
||||
},
|
||||
"environments": {
|
||||
"items": {
|
||||
"type": "string",
|
||||
"$ref": "#/components/schemas/projectEnvironmentSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
@ -3514,6 +3514,9 @@ The provider you choose for your addon dictates what properties the \`parameters
|
||||
"changeRequestsEnabled": {
|
||||
"type": "boolean",
|
||||
},
|
||||
"defaultStrategy": {
|
||||
"$ref": "#/components/schemas/createFeatureStrategySchema",
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
},
|
||||
@ -3541,11 +3544,24 @@ The provider you choose for your addon dictates what properties the \`parameters
|
||||
"environments": {
|
||||
"description": "The environments that are enabled for this project",
|
||||
"example": [
|
||||
"development",
|
||||
"production",
|
||||
{
|
||||
"environment": "development",
|
||||
},
|
||||
{
|
||||
"defaultStrategy": {
|
||||
"constraints": [],
|
||||
"name": "flexibleRollout",
|
||||
"parameters": {
|
||||
"groupId": "stickytoggle",
|
||||
"rollout": "50",
|
||||
"stickiness": "customAppName",
|
||||
},
|
||||
},
|
||||
"environment": "production",
|
||||
},
|
||||
],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"$ref": "#/components/schemas/projectEnvironmentSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
@ -9169,6 +9185,83 @@ If the provided project does not exist, the list of events will be empty.",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/projects/{projectId}/environments/{environment}/default-strategy": {
|
||||
"post": {
|
||||
"description": "Adds a default strategy for this environment. Unleash will use this strategy by default when enabling a toggle. Use the wild card "*" for \`:environment\` to add to all environments. ",
|
||||
"operationId": "addDefaultStrategyToProjectEnvironment",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "projectId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "environment",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/createFeatureStrategySchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "createFeatureStrategySchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/createFeatureStrategySchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "createFeatureStrategySchema",
|
||||
},
|
||||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "The ID of the error instance",
|
||||
"example": "9c40958a-daac-400e-98fb-3bb438567008",
|
||||
"type": "string",
|
||||
},
|
||||
"message": {
|
||||
"description": "A description of what went wrong.",
|
||||
"example": "The request payload you provided doesn't conform to the schema. The .parameters property should be object. You sent [].",
|
||||
"type": "string",
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the error kind",
|
||||
"example": "ValidationError",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "The request data does not match what we expect.",
|
||||
},
|
||||
},
|
||||
"tags": [
|
||||
"Projects",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/projects/{projectId}/favorites": {
|
||||
"delete": {
|
||||
"operationId": "removeFavoriteProject",
|
||||
|
@ -2,7 +2,7 @@ import EnvironmentService from '../../../lib/services/environment-service';
|
||||
import { createTestConfig } from '../../config/test-config';
|
||||
import dbInit from '../helpers/database-init';
|
||||
import NotFoundError from '../../../lib/error/notfound-error';
|
||||
import { IUnleashStores } from '../../../lib/types/stores';
|
||||
import { IUnleashStores } from '../../../lib/types';
|
||||
import NameExistsError from '../../../lib/error/name-exists-error';
|
||||
|
||||
let stores: IUnleashStores;
|
||||
@ -164,8 +164,8 @@ test('Setting an override disables all other envs', async () => {
|
||||
.filter((x) => x.name != enabledEnvName)
|
||||
.map((env) => env.enabled);
|
||||
|
||||
expect(targetedEnvironment.enabled).toBe(true);
|
||||
expect(allOtherEnvironments.every((x) => x === false)).toBe(true);
|
||||
expect(targetedEnvironment?.enabled).toBe(true);
|
||||
expect(allOtherEnvironments.every((x) => !x)).toBe(true);
|
||||
});
|
||||
|
||||
test('Passing an empty override does nothing', async () => {
|
||||
@ -185,7 +185,7 @@ test('Passing an empty override does nothing', async () => {
|
||||
(env) => env.name == enabledEnvName,
|
||||
);
|
||||
|
||||
expect(targetedEnvironment.enabled).toBe(true);
|
||||
expect(targetedEnvironment?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('When given overrides should remap projects to override environments', async () => {
|
||||
@ -224,9 +224,9 @@ test('When given overrides should remap projects to override environments', asyn
|
||||
|
||||
await service.overrideEnabledProjects([enabledEnvName]);
|
||||
|
||||
const projects = await stores.projectStore.getEnvironmentsForProject(
|
||||
'default',
|
||||
);
|
||||
const projects = (
|
||||
await stores.projectStore.getEnvironmentsForProject('default')
|
||||
).map((e) => e.environment);
|
||||
|
||||
expect(projects).toContain('enabled');
|
||||
expect(projects).not.toContain('default');
|
||||
@ -263,6 +263,6 @@ test('Override works correctly when enabling default and disabling prod and dev'
|
||||
|
||||
expect(envNames).toContain('production');
|
||||
expect(envNames).toContain('development');
|
||||
expect(targetedEnvironment.enabled).toBe(true);
|
||||
expect(allOtherEnvironments.every((x) => x === false)).toBe(true);
|
||||
expect(targetedEnvironment?.enabled).toBe(true);
|
||||
expect(allOtherEnvironments.every((x) => !x)).toBe(true);
|
||||
});
|
||||
|
@ -727,8 +727,12 @@ test('A newly created project only gets connected to enabled environments', asyn
|
||||
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();
|
||||
expect(
|
||||
connectedEnvs.some((e) => e.environment === enabledEnv),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
connectedEnvs.some((e) => e.environment === disabledEnv),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should have environments sorted in order', async () => {
|
||||
@ -768,7 +772,13 @@ test('should have environments sorted in order', async () => {
|
||||
const connectedEnvs =
|
||||
await db.stores.projectStore.getEnvironmentsForProject(project.id);
|
||||
|
||||
expect(connectedEnvs).toEqual(['default', first, second, third, fourth]);
|
||||
expect(connectedEnvs.map((e) => e.environment)).toEqual([
|
||||
'default',
|
||||
first,
|
||||
second,
|
||||
third,
|
||||
fourth,
|
||||
]);
|
||||
});
|
||||
|
||||
test('should add a user to the project with a custom role', async () => {
|
||||
|
24
src/test/fixtures/fake-project-store.ts
vendored
24
src/test/fixtures/fake-project-store.ts
vendored
@ -3,6 +3,7 @@ import {
|
||||
IProjectInsert,
|
||||
IProjectSettings,
|
||||
IProjectStore,
|
||||
ProjectEnvironment,
|
||||
} from '../../lib/types/stores/project-store';
|
||||
import {
|
||||
IEnvironment,
|
||||
@ -15,13 +16,14 @@ import {
|
||||
IEnvironmentProjectLink,
|
||||
IProjectMembersCount,
|
||||
} from 'lib/db/project-store';
|
||||
import { CreateFeatureStrategySchema } from '../../lib/openapi';
|
||||
|
||||
export default class FakeProjectStore implements IProjectStore {
|
||||
projects: IProject[] = [];
|
||||
|
||||
projectEnvironment: Map<string, Set<string>> = new Map();
|
||||
|
||||
getEnvironmentsForProject(): Promise<string[]> {
|
||||
getEnvironmentsForProject(): Promise<ProjectEnvironment[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@ -180,4 +182,24 @@ export default class FakeProjectStore implements IProjectStore {
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
updateDefaultStrategy(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
strategy: CreateFeatureStrategySchema,
|
||||
): Promise<CreateFeatureStrategySchema> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getDefaultStrategy(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
): Promise<CreateFeatureStrategySchema | undefined> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user