mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-09 00:18:26 +01:00
feat: Import limit validation (#4669)
This commit is contained in:
parent
ed6547b6f1
commit
2b2f5e20fa
@ -41,7 +41,10 @@ import {
|
|||||||
TagTypeService,
|
TagTypeService,
|
||||||
} from '../../services';
|
} from '../../services';
|
||||||
import { isValidField } from './import-context-validation';
|
import { isValidField } from './import-context-validation';
|
||||||
import { IImportTogglesStore } from './import-toggles-store-type';
|
import {
|
||||||
|
IImportTogglesStore,
|
||||||
|
ProjectFeaturesLimit,
|
||||||
|
} from './import-toggles-store-type';
|
||||||
import { ImportPermissionsService, Mode } from './import-permissions-service';
|
import { ImportPermissionsService, Mode } from './import-permissions-service';
|
||||||
import { ImportValidationMessages } from './import-validation-messages';
|
import { ImportValidationMessages } from './import-validation-messages';
|
||||||
import { findDuplicates } from '../../util/findDuplicates';
|
import { findDuplicates } from '../../util/findDuplicates';
|
||||||
@ -158,6 +161,7 @@ export default class ExportImportService {
|
|||||||
missingPermissions,
|
missingPermissions,
|
||||||
duplicateFeatures,
|
duplicateFeatures,
|
||||||
featureNameCheckResult,
|
featureNameCheckResult,
|
||||||
|
featureLimitResult,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getUnsupportedStrategies(dto),
|
this.getUnsupportedStrategies(dto),
|
||||||
this.getUsedCustomStrategies(dto),
|
this.getUsedCustomStrategies(dto),
|
||||||
@ -172,6 +176,7 @@ export default class ExportImportService {
|
|||||||
),
|
),
|
||||||
this.getDuplicateFeatures(dto),
|
this.getDuplicateFeatures(dto),
|
||||||
this.getInvalidFeatureNames(dto),
|
this.getInvalidFeatureNames(dto),
|
||||||
|
this.getFeatureLimit(dto),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const errors = ImportValidationMessages.compileErrors({
|
const errors = ImportValidationMessages.compileErrors({
|
||||||
@ -181,6 +186,7 @@ export default class ExportImportService {
|
|||||||
otherProjectFeatures,
|
otherProjectFeatures,
|
||||||
duplicateFeatures,
|
duplicateFeatures,
|
||||||
featureNameCheckResult,
|
featureNameCheckResult,
|
||||||
|
featureLimitResult,
|
||||||
});
|
});
|
||||||
const warnings = ImportValidationMessages.compileWarnings({
|
const warnings = ImportValidationMessages.compileWarnings({
|
||||||
archivedFeatures,
|
archivedFeatures,
|
||||||
@ -511,6 +517,16 @@ export default class ExportImportService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getFeatureLimit({
|
||||||
|
project,
|
||||||
|
data,
|
||||||
|
}: ImportTogglesSchema): Promise<ProjectFeaturesLimit> {
|
||||||
|
return this.importTogglesStore.getProjectFeaturesLimit(
|
||||||
|
[...new Set(data.features.map((f) => f.name))],
|
||||||
|
project,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async getUnsupportedStrategies(
|
private async getUnsupportedStrategies(
|
||||||
dto: ImportTogglesSchema,
|
dto: ImportTogglesSchema,
|
||||||
): Promise<FeatureStrategySchema[]> {
|
): Promise<FeatureStrategySchema[]> {
|
||||||
|
@ -844,7 +844,8 @@ test('reject import with duplicate features', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('validate import data', async () => {
|
test('validate import data', async () => {
|
||||||
await createProjects();
|
const featureLimit = 1;
|
||||||
|
await createProjects([DEFAULT_PROJECT], featureLimit);
|
||||||
|
|
||||||
const contextField: IContextFieldDto = {
|
const contextField: IContextFieldDto = {
|
||||||
name: 'validate_context_field',
|
name: 'validate_context_field',
|
||||||
@ -864,7 +865,11 @@ test('validate import data', async () => {
|
|||||||
...defaultImportPayload,
|
...defaultImportPayload,
|
||||||
data: {
|
data: {
|
||||||
...defaultImportPayload.data,
|
...defaultImportPayload.data,
|
||||||
features: [exportedFeature, exportedFeature],
|
features: [
|
||||||
|
exportedFeature,
|
||||||
|
exportedFeature,
|
||||||
|
anotherExportedFeature,
|
||||||
|
],
|
||||||
featureStrategies: [{ name: 'customStrategy' }],
|
featureStrategies: [{ name: 'customStrategy' }],
|
||||||
segments: [{ id: 1, name: 'customSegment' }],
|
segments: [{ id: 1, name: 'customSegment' }],
|
||||||
contextFields: [
|
contextFields: [
|
||||||
@ -909,7 +914,15 @@ test('validate import data', async () => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
message: expect.stringMatching(/\btestpattern.+\b/),
|
message: expect.stringMatching(/\btestpattern.+\b/),
|
||||||
affectedItems: [defaultFeatureName],
|
affectedItems: [
|
||||||
|
defaultFeatureName,
|
||||||
|
anotherExportedFeature.name,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'We detected you want to create 2 new features to a project that already has 0 existing features, exceeding the maximum limit of 1.',
|
||||||
|
affectedItems: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
warnings: [
|
warnings: [
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
export interface ProjectFeaturesLimit {
|
||||||
|
limit: number;
|
||||||
|
newFeaturesCount: number;
|
||||||
|
currentFeaturesCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IImportTogglesStore {
|
export interface IImportTogglesStore {
|
||||||
deleteStrategiesForFeatures(
|
deleteStrategiesForFeatures(
|
||||||
featureNames: string[],
|
featureNames: string[],
|
||||||
@ -16,6 +22,11 @@ export interface IImportTogglesStore {
|
|||||||
project: string,
|
project: string,
|
||||||
): Promise<string[]>;
|
): Promise<string[]>;
|
||||||
|
|
||||||
|
getProjectFeaturesLimit(
|
||||||
|
featureNames: string[],
|
||||||
|
project: string,
|
||||||
|
): Promise<ProjectFeaturesLimit>;
|
||||||
|
|
||||||
deleteTagsForFeatures(tags: string[]): Promise<void>;
|
deleteTagsForFeatures(tags: string[]): Promise<void>;
|
||||||
|
|
||||||
strategiesExistForFeatures(
|
strategiesExistForFeatures(
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { IImportTogglesStore } from './import-toggles-store-type';
|
import {
|
||||||
|
IImportTogglesStore,
|
||||||
|
ProjectFeaturesLimit,
|
||||||
|
} from './import-toggles-store-type';
|
||||||
import { Db } from '../../db/db';
|
import { Db } from '../../db/db';
|
||||||
|
|
||||||
const T = {
|
const T = {
|
||||||
featureStrategies: 'feature_strategies',
|
featureStrategies: 'feature_strategies',
|
||||||
features: 'features',
|
features: 'features',
|
||||||
featureTag: 'feature_tag',
|
featureTag: 'feature_tag',
|
||||||
|
projectSettings: 'project_settings',
|
||||||
};
|
};
|
||||||
export class ImportTogglesStore implements IImportTogglesStore {
|
export class ImportTogglesStore implements IImportTogglesStore {
|
||||||
private db: Db;
|
private db: Db;
|
||||||
@ -86,6 +90,38 @@ export class ImportTogglesStore implements IImportTogglesStore {
|
|||||||
return rows.map((row) => row.name);
|
return rows.map((row) => row.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectFeaturesLimit(
|
||||||
|
featureNames: string[],
|
||||||
|
project: string,
|
||||||
|
): Promise<ProjectFeaturesLimit> {
|
||||||
|
const row = await this.db(T.projectSettings)
|
||||||
|
.select(['feature_limit'])
|
||||||
|
.where('project', project)
|
||||||
|
.first();
|
||||||
|
const limit: number = row?.feature_limit ?? Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
const existingFeaturesCount = await this.db(T.features)
|
||||||
|
.whereIn('name', featureNames)
|
||||||
|
.andWhere('project', project)
|
||||||
|
.where('archived_at', null)
|
||||||
|
.count()
|
||||||
|
.then((res) => Number(res[0].count));
|
||||||
|
|
||||||
|
const newFeaturesCount = featureNames.length - existingFeaturesCount;
|
||||||
|
|
||||||
|
const currentFeaturesCount = await this.db(T.features)
|
||||||
|
.where('project', project)
|
||||||
|
.count()
|
||||||
|
.where('archived_at', null)
|
||||||
|
.then((res) => Number(res[0].count));
|
||||||
|
|
||||||
|
return {
|
||||||
|
limit,
|
||||||
|
newFeaturesCount,
|
||||||
|
currentFeaturesCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async deleteTagsForFeatures(features: string[]): Promise<void> {
|
async deleteTagsForFeatures(features: string[]): Promise<void> {
|
||||||
return this.db(T.featureTag).whereIn('feature_name', features).del();
|
return this.db(T.featureTag).whereIn('feature_name', features).del();
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
ImportTogglesValidateItemSchema,
|
ImportTogglesValidateItemSchema,
|
||||||
} from '../../openapi';
|
} from '../../openapi';
|
||||||
import { IContextFieldDto } from '../../types/stores/context-field-store';
|
import { IContextFieldDto } from '../../types/stores/context-field-store';
|
||||||
|
import { ProjectFeaturesLimit } from './import-toggles-store-type';
|
||||||
|
|
||||||
export interface IErrorsParams {
|
export interface IErrorsParams {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
@ -12,6 +13,7 @@ export interface IErrorsParams {
|
|||||||
otherProjectFeatures: string[];
|
otherProjectFeatures: string[];
|
||||||
duplicateFeatures: string[];
|
duplicateFeatures: string[];
|
||||||
featureNameCheckResult: FeatureNameCheckResult;
|
featureNameCheckResult: FeatureNameCheckResult;
|
||||||
|
featureLimitResult: ProjectFeaturesLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWarningParams {
|
export interface IWarningParams {
|
||||||
@ -43,6 +45,7 @@ export class ImportValidationMessages {
|
|||||||
otherProjectFeatures,
|
otherProjectFeatures,
|
||||||
duplicateFeatures,
|
duplicateFeatures,
|
||||||
featureNameCheckResult,
|
featureNameCheckResult,
|
||||||
|
featureLimitResult,
|
||||||
}: IErrorsParams): ImportTogglesValidateItemSchema[] {
|
}: IErrorsParams): ImportTogglesValidateItemSchema[] {
|
||||||
const errors: ImportTogglesValidateItemSchema[] = [];
|
const errors: ImportTogglesValidateItemSchema[] = [];
|
||||||
|
|
||||||
@ -92,6 +95,16 @@ export class ImportValidationMessages {
|
|||||||
affectedItems: [...featureNameCheckResult.invalidNames].sort(),
|
affectedItems: [...featureNameCheckResult.invalidNames].sort(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
featureLimitResult.currentFeaturesCount +
|
||||||
|
featureLimitResult.newFeaturesCount >
|
||||||
|
featureLimitResult.limit
|
||||||
|
) {
|
||||||
|
errors.push({
|
||||||
|
message: `We detected you want to create ${featureLimitResult.newFeaturesCount} new features to a project that already has ${featureLimitResult.currentFeaturesCount} existing features, exceeding the maximum limit of ${featureLimitResult.limit}.`,
|
||||||
|
affectedItems: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user