mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Permissions update import (#3141)
This commit is contained in:
		
							parent
							
								
									350b55644a
								
							
						
					
					
						commit
						996fb1c104
					
				@ -0,0 +1,346 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					    IUnleashTest,
 | 
				
			||||||
 | 
					    setupAppWithAuth,
 | 
				
			||||||
 | 
					} from '../../test/e2e/helpers/test-helper';
 | 
				
			||||||
 | 
					import dbInit, { ITestDb } from '../../test/e2e/helpers/database-init';
 | 
				
			||||||
 | 
					import getLogger from '../../test/fixtures/no-logger';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    DEFAULT_PROJECT,
 | 
				
			||||||
 | 
					    IEnvironmentStore,
 | 
				
			||||||
 | 
					    IEventStore,
 | 
				
			||||||
 | 
					    IFeatureToggleStore,
 | 
				
			||||||
 | 
					    IProjectStore,
 | 
				
			||||||
 | 
					    IUnleashStores,
 | 
				
			||||||
 | 
					    RoleName,
 | 
				
			||||||
 | 
					} from '../types';
 | 
				
			||||||
 | 
					import { ImportTogglesSchema, VariantsSchema } from '../openapi';
 | 
				
			||||||
 | 
					import { IContextFieldDto } from '../types/stores/context-field-store';
 | 
				
			||||||
 | 
					import { AccessService } from '../services';
 | 
				
			||||||
 | 
					import { DEFAULT_ENV } from '../util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let app: IUnleashTest;
 | 
				
			||||||
 | 
					let db: ITestDb;
 | 
				
			||||||
 | 
					let eventStore: IEventStore;
 | 
				
			||||||
 | 
					let environmentStore: IEnvironmentStore;
 | 
				
			||||||
 | 
					let projectStore: IProjectStore;
 | 
				
			||||||
 | 
					let toggleStore: IFeatureToggleStore;
 | 
				
			||||||
 | 
					let accessService: AccessService;
 | 
				
			||||||
 | 
					let adminRole;
 | 
				
			||||||
 | 
					let stores: IUnleashStores;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const regularUserName = 'import-user';
 | 
				
			||||||
 | 
					const adminUserName = 'admin-user';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const validateImport = (importPayload: ImportTogglesSchema, status = 200) =>
 | 
				
			||||||
 | 
					    app.request
 | 
				
			||||||
 | 
					        .post('/api/admin/features-batch/full-validate')
 | 
				
			||||||
 | 
					        .send(importPayload)
 | 
				
			||||||
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
 | 
					        .expect(status);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createContextField = async (contextField: IContextFieldDto) => {
 | 
				
			||||||
 | 
					    await app.request.post(`/api/admin/context`).send(contextField).expect(201);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createFeature = async (featureName: string) => {
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .post(`/api/admin/projects/${DEFAULT_PROJECT}/features`)
 | 
				
			||||||
 | 
					        .send({
 | 
				
			||||||
 | 
					            name: featureName,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
 | 
					        .expect(201);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createFeatureToggleWithStrategy = async (featureName: string) => {
 | 
				
			||||||
 | 
					    await createFeature(featureName);
 | 
				
			||||||
 | 
					    return app.request
 | 
				
			||||||
 | 
					        .post(
 | 
				
			||||||
 | 
					            `/api/admin/projects/${DEFAULT_PROJECT}/features/${featureName}/environments/${DEFAULT_ENV}/strategies`,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .send({
 | 
				
			||||||
 | 
					            name: 'default',
 | 
				
			||||||
 | 
					            parameters: {
 | 
				
			||||||
 | 
					                userId: 'string',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const archiveFeature = async (featureName: string) => {
 | 
				
			||||||
 | 
					    await app.request
 | 
				
			||||||
 | 
					        .delete(
 | 
				
			||||||
 | 
					            `/api/admin/projects/${DEFAULT_PROJECT}/features/${featureName}`,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
 | 
					        .expect(202);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createProject = async () => {
 | 
				
			||||||
 | 
					    await db.stores.environmentStore.create({
 | 
				
			||||||
 | 
					        name: DEFAULT_ENV,
 | 
				
			||||||
 | 
					        type: 'production',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    await db.stores.projectStore.create({
 | 
				
			||||||
 | 
					        name: DEFAULT_PROJECT,
 | 
				
			||||||
 | 
					        description: '',
 | 
				
			||||||
 | 
					        id: DEFAULT_PROJECT,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const newFeature = 'new_feature';
 | 
				
			||||||
 | 
					const archivedFeature = 'archived_feature';
 | 
				
			||||||
 | 
					const existingFeature = 'existing_feature';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const variants: VariantsSchema = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        name: 'variantA',
 | 
				
			||||||
 | 
					        weight: 500,
 | 
				
			||||||
 | 
					        payload: {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            value: 'payloadA',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        overrides: [],
 | 
				
			||||||
 | 
					        stickiness: 'default',
 | 
				
			||||||
 | 
					        weightType: 'variable',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        name: 'variantB',
 | 
				
			||||||
 | 
					        weight: 500,
 | 
				
			||||||
 | 
					        payload: {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            value: 'payloadB',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        overrides: [],
 | 
				
			||||||
 | 
					        stickiness: 'default',
 | 
				
			||||||
 | 
					        weightType: 'variable',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					const feature1: ImportTogglesSchema['data']['features'][0] = {
 | 
				
			||||||
 | 
					    project: 'old_project',
 | 
				
			||||||
 | 
					    name: archivedFeature,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const feature2: ImportTogglesSchema['data']['features'][0] = {
 | 
				
			||||||
 | 
					    project: 'old_project',
 | 
				
			||||||
 | 
					    name: newFeature,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const feature3: ImportTogglesSchema['data']['features'][0] = {
 | 
				
			||||||
 | 
					    project: 'old_project',
 | 
				
			||||||
 | 
					    name: existingFeature,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					const constraints: ImportTogglesSchema['data']['featureStrategies'][0]['constraints'] =
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            values: ['conduit'],
 | 
				
			||||||
 | 
					            inverted: false,
 | 
				
			||||||
 | 
					            operator: 'IN',
 | 
				
			||||||
 | 
					            contextName: 'appName',
 | 
				
			||||||
 | 
					            caseInsensitive: false,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					const exportedStrategy: ImportTogglesSchema['data']['featureStrategies'][0] = {
 | 
				
			||||||
 | 
					    featureName: newFeature,
 | 
				
			||||||
 | 
					    id: '798cb25a-2abd-47bd-8a95-40ec13472309',
 | 
				
			||||||
 | 
					    name: 'default',
 | 
				
			||||||
 | 
					    parameters: {},
 | 
				
			||||||
 | 
					    constraints,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tags = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        featureName: newFeature,
 | 
				
			||||||
 | 
					        tagType: 'simple',
 | 
				
			||||||
 | 
					        tagValue: 'tag1',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        featureName: newFeature,
 | 
				
			||||||
 | 
					        tagType: 'simple',
 | 
				
			||||||
 | 
					        tagValue: 'tag2',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        featureName: newFeature,
 | 
				
			||||||
 | 
					        tagType: 'special_tag',
 | 
				
			||||||
 | 
					        tagValue: 'feature_tagged',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const tagTypes = [
 | 
				
			||||||
 | 
					    { name: 'bestt', description: 'test' },
 | 
				
			||||||
 | 
					    { name: 'special_tag', description: 'this is my special tag' },
 | 
				
			||||||
 | 
					    { name: 'special_tag', description: 'this is my special tag' }, // deliberate duplicate
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const importPayload: ImportTogglesSchema = {
 | 
				
			||||||
 | 
					    data: {
 | 
				
			||||||
 | 
					        features: [feature1, feature2, feature3],
 | 
				
			||||||
 | 
					        featureStrategies: [exportedStrategy],
 | 
				
			||||||
 | 
					        featureEnvironments: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                enabled: true,
 | 
				
			||||||
 | 
					                environment: 'irrelevant',
 | 
				
			||||||
 | 
					                featureName: newFeature,
 | 
				
			||||||
 | 
					                name: newFeature,
 | 
				
			||||||
 | 
					                variants,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        featureTags: tags,
 | 
				
			||||||
 | 
					        tagTypes,
 | 
				
			||||||
 | 
					        contextFields: [],
 | 
				
			||||||
 | 
					        segments: [],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    project: DEFAULT_PROJECT,
 | 
				
			||||||
 | 
					    environment: DEFAULT_ENV,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createUserEditorAccess = async (name, email) => {
 | 
				
			||||||
 | 
					    const { userStore } = stores;
 | 
				
			||||||
 | 
					    const user = await userStore.insert({ name, email });
 | 
				
			||||||
 | 
					    return user;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createUserAdminAccess = async (name, email) => {
 | 
				
			||||||
 | 
					    const { userStore } = stores;
 | 
				
			||||||
 | 
					    const user = await userStore.insert({ name, email });
 | 
				
			||||||
 | 
					    await accessService.addUserToRole(user.id, adminRole.id, 'default');
 | 
				
			||||||
 | 
					    return user;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loginRegularUser = () =>
 | 
				
			||||||
 | 
					    app.request
 | 
				
			||||||
 | 
					        .post(`/auth/demo/login`)
 | 
				
			||||||
 | 
					        .send({
 | 
				
			||||||
 | 
					            email: `${regularUserName}@getunleash.io`,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loginAdminUser = () =>
 | 
				
			||||||
 | 
					    app.request
 | 
				
			||||||
 | 
					        .post(`/auth/demo/login`)
 | 
				
			||||||
 | 
					        .send({
 | 
				
			||||||
 | 
					            email: `${adminUserName}@getunleash.io`,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					beforeAll(async () => {
 | 
				
			||||||
 | 
					    db = await dbInit('export_import_permissions_api_serial', getLogger);
 | 
				
			||||||
 | 
					    stores = db.stores;
 | 
				
			||||||
 | 
					    app = await setupAppWithAuth(
 | 
				
			||||||
 | 
					        db.stores,
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            experimental: {
 | 
				
			||||||
 | 
					                flags: {
 | 
				
			||||||
 | 
					                    featuresExportImport: true,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        db.rawDatabase,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    eventStore = db.stores.eventStore;
 | 
				
			||||||
 | 
					    environmentStore = db.stores.environmentStore;
 | 
				
			||||||
 | 
					    projectStore = db.stores.projectStore;
 | 
				
			||||||
 | 
					    toggleStore = db.stores.featureToggleStore;
 | 
				
			||||||
 | 
					    accessService = app.services.accessService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const roles = await accessService.getRootRoles();
 | 
				
			||||||
 | 
					    adminRole = roles.find((role) => role.name === RoleName.ADMIN);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await createUserEditorAccess(
 | 
				
			||||||
 | 
					        regularUserName,
 | 
				
			||||||
 | 
					        `${regularUserName}@getunleash.io`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    await createUserAdminAccess(
 | 
				
			||||||
 | 
					        adminUserName,
 | 
				
			||||||
 | 
					        `${adminUserName}@getunleash.io`,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					beforeEach(async () => {
 | 
				
			||||||
 | 
					    await eventStore.deleteAll();
 | 
				
			||||||
 | 
					    await toggleStore.deleteAll();
 | 
				
			||||||
 | 
					    await projectStore.deleteAll();
 | 
				
			||||||
 | 
					    await environmentStore.deleteAll();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					afterAll(async () => {
 | 
				
			||||||
 | 
					    await app.destroy();
 | 
				
			||||||
 | 
					    await db.destroy();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('validate import data', async () => {
 | 
				
			||||||
 | 
					    await loginAdminUser();
 | 
				
			||||||
 | 
					    await createProject();
 | 
				
			||||||
 | 
					    const contextField: IContextFieldDto = {
 | 
				
			||||||
 | 
					        name: 'validate_context_field',
 | 
				
			||||||
 | 
					        legalValues: [{ value: 'Value1' }],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const createdContextField: IContextFieldDto = {
 | 
				
			||||||
 | 
					        name: 'created_context_field',
 | 
				
			||||||
 | 
					        legalValues: [{ value: 'new_value' }],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await createFeature(archivedFeature);
 | 
				
			||||||
 | 
					    await archiveFeature(archivedFeature);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await createFeatureToggleWithStrategy(existingFeature);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await createContextField(contextField);
 | 
				
			||||||
 | 
					    const importPayloadWithContextFields: ImportTogglesSchema = {
 | 
				
			||||||
 | 
					        ...importPayload,
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					            ...importPayload.data,
 | 
				
			||||||
 | 
					            featureStrategies: [{ name: 'customStrategy' }],
 | 
				
			||||||
 | 
					            segments: [{ id: 1, name: 'customSegment' }],
 | 
				
			||||||
 | 
					            contextFields: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    ...contextField,
 | 
				
			||||||
 | 
					                    legalValues: [{ value: 'Value2' }],
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                createdContextField,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await loginRegularUser();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { body } = await validateImport(importPayloadWithContextFields, 200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(body).toMatchObject({
 | 
				
			||||||
 | 
					        errors: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                message:
 | 
				
			||||||
 | 
					                    'We detected the following custom strategy in the import file that needs to be created first:',
 | 
				
			||||||
 | 
					                affectedItems: ['customStrategy'],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                message:
 | 
				
			||||||
 | 
					                    'We detected the following context fields that do not have matching legal values with the imported ones:',
 | 
				
			||||||
 | 
					                affectedItems: [contextField.name],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        warnings: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                message:
 | 
				
			||||||
 | 
					                    'The following features will not be imported as they are currently archived. To import them, please unarchive them first:',
 | 
				
			||||||
 | 
					                affectedItems: [archivedFeature],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        permissions: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                message:
 | 
				
			||||||
 | 
					                    'We detected you are missing the following permissions:',
 | 
				
			||||||
 | 
					                affectedItems: [
 | 
				
			||||||
 | 
					                    'Create feature toggles',
 | 
				
			||||||
 | 
					                    'Update feature toggles',
 | 
				
			||||||
 | 
					                    'Update tag types',
 | 
				
			||||||
 | 
					                    'Create context fields',
 | 
				
			||||||
 | 
					                    'Create activation strategies',
 | 
				
			||||||
 | 
					                    'Delete activation strategies',
 | 
				
			||||||
 | 
					                    'Update variants on environment',
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -28,6 +28,7 @@ import {
 | 
				
			|||||||
    UPDATE_FEATURE,
 | 
					    UPDATE_FEATURE,
 | 
				
			||||||
    UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
					    UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
				
			||||||
    UPDATE_TAG_TYPE,
 | 
					    UPDATE_TAG_TYPE,
 | 
				
			||||||
 | 
					    WithRequired,
 | 
				
			||||||
} from '../types';
 | 
					} from '../types';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    ExportResultSchema,
 | 
					    ExportResultSchema,
 | 
				
			||||||
@ -162,8 +163,8 @@ export default class ExportImportService {
 | 
				
			|||||||
        const errors = this.compileErrors(
 | 
					        const errors = this.compileErrors(
 | 
				
			||||||
            dto.project,
 | 
					            dto.project,
 | 
				
			||||||
            unsupportedStrategies,
 | 
					            unsupportedStrategies,
 | 
				
			||||||
            unsupportedContextFields,
 | 
					 | 
				
			||||||
            otherProjectFeatures,
 | 
					            otherProjectFeatures,
 | 
				
			||||||
 | 
					            unsupportedContextFields,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        const warnings = this.compileWarnings(
 | 
					        const warnings = this.compileWarnings(
 | 
				
			||||||
            usedCustomStrategies,
 | 
					            usedCustomStrategies,
 | 
				
			||||||
@ -224,24 +225,32 @@ export default class ExportImportService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async importStrategies(dto: ImportTogglesSchema, user: User) {
 | 
					    private async importStrategies(dto: ImportTogglesSchema, user: User) {
 | 
				
			||||||
 | 
					        const hasFeatureName = (
 | 
				
			||||||
 | 
					            featureStrategy: FeatureStrategySchema,
 | 
				
			||||||
 | 
					        ): featureStrategy is WithRequired<
 | 
				
			||||||
 | 
					            FeatureStrategySchema,
 | 
				
			||||||
 | 
					            'featureName'
 | 
				
			||||||
 | 
					        > => Boolean(featureStrategy.featureName);
 | 
				
			||||||
        await Promise.all(
 | 
					        await Promise.all(
 | 
				
			||||||
            dto.data.featureStrategies?.map((featureStrategy) =>
 | 
					            dto.data.featureStrategies
 | 
				
			||||||
                this.featureToggleService.createStrategy(
 | 
					                ?.filter(hasFeatureName)
 | 
				
			||||||
                    {
 | 
					                .map((featureStrategy) =>
 | 
				
			||||||
                        name: featureStrategy.name,
 | 
					                    this.featureToggleService.createStrategy(
 | 
				
			||||||
                        constraints: featureStrategy.constraints,
 | 
					                        {
 | 
				
			||||||
                        parameters: featureStrategy.parameters,
 | 
					                            name: featureStrategy.name,
 | 
				
			||||||
                        segments: featureStrategy.segments,
 | 
					                            constraints: featureStrategy.constraints,
 | 
				
			||||||
                        sortOrder: featureStrategy.sortOrder,
 | 
					                            parameters: featureStrategy.parameters,
 | 
				
			||||||
                    },
 | 
					                            segments: featureStrategy.segments,
 | 
				
			||||||
                    {
 | 
					                            sortOrder: featureStrategy.sortOrder,
 | 
				
			||||||
                        featureName: featureStrategy.featureName,
 | 
					                        },
 | 
				
			||||||
                        environment: dto.environment,
 | 
					                        {
 | 
				
			||||||
                        projectId: dto.project,
 | 
					                            featureName: featureStrategy.featureName,
 | 
				
			||||||
                    },
 | 
					                            environment: dto.environment,
 | 
				
			||||||
                    extractUsernameFromUser(user),
 | 
					                            projectId: dto.project,
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        extractUsernameFromUser(user),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -268,7 +277,7 @@ export default class ExportImportService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async importContextFields(dto: ImportTogglesSchema, user: User) {
 | 
					    private async importContextFields(dto: ImportTogglesSchema, user: User) {
 | 
				
			||||||
        const newContextFields = await this.getNewContextFields(dto);
 | 
					        const newContextFields = (await this.getNewContextFields(dto)) || [];
 | 
				
			||||||
        await Promise.all(
 | 
					        await Promise.all(
 | 
				
			||||||
            newContextFields.map((contextField) =>
 | 
					            newContextFields.map((contextField) =>
 | 
				
			||||||
                this.contextService.createContextField(
 | 
					                this.contextService.createContextField(
 | 
				
			||||||
@ -297,9 +306,12 @@ export default class ExportImportService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async importToggleVariants(dto: ImportTogglesSchema, user: User) {
 | 
					    private async importToggleVariants(dto: ImportTogglesSchema, user: User) {
 | 
				
			||||||
        const featureEnvsWithVariants = dto.data.featureEnvironments?.filter(
 | 
					        const featureEnvsWithVariants =
 | 
				
			||||||
            (featureEnvironment) => featureEnvironment.variants?.length > 0,
 | 
					            dto.data.featureEnvironments?.filter(
 | 
				
			||||||
        );
 | 
					                (featureEnvironment) =>
 | 
				
			||||||
 | 
					                    Array.isArray(featureEnvironment.variants) &&
 | 
				
			||||||
 | 
					                    featureEnvironment.variants.length > 0,
 | 
				
			||||||
 | 
					            ) || [];
 | 
				
			||||||
        await Promise.all(
 | 
					        await Promise.all(
 | 
				
			||||||
            featureEnvsWithVariants.map((featureEnvironment) =>
 | 
					            featureEnvsWithVariants.map((featureEnvironment) =>
 | 
				
			||||||
                this.featureToggleService.saveVariantsOnEnv(
 | 
					                this.featureToggleService.saveVariantsOnEnv(
 | 
				
			||||||
@ -335,7 +347,10 @@ export default class ExportImportService {
 | 
				
			|||||||
        const unsupportedContextFields = await this.getUnsupportedContextFields(
 | 
					        const unsupportedContextFields = await this.getUnsupportedContextFields(
 | 
				
			||||||
            dto,
 | 
					            dto,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        if (unsupportedContextFields.length > 0) {
 | 
					        if (
 | 
				
			||||||
 | 
					            Array.isArray(unsupportedContextFields) &&
 | 
				
			||||||
 | 
					            unsupportedContextFields.length > 0
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
            throw new BadDataError(
 | 
					            throw new BadDataError(
 | 
				
			||||||
                `Context fields with errors: ${unsupportedContextFields
 | 
					                `Context fields with errors: ${unsupportedContextFields
 | 
				
			||||||
                    .map((field) => field.name)
 | 
					                    .map((field) => field.name)
 | 
				
			||||||
@ -387,9 +402,10 @@ export default class ExportImportService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private async removeArchivedFeatures(dto: ImportTogglesSchema) {
 | 
					    private async removeArchivedFeatures(dto: ImportTogglesSchema) {
 | 
				
			||||||
        const archivedFeatures = await this.getArchivedFeatures(dto);
 | 
					        const archivedFeatures = await this.getArchivedFeatures(dto);
 | 
				
			||||||
        const featureTags = dto.data.featureTags.filter(
 | 
					        const featureTags =
 | 
				
			||||||
            (tag) => !archivedFeatures.includes(tag.featureName),
 | 
					            dto.data.featureTags?.filter(
 | 
				
			||||||
        );
 | 
					                (tag) => !archivedFeatures.includes(tag.featureName),
 | 
				
			||||||
 | 
					            ) || [];
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            ...dto,
 | 
					            ...dto,
 | 
				
			||||||
            data: {
 | 
					            data: {
 | 
				
			||||||
@ -397,12 +413,14 @@ export default class ExportImportService {
 | 
				
			|||||||
                features: dto.data.features.filter(
 | 
					                features: dto.data.features.filter(
 | 
				
			||||||
                    (feature) => !archivedFeatures.includes(feature.name),
 | 
					                    (feature) => !archivedFeatures.includes(feature.name),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                featureEnvironments: dto.data.featureEnvironments.filter(
 | 
					                featureEnvironments: dto.data.featureEnvironments?.filter(
 | 
				
			||||||
                    (environment) =>
 | 
					                    (environment) =>
 | 
				
			||||||
 | 
					                        environment.featureName &&
 | 
				
			||||||
                        !archivedFeatures.includes(environment.featureName),
 | 
					                        !archivedFeatures.includes(environment.featureName),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                featureStrategies: dto.data.featureStrategies.filter(
 | 
					                featureStrategies: dto.data.featureStrategies.filter(
 | 
				
			||||||
                    (strategy) =>
 | 
					                    (strategy) =>
 | 
				
			||||||
 | 
					                        strategy.featureName &&
 | 
				
			||||||
                        !archivedFeatures.includes(strategy.featureName),
 | 
					                        !archivedFeatures.includes(strategy.featureName),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                featureTags,
 | 
					                featureTags,
 | 
				
			||||||
@ -429,8 +447,8 @@ export default class ExportImportService {
 | 
				
			|||||||
    private compileErrors(
 | 
					    private compileErrors(
 | 
				
			||||||
        projectName: string,
 | 
					        projectName: string,
 | 
				
			||||||
        strategies: FeatureStrategySchema[],
 | 
					        strategies: FeatureStrategySchema[],
 | 
				
			||||||
        contextFields: IContextFieldDto[],
 | 
					 | 
				
			||||||
        otherProjectFeatures: string[],
 | 
					        otherProjectFeatures: string[],
 | 
				
			||||||
 | 
					        contextFields?: IContextFieldDto[],
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        const errors: ImportTogglesValidateItemSchema[] = [];
 | 
					        const errors: ImportTogglesValidateItemSchema[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -441,7 +459,7 @@ export default class ExportImportService {
 | 
				
			|||||||
                affectedItems: strategies.map((strategy) => strategy.name),
 | 
					                affectedItems: strategies.map((strategy) => strategy.name),
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (contextFields.length > 0) {
 | 
					        if (Array.isArray(contextFields) && contextFields.length > 0) {
 | 
				
			||||||
            errors.push({
 | 
					            errors.push({
 | 
				
			||||||
                message:
 | 
					                message:
 | 
				
			||||||
                    'We detected the following context fields that do not have matching legal values with the imported ones:',
 | 
					                    'We detected the following context fields that do not have matching legal values with the imported ones:',
 | 
				
			||||||
@ -558,25 +576,52 @@ export default class ExportImportService {
 | 
				
			|||||||
        dto: ImportTogglesSchema,
 | 
					        dto: ImportTogglesSchema,
 | 
				
			||||||
        user: User,
 | 
					        user: User,
 | 
				
			||||||
    ): Promise<string[]> {
 | 
					    ): Promise<string[]> {
 | 
				
			||||||
        const requiredImportPermission = [
 | 
					 | 
				
			||||||
            CREATE_FEATURE,
 | 
					 | 
				
			||||||
            UPDATE_FEATURE,
 | 
					 | 
				
			||||||
            DELETE_FEATURE_STRATEGY,
 | 
					 | 
				
			||||||
            CREATE_FEATURE_STRATEGY,
 | 
					 | 
				
			||||||
            UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
        const [newTagTypes, newContextFields] = await Promise.all([
 | 
					        const [newTagTypes, newContextFields] = await Promise.all([
 | 
				
			||||||
            this.getNewTagTypes(dto),
 | 
					            this.getNewTagTypes(dto),
 | 
				
			||||||
            this.getNewContextFields(dto),
 | 
					            this.getNewContextFields(dto),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        const permissions = [...requiredImportPermission];
 | 
					        const permissions = [UPDATE_FEATURE];
 | 
				
			||||||
        if (newTagTypes.length > 0) {
 | 
					        if (newTagTypes.length > 0) {
 | 
				
			||||||
            permissions.push(UPDATE_TAG_TYPE);
 | 
					            permissions.push(UPDATE_TAG_TYPE);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (newContextFields.length > 0) {
 | 
					        if (Array.isArray(newContextFields) && newContextFields.length > 0) {
 | 
				
			||||||
            permissions.push(CREATE_CONTEXT_FIELD);
 | 
					            permissions.push(CREATE_CONTEXT_FIELD);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const strategiesExistForFeatures =
 | 
				
			||||||
 | 
					            await this.importTogglesStore.strategiesExistForFeatures(
 | 
				
			||||||
 | 
					                dto.data.features.map((feature) => feature.name),
 | 
				
			||||||
 | 
					                dto.environment,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (strategiesExistForFeatures) {
 | 
				
			||||||
 | 
					            permissions.push(DELETE_FEATURE_STRATEGY);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (dto.data.featureStrategies.length > 0) {
 | 
				
			||||||
 | 
					            permissions.push(CREATE_FEATURE_STRATEGY);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const featureEnvsWithVariants =
 | 
				
			||||||
 | 
					            dto.data.featureEnvironments?.filter(
 | 
				
			||||||
 | 
					                (featureEnvironment) =>
 | 
				
			||||||
 | 
					                    Array.isArray(featureEnvironment.variants) &&
 | 
				
			||||||
 | 
					                    featureEnvironment.variants.length > 0,
 | 
				
			||||||
 | 
					            ) || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (featureEnvsWithVariants.length > 0) {
 | 
				
			||||||
 | 
					            permissions.push(UPDATE_FEATURE_ENVIRONMENT_VARIANTS);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const existingFeatures =
 | 
				
			||||||
 | 
					            await this.importTogglesStore.getExistingFeatures(
 | 
				
			||||||
 | 
					                dto.data.features.map((feature) => feature.name),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (existingFeatures.length < dto.data.features.length) {
 | 
				
			||||||
 | 
					            permissions.push(CREATE_FEATURE);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const displayPermissions =
 | 
					        const displayPermissions =
 | 
				
			||||||
            await this.importTogglesStore.getDisplayPermissions(permissions);
 | 
					            await this.importTogglesStore.getDisplayPermissions(permissions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -603,9 +648,10 @@ export default class ExportImportService {
 | 
				
			|||||||
        const existingTagTypes = (await this.tagTypeService.getAll()).map(
 | 
					        const existingTagTypes = (await this.tagTypeService.getAll()).map(
 | 
				
			||||||
            (tagType) => tagType.name,
 | 
					            (tagType) => tagType.name,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        const newTagTypes = dto.data.tagTypes?.filter(
 | 
					        const newTagTypes =
 | 
				
			||||||
            (tagType) => !existingTagTypes.includes(tagType.name),
 | 
					            dto.data.tagTypes?.filter(
 | 
				
			||||||
        );
 | 
					                (tagType) => !existingTagTypes.includes(tagType.name),
 | 
				
			||||||
 | 
					            ) || [];
 | 
				
			||||||
        const uniqueTagTypes = [
 | 
					        const uniqueTagTypes = [
 | 
				
			||||||
            ...new Map(newTagTypes.map((item) => [item.name, item])).values(),
 | 
					            ...new Map(newTagTypes.map((item) => [item.name, item])).values(),
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
@ -657,10 +703,10 @@ export default class ExportImportService {
 | 
				
			|||||||
        const filteredContextFields = contextFields.filter(
 | 
					        const filteredContextFields = contextFields.filter(
 | 
				
			||||||
            (field) =>
 | 
					            (field) =>
 | 
				
			||||||
                featureEnvironments.some((featureEnv) =>
 | 
					                featureEnvironments.some((featureEnv) =>
 | 
				
			||||||
                    featureEnv.variants.some(
 | 
					                    featureEnv.variants?.some(
 | 
				
			||||||
                        (variant) =>
 | 
					                        (variant) =>
 | 
				
			||||||
                            variant.stickiness === field.name ||
 | 
					                            variant.stickiness === field.name ||
 | 
				
			||||||
                            variant.overrides.some(
 | 
					                            variant.overrides?.some(
 | 
				
			||||||
                                (override) =>
 | 
					                                (override) =>
 | 
				
			||||||
                                    override.contextName === field.name,
 | 
					                                    override.contextName === field.name,
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
@ -677,7 +723,7 @@ export default class ExportImportService {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
        const filteredSegments = segments.filter((segment) =>
 | 
					        const filteredSegments = segments.filter((segment) =>
 | 
				
			||||||
            featureStrategies.some((strategy) =>
 | 
					            featureStrategies.some((strategy) =>
 | 
				
			||||||
                strategy.segments.includes(segment.id),
 | 
					                strategy.segments?.includes(segment.id),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        const filteredTagTypes = tagTypes.filter((tagType) =>
 | 
					        const filteredTagTypes = tagTypes.filter((tagType) =>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,11 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    IUnleashTest,
 | 
					    IUnleashTest,
 | 
				
			||||||
    setupAppWithCustomConfig,
 | 
					    setupAppWithCustomConfig,
 | 
				
			||||||
} from '../../helpers/test-helper';
 | 
					} from '../../test/e2e/helpers/test-helper';
 | 
				
			||||||
import dbInit, { ITestDb } from '../../helpers/database-init';
 | 
					import dbInit, { ITestDb } from '../../test/e2e/helpers/database-init';
 | 
				
			||||||
import getLogger from '../../../fixtures/no-logger';
 | 
					import getLogger from '../../test/fixtures/no-logger';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					    DEFAULT_PROJECT,
 | 
				
			||||||
    FeatureToggleDTO,
 | 
					    FeatureToggleDTO,
 | 
				
			||||||
    IEnvironmentStore,
 | 
					    IEnvironmentStore,
 | 
				
			||||||
    IEventStore,
 | 
					    IEventStore,
 | 
				
			||||||
@ -13,14 +14,15 @@ import {
 | 
				
			|||||||
    ISegment,
 | 
					    ISegment,
 | 
				
			||||||
    IStrategyConfig,
 | 
					    IStrategyConfig,
 | 
				
			||||||
    IVariant,
 | 
					    IVariant,
 | 
				
			||||||
} from 'lib/types';
 | 
					} from '../types';
 | 
				
			||||||
import { DEFAULT_ENV } from '../../../../lib/util';
 | 
					import { DEFAULT_ENV } from '../util';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    ContextFieldSchema,
 | 
					    ContextFieldSchema,
 | 
				
			||||||
    ImportTogglesSchema,
 | 
					    ImportTogglesSchema,
 | 
				
			||||||
} from '../../../../lib/openapi';
 | 
					    VariantsSchema,
 | 
				
			||||||
import User from '../../../../lib/types/user';
 | 
					} from '../openapi';
 | 
				
			||||||
import { IContextFieldDto } from '../../../../lib/types/stores/context-field-store';
 | 
					import User from '../types/user';
 | 
				
			||||||
 | 
					import { IContextFieldDto } from '../types/stores/context-field-store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let app: IUnleashTest;
 | 
					let app: IUnleashTest;
 | 
				
			||||||
let db: ITestDb;
 | 
					let db: ITestDb;
 | 
				
			||||||
@ -85,35 +87,30 @@ const createContext = async (context: ContextFieldSchema = defaultContext) => {
 | 
				
			|||||||
        .expect(201);
 | 
					        .expect(201);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createVariants = async (
 | 
					const createVariants = async (feature: string, variants: IVariant[]) => {
 | 
				
			||||||
    project: string,
 | 
					 | 
				
			||||||
    feature: string,
 | 
					 | 
				
			||||||
    environment: string,
 | 
					 | 
				
			||||||
    variants: IVariant[],
 | 
					 | 
				
			||||||
) => {
 | 
					 | 
				
			||||||
    await app.services.featureToggleService.saveVariantsOnEnv(
 | 
					    await app.services.featureToggleService.saveVariantsOnEnv(
 | 
				
			||||||
        project,
 | 
					        DEFAULT_PROJECT,
 | 
				
			||||||
        feature,
 | 
					        feature,
 | 
				
			||||||
        environment,
 | 
					        DEFAULT_ENV,
 | 
				
			||||||
        variants,
 | 
					        variants,
 | 
				
			||||||
        new User({ id: 1 }),
 | 
					        new User({ id: 1 }),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createProject = async (project: string, environment: string) => {
 | 
					const createProject = async () => {
 | 
				
			||||||
    await db.stores.environmentStore.create({
 | 
					    await db.stores.environmentStore.create({
 | 
				
			||||||
        name: environment,
 | 
					        name: DEFAULT_ENV,
 | 
				
			||||||
        type: 'production',
 | 
					        type: 'production',
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await db.stores.projectStore.create({
 | 
					    await db.stores.projectStore.create({
 | 
				
			||||||
        name: project,
 | 
					        name: DEFAULT_PROJECT,
 | 
				
			||||||
        description: '',
 | 
					        description: '',
 | 
				
			||||||
        id: project,
 | 
					        id: DEFAULT_PROJECT,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    await app.request
 | 
					    await app.request
 | 
				
			||||||
        .post(`/api/admin/projects/${project}/environments`)
 | 
					        .post(`/api/admin/projects/${DEFAULT_PROJECT}/environments`)
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            environment,
 | 
					            environment: DEFAULT_ENV,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
        .expect(200);
 | 
					        .expect(200);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -128,9 +125,9 @@ const createContextField = async (contextField: IContextFieldDto) => {
 | 
				
			|||||||
    await app.request.post(`/api/admin/context`).send(contextField).expect(201);
 | 
					    await app.request.post(`/api/admin/context`).send(contextField).expect(201);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createFeature = async (featureName: string, project: string) => {
 | 
					const createFeature = async (featureName: string) => {
 | 
				
			||||||
    await app.request
 | 
					    await app.request
 | 
				
			||||||
        .post(`/api/admin/projects/${project}/features`)
 | 
					        .post(`/api/admin/projects/${DEFAULT_PROJECT}/features`)
 | 
				
			||||||
        .send({
 | 
					        .send({
 | 
				
			||||||
            name: featureName,
 | 
					            name: featureName,
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
@ -138,9 +135,11 @@ const createFeature = async (featureName: string, project: string) => {
 | 
				
			|||||||
        .expect(201);
 | 
					        .expect(201);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const archiveFeature = async (featureName: string, project: string) => {
 | 
					const archiveFeature = async (featureName: string) => {
 | 
				
			||||||
    await app.request
 | 
					    await app.request
 | 
				
			||||||
        .delete(`/api/admin/projects/${project}/features/${featureName}`)
 | 
					        .delete(
 | 
				
			||||||
 | 
					            `/api/admin/projects/${DEFAULT_PROJECT}/features/${featureName}`,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        .set('Content-Type', 'application/json')
 | 
					        .set('Content-Type', 'application/json')
 | 
				
			||||||
        .expect(202);
 | 
					        .expect(202);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -188,7 +187,7 @@ afterAll(async () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
test('exports features', async () => {
 | 
					test('exports features', async () => {
 | 
				
			||||||
    const segmentName = 'my-segment';
 | 
					    const segmentName = 'my-segment';
 | 
				
			||||||
    await createProject('default', 'default');
 | 
					    await createProject();
 | 
				
			||||||
    const segment = await createSegment({ name: segmentName, constraints: [] });
 | 
					    const segment = await createSegment({ name: segmentName, constraints: [] });
 | 
				
			||||||
    const strategy = {
 | 
					    const strategy = {
 | 
				
			||||||
        name: 'default',
 | 
					        name: 'default',
 | 
				
			||||||
@ -249,7 +248,7 @@ test('exports features', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should export custom context fields from strategies and variants', async () => {
 | 
					test('should export custom context fields from strategies and variants', async () => {
 | 
				
			||||||
    await createProject('default', 'default');
 | 
					    await createProject();
 | 
				
			||||||
    const strategyContext = {
 | 
					    const strategyContext = {
 | 
				
			||||||
        name: 'strategy-context',
 | 
					        name: 'strategy-context',
 | 
				
			||||||
        legalValues: [
 | 
					        legalValues: [
 | 
				
			||||||
@ -301,7 +300,7 @@ test('should export custom context fields from strategies and variants', async (
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
    await createContext(variantStickinessContext);
 | 
					    await createContext(variantStickinessContext);
 | 
				
			||||||
    await createContext(variantOverridesContext);
 | 
					    await createContext(variantOverridesContext);
 | 
				
			||||||
    await createVariants('default', 'first_feature', 'default', [
 | 
					    await createVariants('first_feature', [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            name: 'irrelevant',
 | 
					            name: 'irrelevant',
 | 
				
			||||||
            weight: 1000,
 | 
					            weight: 1000,
 | 
				
			||||||
@ -351,7 +350,7 @@ test('should export custom context fields from strategies and variants', async (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
test('should export tags', async () => {
 | 
					test('should export tags', async () => {
 | 
				
			||||||
    const featureName = 'first_feature';
 | 
					    const featureName = 'first_feature';
 | 
				
			||||||
    await createProject('default', 'default');
 | 
					    await createProject();
 | 
				
			||||||
    await createToggle(
 | 
					    await createToggle(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            name: featureName,
 | 
					            name: featureName,
 | 
				
			||||||
@ -390,7 +389,7 @@ test('should export tags', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('returns no features, when no feature was requested', async () => {
 | 
					test('returns no features, when no feature was requested', async () => {
 | 
				
			||||||
    await createProject('default', 'default');
 | 
					    await createProject();
 | 
				
			||||||
    await createToggle({
 | 
					    await createToggle({
 | 
				
			||||||
        name: 'first_feature',
 | 
					        name: 'first_feature',
 | 
				
			||||||
        description: 'the #1 feature',
 | 
					        description: 'the #1 feature',
 | 
				
			||||||
@ -424,34 +423,31 @@ const importToggles = (
 | 
				
			|||||||
        .expect(expect);
 | 
					        .expect(expect);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const defaultFeature = 'first_feature';
 | 
					const defaultFeature = 'first_feature';
 | 
				
			||||||
const defaultProject = 'default';
 | 
					 | 
				
			||||||
const defaultEnvironment = 'defalt';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const variants: ImportTogglesSchema['data']['featureEnvironments'][0]['variants'] =
 | 
					const variants: VariantsSchema = [
 | 
				
			||||||
    [
 | 
					    {
 | 
				
			||||||
        {
 | 
					        name: 'variantA',
 | 
				
			||||||
            name: 'variantA',
 | 
					        weight: 500,
 | 
				
			||||||
            weight: 500,
 | 
					        payload: {
 | 
				
			||||||
            payload: {
 | 
					            type: 'string',
 | 
				
			||||||
                type: 'string',
 | 
					            value: 'payloadA',
 | 
				
			||||||
                value: 'payloadA',
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            overrides: [],
 | 
					 | 
				
			||||||
            stickiness: 'default',
 | 
					 | 
				
			||||||
            weightType: 'variable',
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        overrides: [],
 | 
				
			||||||
            name: 'variantB',
 | 
					        stickiness: 'default',
 | 
				
			||||||
            weight: 500,
 | 
					        weightType: 'variable',
 | 
				
			||||||
            payload: {
 | 
					    },
 | 
				
			||||||
                type: 'string',
 | 
					    {
 | 
				
			||||||
                value: 'payloadB',
 | 
					        name: 'variantB',
 | 
				
			||||||
            },
 | 
					        weight: 500,
 | 
				
			||||||
            overrides: [],
 | 
					        payload: {
 | 
				
			||||||
            stickiness: 'default',
 | 
					            type: 'string',
 | 
				
			||||||
            weightType: 'variable',
 | 
					            value: 'payloadB',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    ];
 | 
					        overrides: [],
 | 
				
			||||||
 | 
					        stickiness: 'default',
 | 
				
			||||||
 | 
					        weightType: 'variable',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
const exportedFeature: ImportTogglesSchema['data']['features'][0] = {
 | 
					const exportedFeature: ImportTogglesSchema['data']['features'][0] = {
 | 
				
			||||||
    project: 'old_project',
 | 
					    project: 'old_project',
 | 
				
			||||||
    name: 'first_feature',
 | 
					    name: 'first_feature',
 | 
				
			||||||
@ -522,21 +518,17 @@ const defaultImportPayload: ImportTogglesSchema = {
 | 
				
			|||||||
        contextFields: [],
 | 
					        contextFields: [],
 | 
				
			||||||
        segments: [],
 | 
					        segments: [],
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    project: defaultProject,
 | 
					    project: DEFAULT_PROJECT,
 | 
				
			||||||
    environment: defaultEnvironment,
 | 
					    environment: DEFAULT_ENV,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getFeature = async (feature: string) =>
 | 
					const getFeature = async (feature: string) =>
 | 
				
			||||||
    app.request.get(`/api/admin/features/${feature}`).expect(200);
 | 
					    app.request.get(`/api/admin/features/${feature}`).expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getFeatureEnvironment = (
 | 
					const getFeatureEnvironment = (feature: string) =>
 | 
				
			||||||
    project: string,
 | 
					 | 
				
			||||||
    feature: string,
 | 
					 | 
				
			||||||
    environment: string,
 | 
					 | 
				
			||||||
) =>
 | 
					 | 
				
			||||||
    app.request
 | 
					    app.request
 | 
				
			||||||
        .get(
 | 
					        .get(
 | 
				
			||||||
            `/api/admin/projects/${project}/features/${feature}/environments/${environment}`,
 | 
					            `/api/admin/projects/${DEFAULT_PROJECT}/features/${feature}/environments/${DEFAULT_ENV}`,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .expect(200);
 | 
					        .expect(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -551,25 +543,23 @@ const validateImport = (importPayload: ImportTogglesSchema, status = 200) =>
 | 
				
			|||||||
        .expect(status);
 | 
					        .expect(status);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('import features to existing project and environment', async () => {
 | 
					test('import features to existing project and environment', async () => {
 | 
				
			||||||
    await createProject(defaultProject, defaultEnvironment);
 | 
					    await createProject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await importToggles(defaultImportPayload);
 | 
					    await importToggles(defaultImportPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { body: importedFeature } = await getFeature(defaultFeature);
 | 
					    const { body: importedFeature } = await getFeature(defaultFeature);
 | 
				
			||||||
    expect(importedFeature).toMatchObject({
 | 
					    expect(importedFeature).toMatchObject({
 | 
				
			||||||
        name: 'first_feature',
 | 
					        name: 'first_feature',
 | 
				
			||||||
        project: defaultProject,
 | 
					        project: DEFAULT_PROJECT,
 | 
				
			||||||
        variants,
 | 
					        variants,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { body: importedFeatureEnvironment } = await getFeatureEnvironment(
 | 
					    const { body: importedFeatureEnvironment } = await getFeatureEnvironment(
 | 
				
			||||||
        defaultProject,
 | 
					 | 
				
			||||||
        defaultFeature,
 | 
					        defaultFeature,
 | 
				
			||||||
        defaultEnvironment,
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    expect(importedFeatureEnvironment).toMatchObject({
 | 
					    expect(importedFeatureEnvironment).toMatchObject({
 | 
				
			||||||
        name: defaultFeature,
 | 
					        name: defaultFeature,
 | 
				
			||||||
        environment: defaultEnvironment,
 | 
					        environment: DEFAULT_ENV,
 | 
				
			||||||
        enabled: true,
 | 
					        enabled: true,
 | 
				
			||||||
        strategies: [
 | 
					        strategies: [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -589,26 +579,24 @@ test('import features to existing project and environment', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('importing same JSON should work multiple times in a row', async () => {
 | 
					test('importing same JSON should work multiple times in a row', async () => {
 | 
				
			||||||
    await createProject(defaultProject, defaultEnvironment);
 | 
					    await createProject();
 | 
				
			||||||
    await importToggles(defaultImportPayload);
 | 
					    await importToggles(defaultImportPayload);
 | 
				
			||||||
    await importToggles(defaultImportPayload);
 | 
					    await importToggles(defaultImportPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { body: importedFeature } = await getFeature(defaultFeature);
 | 
					    const { body: importedFeature } = await getFeature(defaultFeature);
 | 
				
			||||||
    expect(importedFeature).toMatchObject({
 | 
					    expect(importedFeature).toMatchObject({
 | 
				
			||||||
        name: 'first_feature',
 | 
					        name: 'first_feature',
 | 
				
			||||||
        project: defaultProject,
 | 
					        project: DEFAULT_PROJECT,
 | 
				
			||||||
        variants,
 | 
					        variants,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { body: importedFeatureEnvironment } = await getFeatureEnvironment(
 | 
					    const { body: importedFeatureEnvironment } = await getFeatureEnvironment(
 | 
				
			||||||
        defaultProject,
 | 
					 | 
				
			||||||
        defaultFeature,
 | 
					        defaultFeature,
 | 
				
			||||||
        defaultEnvironment,
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(importedFeatureEnvironment).toMatchObject({
 | 
					    expect(importedFeatureEnvironment).toMatchObject({
 | 
				
			||||||
        name: defaultFeature,
 | 
					        name: defaultFeature,
 | 
				
			||||||
        environment: defaultEnvironment,
 | 
					        environment: DEFAULT_ENV,
 | 
				
			||||||
        enabled: true,
 | 
					        enabled: true,
 | 
				
			||||||
        strategies: [
 | 
					        strategies: [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -623,7 +611,7 @@ test('importing same JSON should work multiple times in a row', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('reject import with unknown context fields', async () => {
 | 
					test('reject import with unknown context fields', async () => {
 | 
				
			||||||
    await createProject(defaultProject, defaultEnvironment);
 | 
					    await createProject();
 | 
				
			||||||
    const contextField = {
 | 
					    const contextField = {
 | 
				
			||||||
        name: 'ContextField1',
 | 
					        name: 'ContextField1',
 | 
				
			||||||
        legalValues: [{ value: 'Value1', description: '' }],
 | 
					        legalValues: [{ value: 'Value1', description: '' }],
 | 
				
			||||||
@ -654,12 +642,14 @@ test('reject import with unknown context fields', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('reject import with unsupported strategies', async () => {
 | 
					test('reject import with unsupported strategies', async () => {
 | 
				
			||||||
    await createProject(defaultProject, defaultEnvironment);
 | 
					    await createProject();
 | 
				
			||||||
    const importPayloadWithContextFields: ImportTogglesSchema = {
 | 
					    const importPayloadWithContextFields: ImportTogglesSchema = {
 | 
				
			||||||
        ...defaultImportPayload,
 | 
					        ...defaultImportPayload,
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
            ...defaultImportPayload.data,
 | 
					            ...defaultImportPayload.data,
 | 
				
			||||||
            featureStrategies: [{ name: 'customStrategy' }],
 | 
					            featureStrategies: [
 | 
				
			||||||
 | 
					                { name: 'customStrategy', featureName: 'featureName' },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -675,7 +665,7 @@ test('reject import with unsupported strategies', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('validate import data', async () => {
 | 
					test('validate import data', async () => {
 | 
				
			||||||
    await createProject(defaultProject, defaultEnvironment);
 | 
					    await createProject();
 | 
				
			||||||
    const contextField: IContextFieldDto = {
 | 
					    const contextField: IContextFieldDto = {
 | 
				
			||||||
        name: 'validate_context_field',
 | 
					        name: 'validate_context_field',
 | 
				
			||||||
        legalValues: [{ value: 'Value1' }],
 | 
					        legalValues: [{ value: 'Value1' }],
 | 
				
			||||||
@ -686,8 +676,8 @@ test('validate import data', async () => {
 | 
				
			|||||||
        legalValues: [{ value: 'new_value' }],
 | 
					        legalValues: [{ value: 'new_value' }],
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await createFeature(defaultFeature, defaultProject);
 | 
					    await createFeature(defaultFeature);
 | 
				
			||||||
    await archiveFeature(defaultFeature, defaultProject);
 | 
					    await archiveFeature(defaultFeature);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await createContextField(contextField);
 | 
					    await createContextField(contextField);
 | 
				
			||||||
    const importPayloadWithContextFields: ImportTogglesSchema = {
 | 
					    const importPayloadWithContextFields: ImportTogglesSchema = {
 | 
				
			||||||
@ -733,7 +723,7 @@ test('validate import data', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should create new context', async () => {
 | 
					test('should create new context', async () => {
 | 
				
			||||||
    await createProject(defaultProject, defaultEnvironment);
 | 
					    await createProject();
 | 
				
			||||||
    const context = {
 | 
					    const context = {
 | 
				
			||||||
        name: 'create-new-context',
 | 
					        name: 'create-new-context',
 | 
				
			||||||
        legalValues: [{ value: 'Value1' }],
 | 
					        legalValues: [{ value: 'Value1' }],
 | 
				
			||||||
@ -753,10 +743,10 @@ test('should create new context', async () => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should not import archived features tags', async () => {
 | 
					test('should not import archived features tags', async () => {
 | 
				
			||||||
    await createProject(defaultProject, defaultEnvironment);
 | 
					    await createProject();
 | 
				
			||||||
    await importToggles(defaultImportPayload);
 | 
					    await importToggles(defaultImportPayload);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await archiveFeature(defaultFeature, defaultProject);
 | 
					    await archiveFeature(defaultFeature);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await importToggles({
 | 
					    await importToggles({
 | 
				
			||||||
        ...defaultImportPayload,
 | 
					        ...defaultImportPayload,
 | 
				
			||||||
@ -13,7 +13,14 @@ export interface IImportTogglesStore {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    deleteTagsForFeatures(tags: string[]): Promise<void>;
 | 
					    deleteTagsForFeatures(tags: string[]): Promise<void>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    strategiesExistForFeatures(
 | 
				
			||||||
 | 
					        featureNames: string[],
 | 
				
			||||||
 | 
					        environment: string,
 | 
				
			||||||
 | 
					    ): Promise<boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getDisplayPermissions(
 | 
					    getDisplayPermissions(
 | 
				
			||||||
        names: string[],
 | 
					        names: string[],
 | 
				
			||||||
    ): Promise<{ name: string; displayName: string }[]>;
 | 
					    ): Promise<{ name: string; displayName: string }[]>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getExistingFeatures(featureNames: string[]): Promise<string[]>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ import { Knex } from 'knex';
 | 
				
			|||||||
const T = {
 | 
					const T = {
 | 
				
			||||||
    featureStrategies: 'feature_strategies',
 | 
					    featureStrategies: 'feature_strategies',
 | 
				
			||||||
    features: 'features',
 | 
					    features: 'features',
 | 
				
			||||||
    feature_tag: 'feature_tag',
 | 
					    featureTag: 'feature_tag',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
export class ImportTogglesStore implements IImportTogglesStore {
 | 
					export class ImportTogglesStore implements IImportTogglesStore {
 | 
				
			||||||
    private db: Knex;
 | 
					    private db: Knex;
 | 
				
			||||||
@ -35,6 +35,20 @@ export class ImportTogglesStore implements IImportTogglesStore {
 | 
				
			|||||||
            .del();
 | 
					            .del();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async strategiesExistForFeatures(
 | 
				
			||||||
 | 
					        featureNames: string[],
 | 
				
			||||||
 | 
					        environment: string,
 | 
				
			||||||
 | 
					    ): Promise<boolean> {
 | 
				
			||||||
 | 
					        const result = await this.db.raw(
 | 
				
			||||||
 | 
					            'SELECT EXISTS (SELECT 1 FROM feature_strategies WHERE environment = ? and feature_name in  (' +
 | 
				
			||||||
 | 
					                featureNames.map(() => '?').join(',') +
 | 
				
			||||||
 | 
					                ')) AS present',
 | 
				
			||||||
 | 
					            [environment, ...featureNames],
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        const { present } = result.rows[0];
 | 
				
			||||||
 | 
					        return present;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getArchivedFeatures(featureNames: string[]): Promise<string[]> {
 | 
					    async getArchivedFeatures(featureNames: string[]): Promise<string[]> {
 | 
				
			||||||
        const rows = await this.db(T.features)
 | 
					        const rows = await this.db(T.features)
 | 
				
			||||||
            .select('name')
 | 
					            .select('name')
 | 
				
			||||||
@ -43,6 +57,11 @@ export class ImportTogglesStore implements IImportTogglesStore {
 | 
				
			|||||||
        return rows.map((row) => row.name);
 | 
					        return rows.map((row) => row.name);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getExistingFeatures(featureNames: string[]): Promise<string[]> {
 | 
				
			||||||
 | 
					        const rows = await this.db(T.features).whereIn('name', featureNames);
 | 
				
			||||||
 | 
					        return rows.map((row) => row.name);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getFeaturesInOtherProjects(
 | 
					    async getFeaturesInOtherProjects(
 | 
				
			||||||
        featureNames: string[],
 | 
					        featureNames: string[],
 | 
				
			||||||
        project: string,
 | 
					        project: string,
 | 
				
			||||||
@ -54,7 +73,7 @@ export class ImportTogglesStore implements IImportTogglesStore {
 | 
				
			|||||||
        return rows.map((row) => ({ name: row.name, project: row.project }));
 | 
					        return rows.map((row) => ({ name: row.name, project: row.project }));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async deleteTagsForFeatures(tags: string[]): Promise<void> {
 | 
					    async deleteTagsForFeatures(features: string[]): Promise<void> {
 | 
				
			||||||
        return this.db(T.feature_tag).whereIn('feature_name', tags).del();
 | 
					        return this.db(T.featureTag).whereIn('feature_name', features).del();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -8,3 +8,5 @@ export type PartialDeep<T> = T extends object
 | 
				
			|||||||
// Mark one or more properties as optional.
 | 
					// Mark one or more properties as optional.
 | 
				
			||||||
export type PartialSome<T, K extends keyof T> = Pick<Partial<T>, K> &
 | 
					export type PartialSome<T, K extends keyof T> = Pick<Partial<T>, K> &
 | 
				
			||||||
    Omit<T, K>;
 | 
					    Omit<T, K>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
 | 
				
			||||||
 | 
				
			|||||||
@ -70,8 +70,9 @@ export async function setupAppWithCustomConfig(
 | 
				
			|||||||
export async function setupAppWithAuth(
 | 
					export async function setupAppWithAuth(
 | 
				
			||||||
    stores: IUnleashStores,
 | 
					    stores: IUnleashStores,
 | 
				
			||||||
    customOptions?: any,
 | 
					    customOptions?: any,
 | 
				
			||||||
 | 
					    db?: Db,
 | 
				
			||||||
): Promise<IUnleashTest> {
 | 
					): Promise<IUnleashTest> {
 | 
				
			||||||
    return createApp(stores, IAuthType.DEMO, undefined, customOptions);
 | 
					    return createApp(stores, IAuthType.DEMO, undefined, customOptions, db);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function setupAppWithCustomAuth(
 | 
					export async function setupAppWithCustomAuth(
 | 
				
			||||||
 | 
				
			|||||||
@ -27,7 +27,7 @@
 | 
				
			|||||||
    /* Strict Type-Checking Options */
 | 
					    /* Strict Type-Checking Options */
 | 
				
			||||||
    //    "strict": true,                           /* Enable all strict type-checking options. */
 | 
					    //    "strict": true,                           /* Enable all strict type-checking options. */
 | 
				
			||||||
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
 | 
					    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
 | 
				
			||||||
    // "strictNullChecks": true,              /* Enable strict null checks. */
 | 
					    //     "strictNullChecks": true,              /* Enable strict null checks. */
 | 
				
			||||||
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
 | 
					    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
 | 
				
			||||||
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
 | 
					    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
 | 
				
			||||||
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
 | 
					    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user