mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
feat: export dependent feature toggles (#5007)
This commit is contained in:
parent
7343183f2d
commit
2059706e77
@ -1,4 +1,5 @@
|
|||||||
import { IDependency } from '../../types';
|
import { IDependency, IFeatureDependency } from '../../types';
|
||||||
|
import { FeatureDependency } from './dependent-features';
|
||||||
|
|
||||||
export interface IDependentFeaturesReadModel {
|
export interface IDependentFeaturesReadModel {
|
||||||
getChildren(parents: string[]): Promise<string[]>;
|
getChildren(parents: string[]): Promise<string[]>;
|
||||||
@ -6,6 +7,7 @@ export interface IDependentFeaturesReadModel {
|
|||||||
// we're interested in the list of parents, not orphans
|
// we're interested in the list of parents, not orphans
|
||||||
getOrphanParents(parentsAndChildren: string[]): Promise<string[]>;
|
getOrphanParents(parentsAndChildren: string[]): Promise<string[]>;
|
||||||
getParents(child: string): Promise<IDependency[]>;
|
getParents(child: string): Promise<IDependency[]>;
|
||||||
|
getDependencies(children: string[]): Promise<IFeatureDependency[]>;
|
||||||
getParentOptions(child: string): Promise<string[]>;
|
getParentOptions(child: string): Promise<string[]>;
|
||||||
hasDependencies(feature: string): Promise<boolean>;
|
hasDependencies(feature: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Db } from '../../db/db';
|
import { Db } from '../../db/db';
|
||||||
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
|
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
|
||||||
import { IDependency } from '../../types';
|
import { IDependency, IFeatureDependency } from '../../types';
|
||||||
|
import { FeatureDependency } from './dependent-features';
|
||||||
|
|
||||||
export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
||||||
private db: Db;
|
private db: Db;
|
||||||
@ -43,6 +44,22 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDependencies(children: string[]): Promise<IFeatureDependency[]> {
|
||||||
|
const rows = await this.db('dependent_features').whereIn(
|
||||||
|
'child',
|
||||||
|
children,
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
feature: row.child,
|
||||||
|
dependency: {
|
||||||
|
feature: row.parent,
|
||||||
|
enabled: row.enabled,
|
||||||
|
variants: row.variants,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async getParentOptions(child: string): Promise<string[]> {
|
async getParentOptions(child: string): Promise<string[]> {
|
||||||
const result = await this.db('features')
|
const result = await this.db('features')
|
||||||
.where('features.name', child)
|
.where('features.name', child)
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
|
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
|
||||||
import { IDependency } from '../../types';
|
import { IDependency, IFeatureDependency } from '../../types';
|
||||||
|
import { FeatureDependency } from './dependent-features';
|
||||||
|
|
||||||
export class FakeDependentFeaturesReadModel
|
export class FakeDependentFeaturesReadModel
|
||||||
implements IDependentFeaturesReadModel
|
implements IDependentFeaturesReadModel
|
||||||
{
|
{
|
||||||
|
getDependencies(): Promise<IFeatureDependency[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
getChildren(): Promise<string[]> {
|
getChildren(): Promise<string[]> {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,8 @@ import {
|
|||||||
createPrivateProjectChecker,
|
createPrivateProjectChecker,
|
||||||
} from '../private-project/createPrivateProjectChecker';
|
} from '../private-project/createPrivateProjectChecker';
|
||||||
import { DbServiceFactory } from 'lib/db/transaction';
|
import { DbServiceFactory } from 'lib/db/transaction';
|
||||||
|
import { DependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model';
|
||||||
|
import { FakeDependentFeaturesReadModel } from '../dependent-features/fake-dependent-features-read-model';
|
||||||
|
|
||||||
export const createFakeExportImportTogglesService = (
|
export const createFakeExportImportTogglesService = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -102,6 +104,8 @@ export const createFakeExportImportTogglesService = (
|
|||||||
{ getLogger },
|
{ getLogger },
|
||||||
eventService,
|
eventService,
|
||||||
);
|
);
|
||||||
|
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
|
||||||
|
|
||||||
const exportImportService = new ExportImportService(
|
const exportImportService = new ExportImportService(
|
||||||
{
|
{
|
||||||
importTogglesStore,
|
importTogglesStore,
|
||||||
@ -123,6 +127,7 @@ export const createFakeExportImportTogglesService = (
|
|||||||
strategyService,
|
strategyService,
|
||||||
tagTypeService,
|
tagTypeService,
|
||||||
},
|
},
|
||||||
|
dependentFeaturesReadModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
return exportImportService;
|
return exportImportService;
|
||||||
@ -213,6 +218,8 @@ export const deferredExportImportTogglesService = (
|
|||||||
{ getLogger },
|
{ getLogger },
|
||||||
eventService,
|
eventService,
|
||||||
);
|
);
|
||||||
|
const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);
|
||||||
|
|
||||||
const exportImportService = new ExportImportService(
|
const exportImportService = new ExportImportService(
|
||||||
{
|
{
|
||||||
importTogglesStore,
|
importTogglesStore,
|
||||||
@ -234,6 +241,7 @@ export const deferredExportImportTogglesService = (
|
|||||||
strategyService,
|
strategyService,
|
||||||
tagTypeService,
|
tagTypeService,
|
||||||
},
|
},
|
||||||
|
dependentFeaturesReadModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
return exportImportService;
|
return exportImportService;
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import { IUnleashConfig } from '../../types/option';
|
import { Logger } from '../../logger';
|
||||||
|
import { IStrategy } from '../../types/stores/strategy-store';
|
||||||
|
import { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type';
|
||||||
|
import { IFeatureStrategiesStore } from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||||
import {
|
import {
|
||||||
|
IUnleashConfig,
|
||||||
|
IContextFieldStore,
|
||||||
|
IUnleashStores,
|
||||||
|
ISegmentStore,
|
||||||
|
IFeatureEnvironmentStore,
|
||||||
|
ITagTypeStore,
|
||||||
|
IFeatureTagStore,
|
||||||
FeatureToggleDTO,
|
FeatureToggleDTO,
|
||||||
IFeatureStrategy,
|
IFeatureStrategy,
|
||||||
IFeatureStrategySegment,
|
IFeatureStrategySegment,
|
||||||
IVariant,
|
IVariant,
|
||||||
} from '../../types/model';
|
} from '../../types';
|
||||||
import { Logger } from '../../logger';
|
import { ExportQuerySchema, ImportTogglesSchema } from '../../openapi';
|
||||||
import { IFeatureTagStore } from '../../types/stores/feature-tag-store';
|
|
||||||
import { ITagTypeStore } from '../../types/stores/tag-type-store';
|
|
||||||
import { IStrategy } from '../../types/stores/strategy-store';
|
|
||||||
import { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type';
|
|
||||||
import { IFeatureStrategiesStore } from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
|
||||||
import { IFeatureEnvironmentStore } from '../../types/stores/feature-environment-store';
|
|
||||||
import { IContextFieldStore, IUnleashStores } from '../../types/stores';
|
|
||||||
import { ISegmentStore } from '../../types/stores/segment-store';
|
|
||||||
import { ExportQuerySchema } from '../../openapi/spec/export-query-schema';
|
|
||||||
import {
|
import {
|
||||||
FEATURES_EXPORTED,
|
FEATURES_EXPORTED,
|
||||||
FEATURES_IMPORTED,
|
FEATURES_IMPORTED,
|
||||||
@ -27,7 +28,6 @@ import {
|
|||||||
FeatureStrategySchema,
|
FeatureStrategySchema,
|
||||||
ImportTogglesValidateSchema,
|
ImportTogglesValidateSchema,
|
||||||
} from '../../openapi';
|
} from '../../openapi';
|
||||||
import { ImportTogglesSchema } from '../../openapi/spec/import-toggles-schema';
|
|
||||||
import User from '../../types/user';
|
import User from '../../types/user';
|
||||||
import { BadDataError } from '../../error';
|
import { BadDataError } from '../../error';
|
||||||
import { extractUsernameFromUser } from '../../util';
|
import { extractUsernameFromUser } from '../../util';
|
||||||
@ -49,6 +49,8 @@ import { ImportPermissionsService, Mode } from './import-permissions-service';
|
|||||||
import { ImportValidationMessages } from './import-validation-messages';
|
import { ImportValidationMessages } from './import-validation-messages';
|
||||||
import { findDuplicates } from '../../util/findDuplicates';
|
import { findDuplicates } from '../../util/findDuplicates';
|
||||||
import { FeatureNameCheckResultWithFeaturePattern } from '../feature-toggle/feature-toggle-service';
|
import { FeatureNameCheckResultWithFeaturePattern } from '../feature-toggle/feature-toggle-service';
|
||||||
|
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
|
||||||
|
import groupBy from 'lodash.groupby';
|
||||||
|
|
||||||
export type IImportService = {
|
export type IImportService = {
|
||||||
validate(
|
validate(
|
||||||
@ -105,6 +107,8 @@ export default class ExportImportService
|
|||||||
|
|
||||||
private importPermissionsService: ImportPermissionsService;
|
private importPermissionsService: ImportPermissionsService;
|
||||||
|
|
||||||
|
private dependentFeaturesReadModel: IDependentFeaturesReadModel;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stores: Pick<
|
stores: Pick<
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
@ -139,6 +143,7 @@ export default class ExportImportService
|
|||||||
| 'tagTypeService'
|
| 'tagTypeService'
|
||||||
| 'featureTagService'
|
| 'featureTagService'
|
||||||
>,
|
>,
|
||||||
|
dependentFeaturesReadModel: IDependentFeaturesReadModel,
|
||||||
) {
|
) {
|
||||||
this.toggleStore = stores.featureToggleStore;
|
this.toggleStore = stores.featureToggleStore;
|
||||||
this.importTogglesStore = stores.importTogglesStore;
|
this.importTogglesStore = stores.importTogglesStore;
|
||||||
@ -162,6 +167,7 @@ export default class ExportImportService
|
|||||||
this.tagTypeService,
|
this.tagTypeService,
|
||||||
this.contextService,
|
this.contextService,
|
||||||
);
|
);
|
||||||
|
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
|
||||||
this.logger = getLogger('services/state-service.js');
|
this.logger = getLogger('services/state-service.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,7 +338,10 @@ export default class ExportImportService
|
|||||||
if (tag.tagType) {
|
if (tag.tagType) {
|
||||||
await this.featureTagService.addTag(
|
await this.featureTagService.addTag(
|
||||||
tag.featureName,
|
tag.featureName,
|
||||||
{ type: tag.tagType, value: tag.tagValue },
|
{
|
||||||
|
type: tag.tagType,
|
||||||
|
value: tag.tagValue,
|
||||||
|
},
|
||||||
extractUsernameFromUser(user),
|
extractUsernameFromUser(user),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -657,6 +666,7 @@ export default class ExportImportService
|
|||||||
featureTags,
|
featureTags,
|
||||||
segments,
|
segments,
|
||||||
tagTypes,
|
tagTypes,
|
||||||
|
featureDependencies,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.toggleStore.getAllByNames(featureNames),
|
this.toggleStore.getAllByNames(featureNames),
|
||||||
await this.featureEnvironmentStore.getAllByFeatures(
|
await this.featureEnvironmentStore.getAllByFeatures(
|
||||||
@ -672,6 +682,7 @@ export default class ExportImportService
|
|||||||
this.featureTagStore.getAllByFeatures(featureNames),
|
this.featureTagStore.getAllByFeatures(featureNames),
|
||||||
this.segmentStore.getAll(),
|
this.segmentStore.getAll(),
|
||||||
this.tagTypeStore.getAll(),
|
this.tagTypeStore.getAll(),
|
||||||
|
this.dependentFeaturesReadModel.getDependencies(featureNames),
|
||||||
]);
|
]);
|
||||||
this.addSegmentsToStrategies(featureStrategies, strategySegments);
|
this.addSegmentsToStrategies(featureStrategies, strategySegments);
|
||||||
const filteredContextFields = contextFields
|
const filteredContextFields = contextFields
|
||||||
@ -708,6 +719,19 @@ export default class ExportImportService
|
|||||||
const filteredTagTypes = tagTypes.filter((tagType) =>
|
const filteredTagTypes = tagTypes.filter((tagType) =>
|
||||||
featureTags.map((tag) => tag.tagType).includes(tagType.name),
|
featureTags.map((tag) => tag.tagType).includes(tagType.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groupedFeatureDependencies = groupBy(
|
||||||
|
featureDependencies,
|
||||||
|
'feature',
|
||||||
|
);
|
||||||
|
|
||||||
|
const mappedFeatureDependencies = Object.entries(
|
||||||
|
groupedFeatureDependencies,
|
||||||
|
).map(([feature, dependencies]) => ({
|
||||||
|
feature,
|
||||||
|
dependencies: dependencies.map((d) => d.dependency),
|
||||||
|
}));
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
features: features.map((item) => {
|
features: features.map((item) => {
|
||||||
const { createdAt, archivedAt, lastSeenAt, ...rest } = item;
|
const { createdAt, archivedAt, lastSeenAt, ...rest } = item;
|
||||||
@ -741,9 +765,13 @@ export default class ExportImportService
|
|||||||
featureTags,
|
featureTags,
|
||||||
segments: filteredSegments.map((item) => {
|
segments: filteredSegments.map((item) => {
|
||||||
const { id, name } = item;
|
const { id, name } = item;
|
||||||
return { id, name };
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
tagTypes: filteredTagTypes,
|
tagTypes: filteredTagTypes,
|
||||||
|
dependencies: mappedFeatureDependencies,
|
||||||
};
|
};
|
||||||
await this.eventService.storeEvent({
|
await this.eventService.storeEvent({
|
||||||
type: FEATURES_EXPORTED,
|
type: FEATURES_EXPORTED,
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
import { DEFAULT_ENV } from '../../util';
|
import { DEFAULT_ENV } from '../../util';
|
||||||
import {
|
import {
|
||||||
ContextFieldSchema,
|
ContextFieldSchema,
|
||||||
|
CreateDependentFeatureSchema,
|
||||||
ImportTogglesSchema,
|
ImportTogglesSchema,
|
||||||
UpsertSegmentSchema,
|
UpsertSegmentSchema,
|
||||||
VariantsSchema,
|
VariantsSchema,
|
||||||
@ -47,7 +48,10 @@ const defaultContext: ContextFieldSchema = {
|
|||||||
description: 'A region',
|
description: 'A region',
|
||||||
legalValues: [
|
legalValues: [
|
||||||
{ value: 'north' },
|
{ value: 'north' },
|
||||||
{ value: 'south', description: 'south-desc' },
|
{
|
||||||
|
value: 'south',
|
||||||
|
description: 'south-desc',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -68,7 +72,11 @@ const createToggle = async (
|
|||||||
if (strategy) {
|
if (strategy) {
|
||||||
await app.services.featureToggleServiceV2.createStrategy(
|
await app.services.featureToggleServiceV2.createStrategy(
|
||||||
strategy,
|
strategy,
|
||||||
{ projectId, featureName: toggle.name, environment: DEFAULT_ENV },
|
{
|
||||||
|
projectId,
|
||||||
|
featureName: toggle.name,
|
||||||
|
environment: DEFAULT_ENV,
|
||||||
|
},
|
||||||
username,
|
username,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -152,6 +160,7 @@ beforeAll(async () => {
|
|||||||
flags: {
|
flags: {
|
||||||
featuresExportImport: true,
|
featuresExportImport: true,
|
||||||
featureNamingPattern: true,
|
featureNamingPattern: true,
|
||||||
|
dependentFeatures: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -193,7 +202,10 @@ describe('import-export for project-specific segments', () => {
|
|||||||
});
|
});
|
||||||
const strategy = {
|
const strategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
parameters: { rollout: '100', stickiness: 'default' },
|
parameters: {
|
||||||
|
rollout: '100',
|
||||||
|
stickiness: 'default',
|
||||||
|
},
|
||||||
constraints: [
|
constraints: [
|
||||||
{
|
{
|
||||||
contextName: 'appName',
|
contextName: 'appName',
|
||||||
@ -249,10 +261,16 @@ describe('import-export for project-specific segments', () => {
|
|||||||
test('exports features', async () => {
|
test('exports features', async () => {
|
||||||
const segmentName = 'my-segment';
|
const segmentName = 'my-segment';
|
||||||
await createProjects();
|
await createProjects();
|
||||||
const segment = await createSegment({ name: segmentName, constraints: [] });
|
const segment = await createSegment({
|
||||||
|
name: segmentName,
|
||||||
|
constraints: [],
|
||||||
|
});
|
||||||
const strategy = {
|
const strategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
parameters: { rollout: '100', stickiness: 'default' },
|
parameters: {
|
||||||
|
rollout: '100',
|
||||||
|
stickiness: 'default',
|
||||||
|
},
|
||||||
constraints: [
|
constraints: [
|
||||||
{
|
{
|
||||||
contextName: 'appName',
|
contextName: 'appName',
|
||||||
@ -276,6 +294,8 @@ test('exports features', async () => {
|
|||||||
},
|
},
|
||||||
strategy,
|
strategy,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await app.addDependency(defaultFeatureName, 'second_feature');
|
||||||
const { body } = await app.request
|
const { body } = await app.request
|
||||||
.post('/api/admin/features-batch/export')
|
.post('/api/admin/features-batch/export')
|
||||||
.send({
|
.send({
|
||||||
@ -306,6 +326,17 @@ test('exports features', async () => {
|
|||||||
name: segmentName,
|
name: segmentName,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
dependencies: [
|
||||||
|
{
|
||||||
|
feature: defaultFeatureName,
|
||||||
|
dependencies: [
|
||||||
|
{
|
||||||
|
feature: 'second_feature',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -313,7 +344,10 @@ test('exports features by tag', async () => {
|
|||||||
await createProjects();
|
await createProjects();
|
||||||
const strategy = {
|
const strategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
parameters: { rollout: '100', stickiness: 'default' },
|
parameters: {
|
||||||
|
rollout: '100',
|
||||||
|
stickiness: 'default',
|
||||||
|
},
|
||||||
constraints: [
|
constraints: [
|
||||||
{
|
{
|
||||||
contextName: 'appName',
|
contextName: 'appName',
|
||||||
@ -386,7 +420,10 @@ test('should export custom context fields from strategies and variants', async (
|
|||||||
await createContext(strategyStickinessContext);
|
await createContext(strategyStickinessContext);
|
||||||
const strategy = {
|
const strategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
parameters: { rollout: '100', stickiness: 'strategy-stickiness' },
|
parameters: {
|
||||||
|
rollout: '100',
|
||||||
|
stickiness: 'strategy-stickiness',
|
||||||
|
},
|
||||||
constraints: [
|
constraints: [
|
||||||
{
|
{
|
||||||
contextName: strategyContext.name,
|
contextName: strategyContext.name,
|
||||||
@ -502,7 +539,12 @@ test('should export tags', async () => {
|
|||||||
featureName: defaultFeatureName,
|
featureName: defaultFeatureName,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
featureTags: [{ featureName, tagValue: 'tag1' }],
|
featureTags: [
|
||||||
|
{
|
||||||
|
featureName,
|
||||||
|
tagValue: 'tag1',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -598,15 +640,33 @@ const tags = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const resultTags = [
|
const resultTags = [
|
||||||
{ value: 'tag1', type: 'simple' },
|
{
|
||||||
{ value: 'tag2', type: 'simple' },
|
value: 'tag1',
|
||||||
{ value: 'feature_tagged', type: 'special_tag' },
|
type: 'simple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tag2',
|
||||||
|
type: 'simple',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'feature_tagged',
|
||||||
|
type: 'special_tag',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const tagTypes = [
|
const tagTypes = [
|
||||||
{ name: 'bestt', description: 'test' },
|
{
|
||||||
{ name: 'special_tag', description: 'this is my special tag' },
|
name: 'bestt',
|
||||||
{ name: 'special_tag', description: 'this is my special tag' }, // deliberate duplicate
|
description: 'test',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'special_tag',
|
||||||
|
description: 'this is my special tag',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'special_tag',
|
||||||
|
description: 'this is my special tag',
|
||||||
|
}, // deliberate duplicate
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultImportPayload: ImportTogglesSchema = {
|
const defaultImportPayload: ImportTogglesSchema = {
|
||||||
@ -724,11 +784,21 @@ test('import multiple features with same tag', async () => {
|
|||||||
|
|
||||||
expect(tags1).toMatchObject({
|
expect(tags1).toMatchObject({
|
||||||
version: 1,
|
version: 1,
|
||||||
tags: [{ value: 'tag1', type: 'simple' }],
|
tags: [
|
||||||
|
{
|
||||||
|
value: 'tag1',
|
||||||
|
type: 'simple',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
expect(tags2).toMatchObject({
|
expect(tags2).toMatchObject({
|
||||||
version: 1,
|
version: 1,
|
||||||
tags: [{ value: 'tag1', type: 'simple' }],
|
tags: [
|
||||||
|
{
|
||||||
|
value: 'tag1',
|
||||||
|
type: 'simple',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -746,7 +816,12 @@ test('can update toggles on subsequent import', async () => {
|
|||||||
...defaultImportPayload,
|
...defaultImportPayload,
|
||||||
data: {
|
data: {
|
||||||
...defaultImportPayload.data,
|
...defaultImportPayload.data,
|
||||||
features: [{ ...exportedFeature, type: 'operational' }],
|
features: [
|
||||||
|
{
|
||||||
|
...exportedFeature,
|
||||||
|
type: 'operational',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -782,7 +857,12 @@ test('reject import with unknown context fields', async () => {
|
|||||||
await createProjects();
|
await createProjects();
|
||||||
const contextField = {
|
const contextField = {
|
||||||
name: 'ContextField1',
|
name: 'ContextField1',
|
||||||
legalValues: [{ value: 'Value1', description: '' }],
|
legalValues: [
|
||||||
|
{
|
||||||
|
value: 'Value1',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
await app.createContextField(contextField);
|
await app.createContextField(contextField);
|
||||||
const importPayloadWithContextFields: ImportTogglesSchema = {
|
const importPayloadWithContextFields: ImportTogglesSchema = {
|
||||||
@ -792,7 +872,12 @@ test('reject import with unknown context fields', async () => {
|
|||||||
contextFields: [
|
contextFields: [
|
||||||
{
|
{
|
||||||
...contextField,
|
...contextField,
|
||||||
legalValues: [{ value: 'Value2', description: '' }],
|
legalValues: [
|
||||||
|
{
|
||||||
|
value: 'Value2',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -813,7 +898,10 @@ test('reject import with unsupported strategies', async () => {
|
|||||||
data: {
|
data: {
|
||||||
...defaultImportPayload.data,
|
...defaultImportPayload.data,
|
||||||
featureStrategies: [
|
featureStrategies: [
|
||||||
{ name: 'customStrategy', featureName: 'featureName' },
|
{
|
||||||
|
name: 'customStrategy',
|
||||||
|
featureName: 'featureName',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -874,7 +962,12 @@ test('validate import data', async () => {
|
|||||||
anotherExportedFeature,
|
anotherExportedFeature,
|
||||||
],
|
],
|
||||||
featureStrategies: [{ name: 'customStrategy' }],
|
featureStrategies: [{ name: 'customStrategy' }],
|
||||||
segments: [{ id: 1, name: 'customSegment' }],
|
segments: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'customSegment',
|
||||||
|
},
|
||||||
|
],
|
||||||
contextFields: [
|
contextFields: [
|
||||||
{
|
{
|
||||||
...contextField,
|
...contextField,
|
||||||
|
@ -182,6 +182,7 @@ import { contextFieldStrategiesSchema } from './spec/context-field-strategies-sc
|
|||||||
import { advancedPlaygroundEnvironmentFeatureSchema } from './spec/advanced-playground-environment-feature-schema';
|
import { advancedPlaygroundEnvironmentFeatureSchema } from './spec/advanced-playground-environment-feature-schema';
|
||||||
import { createFeatureNamingPatternSchema } from './spec/create-feature-naming-pattern-schema';
|
import { createFeatureNamingPatternSchema } from './spec/create-feature-naming-pattern-schema';
|
||||||
import { segmentStrategiesSchema } from './spec/admin-strategies-schema';
|
import { segmentStrategiesSchema } from './spec/admin-strategies-schema';
|
||||||
|
import { featureDependenciesSchema } from './spec/feature-dependencies-schema';
|
||||||
|
|
||||||
// Schemas must have an $id property on the form "#/components/schemas/mySchema".
|
// Schemas must have an $id property on the form "#/components/schemas/mySchema".
|
||||||
export type SchemaId = typeof schemas[keyof typeof schemas]['$id'];
|
export type SchemaId = typeof schemas[keyof typeof schemas]['$id'];
|
||||||
@ -389,6 +390,7 @@ export const schemas: UnleashSchemas = {
|
|||||||
dependentFeatureSchema,
|
dependentFeatureSchema,
|
||||||
createDependentFeatureSchema,
|
createDependentFeatureSchema,
|
||||||
parentFeatureOptionsSchema,
|
parentFeatureOptionsSchema,
|
||||||
|
featureDependenciesSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
||||||
|
@ -12,6 +12,8 @@ import { variantsSchema } from './variants-schema';
|
|||||||
import { constraintSchema } from './constraint-schema';
|
import { constraintSchema } from './constraint-schema';
|
||||||
import { tagTypeSchema } from './tag-type-schema';
|
import { tagTypeSchema } from './tag-type-schema';
|
||||||
import { strategyVariantSchema } from './strategy-variant-schema';
|
import { strategyVariantSchema } from './strategy-variant-schema';
|
||||||
|
import { featureDependenciesSchema } from './feature-dependencies-schema';
|
||||||
|
import { dependentFeatureSchema } from './dependent-feature-schema';
|
||||||
|
|
||||||
export const exportResultSchema = {
|
export const exportResultSchema = {
|
||||||
$id: '#/components/schemas/exportResultSchema',
|
$id: '#/components/schemas/exportResultSchema',
|
||||||
@ -166,6 +168,14 @@ export const exportResultSchema = {
|
|||||||
$ref: '#/components/schemas/tagTypeSchema',
|
$ref: '#/components/schemas/tagTypeSchema',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dependencies: {
|
||||||
|
type: 'array',
|
||||||
|
description:
|
||||||
|
'A list of all the dependencies for features in `features` list.',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/featureDependenciesSchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
@ -182,6 +192,8 @@ export const exportResultSchema = {
|
|||||||
parametersSchema,
|
parametersSchema,
|
||||||
legalValueSchema,
|
legalValueSchema,
|
||||||
tagTypeSchema,
|
tagTypeSchema,
|
||||||
|
featureDependenciesSchema,
|
||||||
|
dependentFeatureSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
34
src/lib/openapi/spec/feature-dependencies-schema.ts
Normal file
34
src/lib/openapi/spec/feature-dependencies-schema.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
import { dependentFeatureSchema } from './dependent-feature-schema';
|
||||||
|
|
||||||
|
export const featureDependenciesSchema = {
|
||||||
|
$id: '#/components/schemas/featureDependenciesSchema',
|
||||||
|
type: 'object',
|
||||||
|
description:
|
||||||
|
'Feature dependency connection between a child feature and its dependencies',
|
||||||
|
required: ['feature', 'dependencies'],
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
feature: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'The name of the child feature.',
|
||||||
|
example: 'child_feature',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'List of parent features for the child feature',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/dependentFeatureSchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
dependentFeatureSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type FeatureDependenciesSchema = FromSchema<
|
||||||
|
typeof featureDependenciesSchema
|
||||||
|
>;
|
@ -146,6 +146,11 @@ export interface IDependency {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFeatureDependency {
|
||||||
|
feature: string;
|
||||||
|
dependency: IDependency;
|
||||||
|
}
|
||||||
|
|
||||||
export type IStrategyVariant = Omit<IVariant, 'overrides'>;
|
export type IStrategyVariant = Omit<IVariant, 'overrides'>;
|
||||||
|
|
||||||
export interface IEnvironment {
|
export interface IEnvironment {
|
||||||
|
Loading…
Reference in New Issue
Block a user