mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +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