mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: feature creation limit crud together with frontend (#4221)
This commit is contained in:
		
							parent
							
								
									c387a19831
								
							
						
					
					
						commit
						3da1cbba47
					
				@ -216,7 +216,6 @@ const getDeleteButtons = async () => {
 | 
			
		||||
            deleteButtons.push(...removeButton);
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
    console.log(deleteButtons);
 | 
			
		||||
    return deleteButtons;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,24 @@ import UIContext from 'contexts/UIContext';
 | 
			
		||||
import { CF_CREATE_BTN_ID } from 'utils/testIds';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
import { GO_BACK } from 'constants/navigate';
 | 
			
		||||
import { Alert, styled } from '@mui/material';
 | 
			
		||||
import useProject from 'hooks/api/getters/useProject/useProject';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
 | 
			
		||||
const StyledAlert = styled(Alert)(({ theme }) => ({
 | 
			
		||||
    marginBottom: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const isFeatureLimitReached = (
 | 
			
		||||
    featureLimit: number | null | undefined,
 | 
			
		||||
    currentFeatureCount: number
 | 
			
		||||
): boolean => {
 | 
			
		||||
    return (
 | 
			
		||||
        featureLimit !== null &&
 | 
			
		||||
        featureLimit !== undefined &&
 | 
			
		||||
        featureLimit <= currentFeatureCount
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const CreateFeature = () => {
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
@ -36,6 +54,8 @@ const CreateFeature = () => {
 | 
			
		||||
        errors,
 | 
			
		||||
    } = useFeatureForm();
 | 
			
		||||
 | 
			
		||||
    const { project: projectInfo } = useProject(project);
 | 
			
		||||
 | 
			
		||||
    const { createFeatureToggle, loading } = useFeatureApi();
 | 
			
		||||
 | 
			
		||||
    const handleSubmit = async (e: Event) => {
 | 
			
		||||
@ -74,6 +94,11 @@ const CreateFeature = () => {
 | 
			
		||||
        navigate(GO_BACK);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const featureLimitReached =
 | 
			
		||||
        isFeatureLimitReached(
 | 
			
		||||
            projectInfo.featureLimit,
 | 
			
		||||
            projectInfo.features.length
 | 
			
		||||
        ) && Boolean(uiConfig.flags.newProjectLayout);
 | 
			
		||||
    return (
 | 
			
		||||
        <FormTemplate
 | 
			
		||||
            loading={loading}
 | 
			
		||||
@ -84,6 +109,18 @@ const CreateFeature = () => {
 | 
			
		||||
            documentationLinkLabel="Feature toggle types documentation"
 | 
			
		||||
            formatApiCode={formatApiCode}
 | 
			
		||||
        >
 | 
			
		||||
            <ConditionallyRender
 | 
			
		||||
                condition={featureLimitReached}
 | 
			
		||||
                show={
 | 
			
		||||
                    <StyledAlert severity="error">
 | 
			
		||||
                        <strong>Feature toggle project limit reached. </strong>{' '}
 | 
			
		||||
                        To be able to create more feature toggles in this
 | 
			
		||||
                        project please increase the feature toggle upper limit
 | 
			
		||||
                        in the project settings.
 | 
			
		||||
                    </StyledAlert>
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <FeatureForm
 | 
			
		||||
                type={type}
 | 
			
		||||
                name={name}
 | 
			
		||||
@ -104,6 +141,7 @@ const CreateFeature = () => {
 | 
			
		||||
            >
 | 
			
		||||
                <CreateButton
 | 
			
		||||
                    name="feature toggle"
 | 
			
		||||
                    disabled={featureLimitReached}
 | 
			
		||||
                    permission={CREATE_FEATURE}
 | 
			
		||||
                    projectId={project}
 | 
			
		||||
                    data-testid={CF_CREATE_BTN_ID}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
import { isFeatureLimitReached } from './CreateFeature';
 | 
			
		||||
 | 
			
		||||
test('isFeatureLimitReached  should return false when featureLimit is null', async () => {
 | 
			
		||||
    expect(isFeatureLimitReached(null, 5)).toBe(false);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('isFeatureLimitReached  should return false when featureLimit is undefined', async () => {
 | 
			
		||||
    expect(isFeatureLimitReached(undefined, 5)).toBe(false);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('isFeatureLimitReached should return false when featureLimit is smaller current feature count', async () => {
 | 
			
		||||
    expect(isFeatureLimitReached(6, 5)).toBe(false);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('isFeatureLimitReached should return true when featureLimit is smaller current feature count', async () => {
 | 
			
		||||
    expect(isFeatureLimitReached(4, 5)).toBe(true);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('isFeatureLimitReached should return true when featureLimit is equal to current feature count', async () => {
 | 
			
		||||
    expect(isFeatureLimitReached(5, 5)).toBe(true);
 | 
			
		||||
});
 | 
			
		||||
@ -236,7 +236,6 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
 | 
			
		||||
                    </StyledRow>
 | 
			
		||||
                </StyledFormControl>
 | 
			
		||||
            </StyledContainer>
 | 
			
		||||
 | 
			
		||||
            <StyledButtonContainer>
 | 
			
		||||
                {children}
 | 
			
		||||
                <StyledCancelButton onClick={handleCancel}>
 | 
			
		||||
 | 
			
		||||
@ -230,8 +230,7 @@ const ProjectForm: React.FC<IProjectForm> = ({
 | 
			
		||||
                                <ConditionallyRender
 | 
			
		||||
                                    condition={
 | 
			
		||||
                                        featureCount !== undefined &&
 | 
			
		||||
                                        featureLimit !== undefined &&
 | 
			
		||||
                                        featureLimit.length > 0
 | 
			
		||||
                                        Boolean(featureLimit)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    show={
 | 
			
		||||
                                        <Box>
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,8 @@ const EditProject = () => {
 | 
			
		||||
        project.name,
 | 
			
		||||
        project.description,
 | 
			
		||||
        defaultStickiness,
 | 
			
		||||
        project.mode
 | 
			
		||||
        project.mode,
 | 
			
		||||
        project.featureLimit ? String(project.featureLimit) : ''
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const formatApiCode = () => {
 | 
			
		||||
 | 
			
		||||
@ -57,11 +57,18 @@ const useProjectForm = (
 | 
			
		||||
            name: projectName,
 | 
			
		||||
            description: projectDesc,
 | 
			
		||||
            defaultStickiness: projectStickiness,
 | 
			
		||||
            featureLimit: featureLimit,
 | 
			
		||||
            featureLimit: getFeatureLimitAsNumber(),
 | 
			
		||||
            mode: projectMode,
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const getFeatureLimitAsNumber = () => {
 | 
			
		||||
        if (featureLimit === '') {
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
        return Number(featureLimit);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const validateProjectId = async () => {
 | 
			
		||||
        if (projectId.length === 0) {
 | 
			
		||||
            setErrors(prev => ({ ...prev, id: 'Id can not be empty.' }));
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ export interface IProject {
 | 
			
		||||
    features: IFeatureToggleListItem[];
 | 
			
		||||
    mode: 'open' | 'protected';
 | 
			
		||||
    defaultStickiness: string;
 | 
			
		||||
    featureLimit?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IProjectHealthReport extends IProject {
 | 
			
		||||
 | 
			
		||||
@ -7,14 +7,12 @@ import {
 | 
			
		||||
    IFlagResolver,
 | 
			
		||||
    IProject,
 | 
			
		||||
    IProjectWithCount,
 | 
			
		||||
    ProjectMode,
 | 
			
		||||
} from '../types';
 | 
			
		||||
import {
 | 
			
		||||
    IProjectHealthUpdate,
 | 
			
		||||
    IProjectInsert,
 | 
			
		||||
    IProjectQuery,
 | 
			
		||||
    IProjectSettings,
 | 
			
		||||
    IProjectSettingsRow,
 | 
			
		||||
    IProjectStore,
 | 
			
		||||
    ProjectEnvironment,
 | 
			
		||||
} from '../types/stores/project-store';
 | 
			
		||||
@ -35,7 +33,11 @@ const COLUMNS = [
 | 
			
		||||
    'updated_at',
 | 
			
		||||
];
 | 
			
		||||
const TABLE = 'projects';
 | 
			
		||||
const SETTINGS_COLUMNS = ['project_mode', 'default_stickiness'];
 | 
			
		||||
const SETTINGS_COLUMNS = [
 | 
			
		||||
    'project_mode',
 | 
			
		||||
    'default_stickiness',
 | 
			
		||||
    'feature_limit',
 | 
			
		||||
];
 | 
			
		||||
const SETTINGS_TABLE = 'project_settings';
 | 
			
		||||
const PROJECT_ENVIRONMENTS = 'project_environments';
 | 
			
		||||
 | 
			
		||||
@ -94,6 +96,20 @@ class ProjectStore implements IProjectStore {
 | 
			
		||||
        return present;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async isFeatureLimitReached(id: string): Promise<boolean> {
 | 
			
		||||
        const result = await this.db.raw(
 | 
			
		||||
            `SELECT EXISTS(SELECT 1
 | 
			
		||||
             FROM project_settings
 | 
			
		||||
             LEFT JOIN features ON project_settings.project = features.project
 | 
			
		||||
             WHERE project_settings.project = ?
 | 
			
		||||
             GROUP BY project_settings.project
 | 
			
		||||
             HAVING project_settings.feature_limit <= COUNT(features.project)) AS present`,
 | 
			
		||||
            [id],
 | 
			
		||||
        );
 | 
			
		||||
        const { present } = result.rows[0];
 | 
			
		||||
        return present;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getProjectsWithCounts(
 | 
			
		||||
        query?: IProjectQuery,
 | 
			
		||||
        userId?: number,
 | 
			
		||||
@ -219,6 +235,7 @@ class ProjectStore implements IProjectStore {
 | 
			
		||||
                project: project.id,
 | 
			
		||||
                project_mode: project.mode,
 | 
			
		||||
                default_stickiness: project.defaultStickiness,
 | 
			
		||||
                feature_limit: project.featureLimit,
 | 
			
		||||
            })
 | 
			
		||||
            .returning('*');
 | 
			
		||||
        return this.mapRow({ ...row[0], ...settingsRow[0] });
 | 
			
		||||
@ -245,12 +262,14 @@ class ProjectStore implements IProjectStore {
 | 
			
		||||
                    .update({
 | 
			
		||||
                        project_mode: data.mode,
 | 
			
		||||
                        default_stickiness: data.defaultStickiness,
 | 
			
		||||
                        feature_limit: data.featureLimit,
 | 
			
		||||
                    });
 | 
			
		||||
            } else {
 | 
			
		||||
                await this.db(SETTINGS_TABLE).insert({
 | 
			
		||||
                    project: data.id,
 | 
			
		||||
                    project_mode: data.mode,
 | 
			
		||||
                    default_stickiness: data.defaultStickiness,
 | 
			
		||||
                    feature_limit: data.featureLimit,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
@ -486,24 +505,6 @@ class ProjectStore implements IProjectStore {
 | 
			
		||||
        return Number(members.count);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getProjectSettings(projectId: string): Promise<IProjectSettings> {
 | 
			
		||||
        const row = await this.db(SETTINGS_TABLE).where({ project: projectId });
 | 
			
		||||
        return this.mapSettingsRow(row[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async setProjectSettings(
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        defaultStickiness: string,
 | 
			
		||||
        mode: ProjectMode,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        await this.db(SETTINGS_TABLE)
 | 
			
		||||
            .update({
 | 
			
		||||
                default_stickiness: defaultStickiness,
 | 
			
		||||
                project_mode: mode,
 | 
			
		||||
            })
 | 
			
		||||
            .where({ project: projectId });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getDefaultStrategy(
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        environment: string,
 | 
			
		||||
@ -537,13 +538,6 @@ class ProjectStore implements IProjectStore {
 | 
			
		||||
            .then((res) => Number(res[0].count));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mapSettingsRow(row?: IProjectSettingsRow): IProjectSettings {
 | 
			
		||||
        return {
 | 
			
		||||
            defaultStickiness: row?.default_stickiness || 'default',
 | 
			
		||||
            mode: row?.project_mode || 'open',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
			
		||||
    mapLinkRow(row): IEnvironmentProjectLink {
 | 
			
		||||
        return {
 | 
			
		||||
@ -567,6 +561,7 @@ class ProjectStore implements IProjectStore {
 | 
			
		||||
            updatedAt: row.updated_at || new Date(),
 | 
			
		||||
            mode: row.project_mode || 'open',
 | 
			
		||||
            defaultStickiness: row.default_stickiness || 'default',
 | 
			
		||||
            featureLimit: row.feature_limit,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ import {
 | 
			
		||||
    UPDATE_FEATURE_ENVIRONMENT_VARIANTS,
 | 
			
		||||
    UPDATE_TAG_TYPE,
 | 
			
		||||
} from '../../types';
 | 
			
		||||
import { InvalidOperationError } from '../../error';
 | 
			
		||||
import { PermissionError } from '../../error';
 | 
			
		||||
 | 
			
		||||
type Mode = 'regular' | 'change_request';
 | 
			
		||||
 | 
			
		||||
@ -149,9 +149,7 @@ export class ImportPermissionsService {
 | 
			
		||||
            mode,
 | 
			
		||||
        );
 | 
			
		||||
        if (missingPermissions.length > 0) {
 | 
			
		||||
            throw new InvalidOperationError(
 | 
			
		||||
                'You are missing permissions to import',
 | 
			
		||||
            );
 | 
			
		||||
            throw new PermissionError(missingPermissions);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,13 @@ export const healthOverviewSchema = {
 | 
			
		||||
            description:
 | 
			
		||||
                "The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",
 | 
			
		||||
        },
 | 
			
		||||
        featureLimit: {
 | 
			
		||||
            type: 'number',
 | 
			
		||||
            nullable: true,
 | 
			
		||||
            example: 100,
 | 
			
		||||
            description:
 | 
			
		||||
                'A limit on the number of features allowed in the project. Null if no limit.',
 | 
			
		||||
        },
 | 
			
		||||
        members: {
 | 
			
		||||
            type: 'integer',
 | 
			
		||||
            description: 'The number of users/members in the project.',
 | 
			
		||||
 | 
			
		||||
@ -51,6 +51,13 @@ export const projectOverviewSchema = {
 | 
			
		||||
            description:
 | 
			
		||||
                "The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",
 | 
			
		||||
        },
 | 
			
		||||
        featureLimit: {
 | 
			
		||||
            type: 'number',
 | 
			
		||||
            nullable: true,
 | 
			
		||||
            example: 100,
 | 
			
		||||
            description:
 | 
			
		||||
                'A limit on the number of features allowed in the project. Null if no limit.',
 | 
			
		||||
        },
 | 
			
		||||
        members: {
 | 
			
		||||
            type: 'number',
 | 
			
		||||
            example: 4,
 | 
			
		||||
 | 
			
		||||
@ -921,6 +921,15 @@ class FeatureToggleService {
 | 
			
		||||
        this.logger.info(`${createdBy} creates feature toggle ${value.name}`);
 | 
			
		||||
        await this.validateName(value.name);
 | 
			
		||||
        const exists = await this.projectStore.hasProject(projectId);
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            this.flagResolver.isEnabled('newProjectLayout') &&
 | 
			
		||||
            (await this.projectStore.isFeatureLimitReached(projectId))
 | 
			
		||||
        ) {
 | 
			
		||||
            throw new InvalidOperationError(
 | 
			
		||||
                'You have reached the maximum number of feature toggles for this project.',
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        if (exists) {
 | 
			
		||||
            let featureData;
 | 
			
		||||
            if (isValidated) {
 | 
			
		||||
 | 
			
		||||
@ -9,5 +9,6 @@ export const projectSchema = joi
 | 
			
		||||
        description: joi.string().allow(null).allow('').optional(),
 | 
			
		||||
        mode: joi.string().valid('open', 'protected').default('open'),
 | 
			
		||||
        defaultStickiness: joi.string().default('default'),
 | 
			
		||||
        featureLimit: joi.number().allow(null).optional(),
 | 
			
		||||
    })
 | 
			
		||||
    .options({ allowUnknown: false, stripUnknown: true });
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,6 @@ import {
 | 
			
		||||
    ProjectGroupAddedEvent,
 | 
			
		||||
    ProjectGroupRemovedEvent,
 | 
			
		||||
    ProjectGroupUpdateRoleEvent,
 | 
			
		||||
    ProjectMode,
 | 
			
		||||
    ProjectUserAddedEvent,
 | 
			
		||||
    ProjectUserRemovedEvent,
 | 
			
		||||
    ProjectUserUpdateRoleEvent,
 | 
			
		||||
@ -37,11 +36,7 @@ import {
 | 
			
		||||
    IFlagResolver,
 | 
			
		||||
    ProjectAccessAddedEvent,
 | 
			
		||||
} from '../types';
 | 
			
		||||
import {
 | 
			
		||||
    IProjectQuery,
 | 
			
		||||
    IProjectSettings,
 | 
			
		||||
    IProjectStore,
 | 
			
		||||
} from '../types/stores/project-store';
 | 
			
		||||
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
 | 
			
		||||
import {
 | 
			
		||||
    IProjectAccessModel,
 | 
			
		||||
    IRoleDescriptor,
 | 
			
		||||
@ -836,6 +831,7 @@ export default class ProjectService {
 | 
			
		||||
            name: project.name,
 | 
			
		||||
            description: project.description,
 | 
			
		||||
            mode: project.mode,
 | 
			
		||||
            featureLimit: project.featureLimit,
 | 
			
		||||
            defaultStickiness: project.defaultStickiness,
 | 
			
		||||
            health: project.health || 0,
 | 
			
		||||
            favorite: favorite,
 | 
			
		||||
@ -847,20 +843,4 @@ export default class ProjectService {
 | 
			
		||||
            version: 1,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getProjectSettings(projectId: string): Promise<IProjectSettings> {
 | 
			
		||||
        return this.store.getProjectSettings(projectId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async setProjectSettings(
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        defaultStickiness: string,
 | 
			
		||||
        mode: ProjectMode,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        return this.store.setProjectSettings(
 | 
			
		||||
            projectId,
 | 
			
		||||
            defaultStickiness,
 | 
			
		||||
            mode,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -196,7 +196,7 @@ export interface IProjectOverview {
 | 
			
		||||
    createdAt: Date | undefined;
 | 
			
		||||
    stats?: IProjectStats;
 | 
			
		||||
    mode: ProjectMode;
 | 
			
		||||
 | 
			
		||||
    featureLimit?: number;
 | 
			
		||||
    defaultStickiness: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -384,6 +384,7 @@ export interface IProject {
 | 
			
		||||
    changeRequestsEnabled?: boolean;
 | 
			
		||||
    mode: ProjectMode;
 | 
			
		||||
    defaultStickiness: string;
 | 
			
		||||
    featureLimit?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -18,11 +18,13 @@ export interface IProjectInsert {
 | 
			
		||||
    updatedAt?: Date;
 | 
			
		||||
    changeRequestsEnabled?: boolean;
 | 
			
		||||
    mode: ProjectMode;
 | 
			
		||||
    featureLimit?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IProjectSettings {
 | 
			
		||||
    mode: ProjectMode;
 | 
			
		||||
    defaultStickiness: string;
 | 
			
		||||
    featureLimit?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IProjectSettingsRow {
 | 
			
		||||
@ -55,11 +57,6 @@ export type ProjectEnvironment = {
 | 
			
		||||
    defaultStrategy?: CreateFeatureStrategySchema;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface IProjectEnvironmentWithChangeRequests {
 | 
			
		||||
    environment: string;
 | 
			
		||||
    changeRequestsEnabled: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IProjectStore extends Store<IProject, string> {
 | 
			
		||||
    hasProject(id: string): Promise<boolean>;
 | 
			
		||||
 | 
			
		||||
@ -109,13 +106,6 @@ export interface IProjectStore extends Store<IProject, string> {
 | 
			
		||||
        projects: string[],
 | 
			
		||||
    ): Promise<void>;
 | 
			
		||||
 | 
			
		||||
    getProjectSettings(projectId: string): Promise<IProjectSettings>;
 | 
			
		||||
    setProjectSettings(
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        defaultStickiness: string,
 | 
			
		||||
        mode: ProjectMode,
 | 
			
		||||
    ): Promise<void>;
 | 
			
		||||
 | 
			
		||||
    getDefaultStrategy(
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        environment: string,
 | 
			
		||||
@ -125,4 +115,6 @@ export interface IProjectStore extends Store<IProject, string> {
 | 
			
		||||
        environment: string,
 | 
			
		||||
        strategy: CreateFeatureStrategySchema,
 | 
			
		||||
    ): Promise<CreateFeatureStrategySchema>;
 | 
			
		||||
 | 
			
		||||
    isFeatureLimitReached(id: string): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								src/migrations/20230711163311-project-feature-limit.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/migrations/20230711163311-project-feature-limit.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
exports.up = function (db, cb) {
 | 
			
		||||
    db.runSql(
 | 
			
		||||
        `
 | 
			
		||||
        ALTER TABLE project_settings
 | 
			
		||||
            ADD COLUMN IF NOT EXISTS "feature_limit" integer;
 | 
			
		||||
        `,
 | 
			
		||||
        cb(),
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
exports.down = function (db, cb) {
 | 
			
		||||
    db.runSql(
 | 
			
		||||
        `
 | 
			
		||||
        ALTER TABLE project_settings DROP COLUMN IF EXISTS "feature_limit";
 | 
			
		||||
        `,
 | 
			
		||||
        cb,
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										29
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							@ -1,16 +1,10 @@
 | 
			
		||||
import {
 | 
			
		||||
    IProjectHealthUpdate,
 | 
			
		||||
    IProjectInsert,
 | 
			
		||||
    IProjectSettings,
 | 
			
		||||
    IProjectStore,
 | 
			
		||||
    ProjectEnvironment,
 | 
			
		||||
} from '../../lib/types/stores/project-store';
 | 
			
		||||
import {
 | 
			
		||||
    IEnvironment,
 | 
			
		||||
    IProject,
 | 
			
		||||
    IProjectWithCount,
 | 
			
		||||
    ProjectMode,
 | 
			
		||||
} from '../../lib/types';
 | 
			
		||||
import { IEnvironment, IProject, IProjectWithCount } from '../../lib/types';
 | 
			
		||||
import NotFoundError from '../../lib/error/notfound-error';
 | 
			
		||||
import {
 | 
			
		||||
    IEnvironmentProjectLink,
 | 
			
		||||
@ -167,22 +161,6 @@ export default class FakeProjectStore implements IProjectStore {
 | 
			
		||||
        throw new Error('Method not implemented');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    getProjectSettings(projectId: string): Promise<IProjectSettings> {
 | 
			
		||||
        throw new Error('Method not implemented.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setProjectSettings(
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        defaultStickiness: string,
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        mode: ProjectMode,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        throw new Error('Method not implemented.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateDefaultStrategy(
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
        projectId: string,
 | 
			
		||||
@ -202,4 +180,9 @@ export default class FakeProjectStore implements IProjectStore {
 | 
			
		||||
    ): Promise<CreateFeatureStrategySchema | undefined> {
 | 
			
		||||
        throw new Error('Method not implemented.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    isFeatureLimitReached(id: string): Promise<boolean> {
 | 
			
		||||
        return Promise.resolve(false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user