mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
Export features (#2905)
This commit is contained in:
parent
1a410a4ed5
commit
b895c99743
@ -108,6 +108,22 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAllByFeatures(
|
||||||
|
features: string[],
|
||||||
|
environment?: string,
|
||||||
|
): Promise<IFeatureEnvironment[]> {
|
||||||
|
let rows = this.db(T.featureEnvs).whereIn('feature_name', features);
|
||||||
|
if (environment) {
|
||||||
|
rows = rows.where({ environment });
|
||||||
|
}
|
||||||
|
return (await rows).map((r) => ({
|
||||||
|
enabled: r.enabled,
|
||||||
|
featureName: r.feature_name,
|
||||||
|
environment: r.environment,
|
||||||
|
variants: r.variants,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async disableEnvironmentIfNoStrategies(
|
async disableEnvironmentIfNoStrategies(
|
||||||
featureName: string,
|
featureName: string,
|
||||||
environment: string,
|
environment: string,
|
||||||
|
@ -16,6 +16,9 @@ export const exportQuerySchema = {
|
|||||||
environment: {
|
environment: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
downloadFile: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {},
|
schemas: {},
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { FromSchema } from 'json-schema-to-ts';
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
import { featureSchema } from './feature-schema';
|
import { featureSchema } from './feature-schema';
|
||||||
import { featureStrategySchema } from './feature-strategy-schema';
|
import { featureStrategySchema } from './feature-strategy-schema';
|
||||||
|
import { featureEnvironmentSchema } from './feature-environment-schema';
|
||||||
|
import { contextFieldSchema } from './context-field-schema';
|
||||||
|
import { featureTagSchema } from './feature-tag-schema';
|
||||||
|
|
||||||
export const exportResultSchema = {
|
export const exportResultSchema = {
|
||||||
$id: '#/components/schemas/exportResultSchema',
|
$id: '#/components/schemas/exportResultSchema',
|
||||||
@ -20,11 +23,32 @@ export const exportResultSchema = {
|
|||||||
$ref: '#/components/schemas/featureStrategySchema',
|
$ref: '#/components/schemas/featureStrategySchema',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
featureEnvironments: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/featureEnvironmentSchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contextFields: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/contextFieldSchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
featureTags: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/featureTagSchema',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
featureSchema,
|
featureSchema,
|
||||||
featureStrategySchema,
|
featureStrategySchema,
|
||||||
|
featureEnvironmentSchema,
|
||||||
|
contextFieldSchema,
|
||||||
|
featureTagSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Response } from 'express';
|
||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { NONE } from '../../types/permissions';
|
import { NONE } from '../../types/permissions';
|
||||||
import { IUnleashConfig } from '../../types/option';
|
import { IUnleashConfig } from '../../types/option';
|
||||||
@ -14,6 +14,8 @@ import { exportResultSchema } from '../../openapi/spec/export-result-schema';
|
|||||||
import { ExportQuerySchema } from '../../openapi/spec/export-query-schema';
|
import { ExportQuerySchema } from '../../openapi/spec/export-query-schema';
|
||||||
import { serializeDates } from '../../types';
|
import { serializeDates } from '../../types';
|
||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../unleash-types';
|
||||||
|
import { format as formatDate } from 'date-fns';
|
||||||
|
import { extractUsername } from '../../util';
|
||||||
|
|
||||||
class ExportImportController extends Controller {
|
class ExportImportController extends Controller {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -58,12 +60,13 @@ class ExportImportController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async export(
|
async export(
|
||||||
req: Request<unknown, unknown, ExportQuerySchema, unknown>,
|
req: IAuthRequest<unknown, unknown, ExportQuerySchema, unknown>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.verifyExportImportEnabled();
|
this.verifyExportImportEnabled();
|
||||||
const query = req.body;
|
const query = req.body;
|
||||||
const data = await this.exportImportService.export(query);
|
const userName = extractUsername(req);
|
||||||
|
const data = await this.exportImportService.export(query, userName);
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
200,
|
200,
|
||||||
@ -71,6 +74,17 @@ class ExportImportController extends Controller {
|
|||||||
exportResultSchema.$id,
|
exportResultSchema.$id,
|
||||||
serializeDates(data),
|
serializeDates(data),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const timestamp = this.getFormattedDate(Date.now());
|
||||||
|
if (query.downloadFile) {
|
||||||
|
res.attachment(`export-${timestamp}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormattedDate(millis: number): string {
|
||||||
|
return formatDate(millis, 'yyyy-MM-dd_HH-mm-ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
private verifyExportImportEnabled() {
|
private verifyExportImportEnabled() {
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
FeatureToggle,
|
FeatureToggle,
|
||||||
IFeatureEnvironment,
|
IFeatureEnvironment,
|
||||||
IFeatureStrategy,
|
IFeatureStrategy,
|
||||||
|
IFeatureStrategySegment,
|
||||||
ITag,
|
ITag,
|
||||||
} from '../types/model';
|
} from '../types/model';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
@ -16,18 +17,13 @@ import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
|||||||
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
||||||
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 { IContextFieldStore, IUnleashStores } from '../types/stores';
|
||||||
import { ISegmentStore } from '../types/stores/segment-store';
|
import { ISegmentStore } from '../types/stores/segment-store';
|
||||||
import { IFlagResolver, IUnleashServices } from 'lib/types';
|
|
||||||
import { IContextFieldDto } from '../types/stores/context-field-store';
|
import { IContextFieldDto } from '../types/stores/context-field-store';
|
||||||
import FeatureToggleService from './feature-toggle-service';
|
import FeatureToggleService from './feature-toggle-service';
|
||||||
import User from 'lib/types/user';
|
import User from 'lib/types/user';
|
||||||
import { ExportQuerySchema } from '../openapi/spec/export-query-schema';
|
import { ExportQuerySchema } from '../openapi/spec/export-query-schema';
|
||||||
|
import { FEATURES_EXPORTED, IFlagResolver, IUnleashServices } from '../types';
|
||||||
export interface IExportQuery {
|
|
||||||
features: string[];
|
|
||||||
environment: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IImportDTO {
|
export interface IImportDTO {
|
||||||
data: IExportData;
|
data: IExportData;
|
||||||
@ -38,7 +34,7 @@ export interface IImportDTO {
|
|||||||
export interface IExportData {
|
export interface IExportData {
|
||||||
features: FeatureToggle[];
|
features: FeatureToggle[];
|
||||||
tags?: ITag[];
|
tags?: ITag[];
|
||||||
contextFields?: IContextFieldDto[];
|
contextFields: IContextFieldDto[];
|
||||||
featureStrategies: IFeatureStrategy[];
|
featureStrategies: IFeatureStrategy[];
|
||||||
featureEnvironments: IFeatureEnvironment[];
|
featureEnvironments: IFeatureEnvironment[];
|
||||||
}
|
}
|
||||||
@ -72,6 +68,8 @@ export default class ExportImportService {
|
|||||||
|
|
||||||
private featureToggleService: FeatureToggleService;
|
private featureToggleService: FeatureToggleService;
|
||||||
|
|
||||||
|
private contextFieldStore: IContextFieldStore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
{
|
{
|
||||||
@ -95,24 +93,71 @@ export default class ExportImportService {
|
|||||||
this.segmentStore = stores.segmentStore;
|
this.segmentStore = stores.segmentStore;
|
||||||
this.flagResolver = flagResolver;
|
this.flagResolver = flagResolver;
|
||||||
this.featureToggleService = featureToggleService;
|
this.featureToggleService = featureToggleService;
|
||||||
|
this.contextFieldStore = stores.contextFieldStore;
|
||||||
this.logger = getLogger('services/state-service.js');
|
this.logger = getLogger('services/state-service.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
async export(query: ExportQuerySchema): Promise<IExportData> {
|
async export(
|
||||||
const [features, featureEnvironments, featureStrategies] =
|
query: ExportQuerySchema,
|
||||||
await Promise.all([
|
userName: string,
|
||||||
this.toggleStore.getAllByNames(query.features),
|
): Promise<IExportData> {
|
||||||
(
|
const [
|
||||||
await this.featureEnvironmentStore.getAll({
|
features,
|
||||||
environment: query.environment,
|
featureEnvironments,
|
||||||
})
|
featureStrategies,
|
||||||
).filter((item) => query.features.includes(item.featureName)),
|
strategySegments,
|
||||||
this.featureStrategiesStore.getAllByFeatures(
|
contextFields,
|
||||||
query.features,
|
featureTags,
|
||||||
query.environment,
|
] = await Promise.all([
|
||||||
|
this.toggleStore.getAllByNames(query.features),
|
||||||
|
await this.featureEnvironmentStore.getAllByFeatures(
|
||||||
|
query.features,
|
||||||
|
query.environment,
|
||||||
|
),
|
||||||
|
this.featureStrategiesStore.getAllByFeatures(
|
||||||
|
query.features,
|
||||||
|
query.environment,
|
||||||
|
),
|
||||||
|
this.segmentStore.getAllFeatureStrategySegments(),
|
||||||
|
this.contextFieldStore.getAll(),
|
||||||
|
this.featureTagStore.getAll(),
|
||||||
|
]);
|
||||||
|
this.addSegmentsToStrategies(featureStrategies, strategySegments);
|
||||||
|
const filteredContextFields = contextFields.filter((field) =>
|
||||||
|
featureStrategies.some((strategy) =>
|
||||||
|
strategy.constraints.some(
|
||||||
|
(constraint) => constraint.contextName === field.name,
|
||||||
),
|
),
|
||||||
]);
|
),
|
||||||
return { features, featureStrategies, featureEnvironments };
|
);
|
||||||
|
const result = {
|
||||||
|
features,
|
||||||
|
featureStrategies,
|
||||||
|
featureEnvironments,
|
||||||
|
contextFields: filteredContextFields,
|
||||||
|
featureTags,
|
||||||
|
};
|
||||||
|
await this.eventStore.store({
|
||||||
|
type: FEATURES_EXPORTED,
|
||||||
|
createdBy: userName,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
addSegmentsToStrategies(
|
||||||
|
featureStrategies: IFeatureStrategy[],
|
||||||
|
strategySegments: IFeatureStrategySegment[],
|
||||||
|
): void {
|
||||||
|
featureStrategies.forEach((featureStrategy) => {
|
||||||
|
featureStrategy.segments = strategySegments
|
||||||
|
.filter(
|
||||||
|
(segment) =>
|
||||||
|
segment.featureStrategyId === featureStrategy.id,
|
||||||
|
)
|
||||||
|
.map((segment) => segment.segmentId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async import(dto: IImportDTO, user: User): Promise<void> {
|
async import(dto: IImportDTO, user: User): Promise<void> {
|
||||||
|
@ -104,6 +104,8 @@ export const FEATURE_UNFAVORITED = 'feature-unfavorited';
|
|||||||
export const PROJECT_FAVORITED = 'project-favorited';
|
export const PROJECT_FAVORITED = 'project-favorited';
|
||||||
export const PROJECT_UNFAVORITED = 'project-unfavorited';
|
export const PROJECT_UNFAVORITED = 'project-unfavorited';
|
||||||
|
|
||||||
|
export const FEATURES_EXPORTED = 'features-exported';
|
||||||
|
|
||||||
export interface IBaseEvent {
|
export interface IBaseEvent {
|
||||||
type: string;
|
type: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
|
@ -15,6 +15,10 @@ export interface IFeatureEnvironmentStore
|
|||||||
getEnvironmentsForFeature(
|
getEnvironmentsForFeature(
|
||||||
featureName: string,
|
featureName: string,
|
||||||
): Promise<IFeatureEnvironment[]>;
|
): Promise<IFeatureEnvironment[]>;
|
||||||
|
getAllByFeatures(
|
||||||
|
features: string[],
|
||||||
|
environment?: string,
|
||||||
|
): Promise<IFeatureEnvironment[]>;
|
||||||
isEnvironmentEnabled(
|
isEnvironmentEnabled(
|
||||||
featureName: string,
|
featureName: string,
|
||||||
environment: string,
|
environment: string,
|
||||||
|
@ -12,10 +12,12 @@ import {
|
|||||||
IFeatureStrategy,
|
IFeatureStrategy,
|
||||||
IFeatureToggleStore,
|
IFeatureToggleStore,
|
||||||
IProjectStore,
|
IProjectStore,
|
||||||
|
ISegment,
|
||||||
IStrategyConfig,
|
IStrategyConfig,
|
||||||
} from 'lib/types';
|
} from 'lib/types';
|
||||||
import { DEFAULT_ENV } from '../../../../lib/util';
|
import { DEFAULT_ENV } from '../../../../lib/util';
|
||||||
import { IImportDTO } from '../../../../lib/services/export-import-service';
|
import { IImportDTO } from '../../../../lib/services/export-import-service';
|
||||||
|
import { ContextFieldSchema } from '../../../../lib/openapi';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -30,9 +32,19 @@ const defaultStrategy: IStrategyConfig = {
|
|||||||
constraints: [],
|
constraints: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultContext: ContextFieldSchema = {
|
||||||
|
name: 'region',
|
||||||
|
description: 'A region',
|
||||||
|
legalValues: [
|
||||||
|
{ value: 'north' },
|
||||||
|
{ value: 'south', description: 'south-desc' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const createToggle = async (
|
const createToggle = async (
|
||||||
toggle: FeatureToggleDTO,
|
toggle: FeatureToggleDTO,
|
||||||
strategy: Omit<IStrategyConfig, 'id'> = defaultStrategy,
|
strategy: Omit<IStrategyConfig, 'id'> = defaultStrategy,
|
||||||
|
tags: string[] = [],
|
||||||
projectId: string = 'default',
|
projectId: string = 'default',
|
||||||
username: string = 'test',
|
username: string = 'test',
|
||||||
) => {
|
) => {
|
||||||
@ -48,6 +60,26 @@ const createToggle = async (
|
|||||||
username,
|
username,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
await Promise.all(
|
||||||
|
tags.map(async (tag) => {
|
||||||
|
return app.services.featureTagService.addTag(
|
||||||
|
toggle.name,
|
||||||
|
{
|
||||||
|
type: 'simple',
|
||||||
|
value: tag,
|
||||||
|
},
|
||||||
|
username,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createContext = async (context: ContextFieldSchema = defaultContext) => {
|
||||||
|
await app.request
|
||||||
|
.post('/api/admin/context')
|
||||||
|
.send(context)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(201);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createProject = async (project: string, environment: string) => {
|
const createProject = async (project: string, environment: string) => {
|
||||||
@ -68,6 +100,12 @@ const createProject = async (project: string, environment: string) => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createSegment = (postData: object): Promise<ISegment> => {
|
||||||
|
return app.services.segmentService.create(postData, {
|
||||||
|
email: 'test@example.com',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('export_import_api_serial', getLogger);
|
db = await dbInit('export_import_api_serial', getLogger);
|
||||||
app = await setupAppWithCustomConfig(db.stores, {
|
app = await setupAppWithCustomConfig(db.stores, {
|
||||||
@ -97,6 +135,7 @@ afterAll(async () => {
|
|||||||
|
|
||||||
test('exports features', async () => {
|
test('exports features', async () => {
|
||||||
await createProject('default', 'default');
|
await createProject('default', 'default');
|
||||||
|
const segment = await createSegment({ name: 'S3', constraints: [] });
|
||||||
const strategy = {
|
const strategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
parameters: { rollout: '100', stickiness: 'default' },
|
parameters: { rollout: '100', stickiness: 'default' },
|
||||||
@ -107,6 +146,7 @@ test('exports features', async () => {
|
|||||||
operator: 'IN' as const,
|
operator: 'IN' as const,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
segments: [segment.id],
|
||||||
};
|
};
|
||||||
await createToggle(
|
await createToggle(
|
||||||
{
|
{
|
||||||
@ -150,6 +190,106 @@ test('exports features', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should export custom context fields', async () => {
|
||||||
|
await createProject('default', 'default');
|
||||||
|
const context = {
|
||||||
|
name: 'test-export',
|
||||||
|
legalValues: [
|
||||||
|
{ value: 'estonia' },
|
||||||
|
{ value: 'norway' },
|
||||||
|
{ value: 'poland' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await createContext(context);
|
||||||
|
const strategy = {
|
||||||
|
name: 'default',
|
||||||
|
parameters: { rollout: '100', stickiness: 'default' },
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
contextName: context.name,
|
||||||
|
values: ['estonia', 'norway'],
|
||||||
|
operator: 'IN' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await createToggle(
|
||||||
|
{
|
||||||
|
name: 'first_feature',
|
||||||
|
description: 'the #1 feature',
|
||||||
|
},
|
||||||
|
strategy,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { body } = await app.request
|
||||||
|
.post('/api/admin/features-batch/export')
|
||||||
|
.send({
|
||||||
|
features: ['first_feature'],
|
||||||
|
environment: 'default',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const { name, ...resultStrategy } = strategy;
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
name: 'first_feature',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
featureStrategies: [resultStrategy],
|
||||||
|
featureEnvironments: [
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
environment: 'default',
|
||||||
|
featureName: 'first_feature',
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
contextFields: [context],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should export tags', async () => {
|
||||||
|
const featureName = 'first_feature';
|
||||||
|
await createProject('default', 'default');
|
||||||
|
await createToggle(
|
||||||
|
{
|
||||||
|
name: featureName,
|
||||||
|
description: 'the #1 feature',
|
||||||
|
},
|
||||||
|
defaultStrategy,
|
||||||
|
['tag1'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { body } = await app.request
|
||||||
|
.post('/api/admin/features-batch/export')
|
||||||
|
.send({
|
||||||
|
features: ['first_feature'],
|
||||||
|
environment: 'default',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const { name, ...resultStrategy } = defaultStrategy;
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
name: 'first_feature',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
featureStrategies: [resultStrategy],
|
||||||
|
featureEnvironments: [
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
environment: 'default',
|
||||||
|
featureName: 'first_feature',
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
featureTags: [{ featureName, tagValue: 'tag1' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('returns all features, when no feature was defined', async () => {
|
test('returns all features, when no feature was defined', async () => {
|
||||||
await createProject('default', 'default');
|
await createProject('default', 'default');
|
||||||
await createToggle({
|
await createToggle({
|
||||||
@ -225,6 +365,7 @@ test('import features to existing project and environment', async () => {
|
|||||||
environment: 'irrelevant',
|
environment: 'irrelevant',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
contextFields: [],
|
||||||
},
|
},
|
||||||
project: project,
|
project: project,
|
||||||
environment: environment,
|
environment: environment,
|
||||||
|
@ -1037,6 +1037,9 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
"exportQuerySchema": {
|
"exportQuerySchema": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"downloadFile": {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
"environment": {
|
"environment": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
@ -1057,12 +1060,30 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
"exportResultSchema": {
|
"exportResultSchema": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"contextFields": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/contextFieldSchema",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
|
"featureEnvironments": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/featureEnvironmentSchema",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
"featureStrategies": {
|
"featureStrategies": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/featureStrategySchema",
|
"$ref": "#/components/schemas/featureStrategySchema",
|
||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
|
"featureTags": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/featureTagSchema",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/featureSchema",
|
"$ref": "#/components/schemas/featureSchema",
|
||||||
|
@ -218,4 +218,13 @@ export default class FakeFeatureEnvironmentStore
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllByFeatures(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
features: string[],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
environment?: string,
|
||||||
|
): Promise<IFeatureEnvironment[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user