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); |             deleteButtons.push(...removeButton); | ||||||
|         }) |         }) | ||||||
|     ); |     ); | ||||||
|     console.log(deleteButtons); |  | ||||||
|     return deleteButtons; |     return deleteButtons; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -12,6 +12,24 @@ import UIContext from 'contexts/UIContext'; | |||||||
| import { CF_CREATE_BTN_ID } from 'utils/testIds'; | import { CF_CREATE_BTN_ID } from 'utils/testIds'; | ||||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | import { formatUnknownError } from 'utils/formatUnknownError'; | ||||||
| import { GO_BACK } from 'constants/navigate'; | 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 CreateFeature = () => { | ||||||
|     const { setToastData, setToastApiError } = useToast(); |     const { setToastData, setToastApiError } = useToast(); | ||||||
| @ -36,6 +54,8 @@ const CreateFeature = () => { | |||||||
|         errors, |         errors, | ||||||
|     } = useFeatureForm(); |     } = useFeatureForm(); | ||||||
| 
 | 
 | ||||||
|  |     const { project: projectInfo } = useProject(project); | ||||||
|  | 
 | ||||||
|     const { createFeatureToggle, loading } = useFeatureApi(); |     const { createFeatureToggle, loading } = useFeatureApi(); | ||||||
| 
 | 
 | ||||||
|     const handleSubmit = async (e: Event) => { |     const handleSubmit = async (e: Event) => { | ||||||
| @ -74,6 +94,11 @@ const CreateFeature = () => { | |||||||
|         navigate(GO_BACK); |         navigate(GO_BACK); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     const featureLimitReached = | ||||||
|  |         isFeatureLimitReached( | ||||||
|  |             projectInfo.featureLimit, | ||||||
|  |             projectInfo.features.length | ||||||
|  |         ) && Boolean(uiConfig.flags.newProjectLayout); | ||||||
|     return ( |     return ( | ||||||
|         <FormTemplate |         <FormTemplate | ||||||
|             loading={loading} |             loading={loading} | ||||||
| @ -84,6 +109,18 @@ const CreateFeature = () => { | |||||||
|             documentationLinkLabel="Feature toggle types documentation" |             documentationLinkLabel="Feature toggle types documentation" | ||||||
|             formatApiCode={formatApiCode} |             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 |             <FeatureForm | ||||||
|                 type={type} |                 type={type} | ||||||
|                 name={name} |                 name={name} | ||||||
| @ -104,6 +141,7 @@ const CreateFeature = () => { | |||||||
|             > |             > | ||||||
|                 <CreateButton |                 <CreateButton | ||||||
|                     name="feature toggle" |                     name="feature toggle" | ||||||
|  |                     disabled={featureLimitReached} | ||||||
|                     permission={CREATE_FEATURE} |                     permission={CREATE_FEATURE} | ||||||
|                     projectId={project} |                     projectId={project} | ||||||
|                     data-testid={CF_CREATE_BTN_ID} |                     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> |                     </StyledRow> | ||||||
|                 </StyledFormControl> |                 </StyledFormControl> | ||||||
|             </StyledContainer> |             </StyledContainer> | ||||||
| 
 |  | ||||||
|             <StyledButtonContainer> |             <StyledButtonContainer> | ||||||
|                 {children} |                 {children} | ||||||
|                 <StyledCancelButton onClick={handleCancel}> |                 <StyledCancelButton onClick={handleCancel}> | ||||||
|  | |||||||
| @ -230,8 +230,7 @@ const ProjectForm: React.FC<IProjectForm> = ({ | |||||||
|                                 <ConditionallyRender |                                 <ConditionallyRender | ||||||
|                                     condition={ |                                     condition={ | ||||||
|                                         featureCount !== undefined && |                                         featureCount !== undefined && | ||||||
|                                         featureLimit !== undefined && |                                         Boolean(featureLimit) | ||||||
|                                         featureLimit.length > 0 |  | ||||||
|                                     } |                                     } | ||||||
|                                     show={ |                                     show={ | ||||||
|                                         <Box> |                                         <Box> | ||||||
|  | |||||||
| @ -54,7 +54,8 @@ const EditProject = () => { | |||||||
|         project.name, |         project.name, | ||||||
|         project.description, |         project.description, | ||||||
|         defaultStickiness, |         defaultStickiness, | ||||||
|         project.mode |         project.mode, | ||||||
|  |         project.featureLimit ? String(project.featureLimit) : '' | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const formatApiCode = () => { |     const formatApiCode = () => { | ||||||
|  | |||||||
| @ -57,11 +57,18 @@ const useProjectForm = ( | |||||||
|             name: projectName, |             name: projectName, | ||||||
|             description: projectDesc, |             description: projectDesc, | ||||||
|             defaultStickiness: projectStickiness, |             defaultStickiness: projectStickiness, | ||||||
|             featureLimit: featureLimit, |             featureLimit: getFeatureLimitAsNumber(), | ||||||
|             mode: projectMode, |             mode: projectMode, | ||||||
|         }; |         }; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  |     const getFeatureLimitAsNumber = () => { | ||||||
|  |         if (featureLimit === '') { | ||||||
|  |             return undefined; | ||||||
|  |         } | ||||||
|  |         return Number(featureLimit); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|     const validateProjectId = async () => { |     const validateProjectId = async () => { | ||||||
|         if (projectId.length === 0) { |         if (projectId.length === 0) { | ||||||
|             setErrors(prev => ({ ...prev, id: 'Id can not be empty.' })); |             setErrors(prev => ({ ...prev, id: 'Id can not be empty.' })); | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ export interface IProject { | |||||||
|     features: IFeatureToggleListItem[]; |     features: IFeatureToggleListItem[]; | ||||||
|     mode: 'open' | 'protected'; |     mode: 'open' | 'protected'; | ||||||
|     defaultStickiness: string; |     defaultStickiness: string; | ||||||
|  |     featureLimit?: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface IProjectHealthReport extends IProject { | export interface IProjectHealthReport extends IProject { | ||||||
|  | |||||||
| @ -7,14 +7,12 @@ import { | |||||||
|     IFlagResolver, |     IFlagResolver, | ||||||
|     IProject, |     IProject, | ||||||
|     IProjectWithCount, |     IProjectWithCount, | ||||||
|     ProjectMode, |  | ||||||
| } from '../types'; | } from '../types'; | ||||||
| import { | import { | ||||||
|     IProjectHealthUpdate, |     IProjectHealthUpdate, | ||||||
|     IProjectInsert, |     IProjectInsert, | ||||||
|     IProjectQuery, |     IProjectQuery, | ||||||
|     IProjectSettings, |     IProjectSettings, | ||||||
|     IProjectSettingsRow, |  | ||||||
|     IProjectStore, |     IProjectStore, | ||||||
|     ProjectEnvironment, |     ProjectEnvironment, | ||||||
| } from '../types/stores/project-store'; | } from '../types/stores/project-store'; | ||||||
| @ -35,7 +33,11 @@ const COLUMNS = [ | |||||||
|     'updated_at', |     'updated_at', | ||||||
| ]; | ]; | ||||||
| const TABLE = 'projects'; | 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 SETTINGS_TABLE = 'project_settings'; | ||||||
| const PROJECT_ENVIRONMENTS = 'project_environments'; | const PROJECT_ENVIRONMENTS = 'project_environments'; | ||||||
| 
 | 
 | ||||||
| @ -94,6 +96,20 @@ class ProjectStore implements IProjectStore { | |||||||
|         return present; |         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( |     async getProjectsWithCounts( | ||||||
|         query?: IProjectQuery, |         query?: IProjectQuery, | ||||||
|         userId?: number, |         userId?: number, | ||||||
| @ -219,6 +235,7 @@ class ProjectStore implements IProjectStore { | |||||||
|                 project: project.id, |                 project: project.id, | ||||||
|                 project_mode: project.mode, |                 project_mode: project.mode, | ||||||
|                 default_stickiness: project.defaultStickiness, |                 default_stickiness: project.defaultStickiness, | ||||||
|  |                 feature_limit: project.featureLimit, | ||||||
|             }) |             }) | ||||||
|             .returning('*'); |             .returning('*'); | ||||||
|         return this.mapRow({ ...row[0], ...settingsRow[0] }); |         return this.mapRow({ ...row[0], ...settingsRow[0] }); | ||||||
| @ -245,12 +262,14 @@ class ProjectStore implements IProjectStore { | |||||||
|                     .update({ |                     .update({ | ||||||
|                         project_mode: data.mode, |                         project_mode: data.mode, | ||||||
|                         default_stickiness: data.defaultStickiness, |                         default_stickiness: data.defaultStickiness, | ||||||
|  |                         feature_limit: data.featureLimit, | ||||||
|                     }); |                     }); | ||||||
|             } else { |             } else { | ||||||
|                 await this.db(SETTINGS_TABLE).insert({ |                 await this.db(SETTINGS_TABLE).insert({ | ||||||
|                     project: data.id, |                     project: data.id, | ||||||
|                     project_mode: data.mode, |                     project_mode: data.mode, | ||||||
|                     default_stickiness: data.defaultStickiness, |                     default_stickiness: data.defaultStickiness, | ||||||
|  |                     feature_limit: data.featureLimit, | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         } catch (err) { |         } catch (err) { | ||||||
| @ -486,24 +505,6 @@ class ProjectStore implements IProjectStore { | |||||||
|         return Number(members.count); |         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( |     async getDefaultStrategy( | ||||||
|         projectId: string, |         projectId: string, | ||||||
|         environment: string, |         environment: string, | ||||||
| @ -537,13 +538,6 @@ class ProjectStore implements IProjectStore { | |||||||
|             .then((res) => Number(res[0].count)); |             .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
 |     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | ||||||
|     mapLinkRow(row): IEnvironmentProjectLink { |     mapLinkRow(row): IEnvironmentProjectLink { | ||||||
|         return { |         return { | ||||||
| @ -567,6 +561,7 @@ class ProjectStore implements IProjectStore { | |||||||
|             updatedAt: row.updated_at || new Date(), |             updatedAt: row.updated_at || new Date(), | ||||||
|             mode: row.project_mode || 'open', |             mode: row.project_mode || 'open', | ||||||
|             defaultStickiness: row.default_stickiness || 'default', |             defaultStickiness: row.default_stickiness || 'default', | ||||||
|  |             featureLimit: row.feature_limit, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ import { | |||||||
|     UPDATE_FEATURE_ENVIRONMENT_VARIANTS, |     UPDATE_FEATURE_ENVIRONMENT_VARIANTS, | ||||||
|     UPDATE_TAG_TYPE, |     UPDATE_TAG_TYPE, | ||||||
| } from '../../types'; | } from '../../types'; | ||||||
| import { InvalidOperationError } from '../../error'; | import { PermissionError } from '../../error'; | ||||||
| 
 | 
 | ||||||
| type Mode = 'regular' | 'change_request'; | type Mode = 'regular' | 'change_request'; | ||||||
| 
 | 
 | ||||||
| @ -149,9 +149,7 @@ export class ImportPermissionsService { | |||||||
|             mode, |             mode, | ||||||
|         ); |         ); | ||||||
|         if (missingPermissions.length > 0) { |         if (missingPermissions.length > 0) { | ||||||
|             throw new InvalidOperationError( |             throw new PermissionError(missingPermissions); | ||||||
|                 'You are missing permissions to import', |  | ||||||
|             ); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -56,6 +56,13 @@ export const healthOverviewSchema = { | |||||||
|             description: |             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.", |                 "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: { |         members: { | ||||||
|             type: 'integer', |             type: 'integer', | ||||||
|             description: 'The number of users/members in the project.', |             description: 'The number of users/members in the project.', | ||||||
|  | |||||||
| @ -51,6 +51,13 @@ export const projectOverviewSchema = { | |||||||
|             description: |             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.", |                 "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: { |         members: { | ||||||
|             type: 'number', |             type: 'number', | ||||||
|             example: 4, |             example: 4, | ||||||
|  | |||||||
| @ -921,6 +921,15 @@ class FeatureToggleService { | |||||||
|         this.logger.info(`${createdBy} creates feature toggle ${value.name}`); |         this.logger.info(`${createdBy} creates feature toggle ${value.name}`); | ||||||
|         await this.validateName(value.name); |         await this.validateName(value.name); | ||||||
|         const exists = await this.projectStore.hasProject(projectId); |         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) { |         if (exists) { | ||||||
|             let featureData; |             let featureData; | ||||||
|             if (isValidated) { |             if (isValidated) { | ||||||
|  | |||||||
| @ -9,5 +9,6 @@ export const projectSchema = joi | |||||||
|         description: joi.string().allow(null).allow('').optional(), |         description: joi.string().allow(null).allow('').optional(), | ||||||
|         mode: joi.string().valid('open', 'protected').default('open'), |         mode: joi.string().valid('open', 'protected').default('open'), | ||||||
|         defaultStickiness: joi.string().default('default'), |         defaultStickiness: joi.string().default('default'), | ||||||
|  |         featureLimit: joi.number().allow(null).optional(), | ||||||
|     }) |     }) | ||||||
|     .options({ allowUnknown: false, stripUnknown: true }); |     .options({ allowUnknown: false, stripUnknown: true }); | ||||||
|  | |||||||
| @ -29,7 +29,6 @@ import { | |||||||
|     ProjectGroupAddedEvent, |     ProjectGroupAddedEvent, | ||||||
|     ProjectGroupRemovedEvent, |     ProjectGroupRemovedEvent, | ||||||
|     ProjectGroupUpdateRoleEvent, |     ProjectGroupUpdateRoleEvent, | ||||||
|     ProjectMode, |  | ||||||
|     ProjectUserAddedEvent, |     ProjectUserAddedEvent, | ||||||
|     ProjectUserRemovedEvent, |     ProjectUserRemovedEvent, | ||||||
|     ProjectUserUpdateRoleEvent, |     ProjectUserUpdateRoleEvent, | ||||||
| @ -37,11 +36,7 @@ import { | |||||||
|     IFlagResolver, |     IFlagResolver, | ||||||
|     ProjectAccessAddedEvent, |     ProjectAccessAddedEvent, | ||||||
| } from '../types'; | } from '../types'; | ||||||
| import { | import { IProjectQuery, IProjectStore } from '../types/stores/project-store'; | ||||||
|     IProjectQuery, |  | ||||||
|     IProjectSettings, |  | ||||||
|     IProjectStore, |  | ||||||
| } from '../types/stores/project-store'; |  | ||||||
| import { | import { | ||||||
|     IProjectAccessModel, |     IProjectAccessModel, | ||||||
|     IRoleDescriptor, |     IRoleDescriptor, | ||||||
| @ -836,6 +831,7 @@ export default class ProjectService { | |||||||
|             name: project.name, |             name: project.name, | ||||||
|             description: project.description, |             description: project.description, | ||||||
|             mode: project.mode, |             mode: project.mode, | ||||||
|  |             featureLimit: project.featureLimit, | ||||||
|             defaultStickiness: project.defaultStickiness, |             defaultStickiness: project.defaultStickiness, | ||||||
|             health: project.health || 0, |             health: project.health || 0, | ||||||
|             favorite: favorite, |             favorite: favorite, | ||||||
| @ -847,20 +843,4 @@ export default class ProjectService { | |||||||
|             version: 1, |             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; |     createdAt: Date | undefined; | ||||||
|     stats?: IProjectStats; |     stats?: IProjectStats; | ||||||
|     mode: ProjectMode; |     mode: ProjectMode; | ||||||
| 
 |     featureLimit?: number; | ||||||
|     defaultStickiness: string; |     defaultStickiness: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -384,6 +384,7 @@ export interface IProject { | |||||||
|     changeRequestsEnabled?: boolean; |     changeRequestsEnabled?: boolean; | ||||||
|     mode: ProjectMode; |     mode: ProjectMode; | ||||||
|     defaultStickiness: string; |     defaultStickiness: string; | ||||||
|  |     featureLimit?: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  | |||||||
| @ -18,11 +18,13 @@ export interface IProjectInsert { | |||||||
|     updatedAt?: Date; |     updatedAt?: Date; | ||||||
|     changeRequestsEnabled?: boolean; |     changeRequestsEnabled?: boolean; | ||||||
|     mode: ProjectMode; |     mode: ProjectMode; | ||||||
|  |     featureLimit?: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface IProjectSettings { | export interface IProjectSettings { | ||||||
|     mode: ProjectMode; |     mode: ProjectMode; | ||||||
|     defaultStickiness: string; |     defaultStickiness: string; | ||||||
|  |     featureLimit?: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface IProjectSettingsRow { | export interface IProjectSettingsRow { | ||||||
| @ -55,11 +57,6 @@ export type ProjectEnvironment = { | |||||||
|     defaultStrategy?: CreateFeatureStrategySchema; |     defaultStrategy?: CreateFeatureStrategySchema; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export interface IProjectEnvironmentWithChangeRequests { |  | ||||||
|     environment: string; |  | ||||||
|     changeRequestsEnabled: boolean; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export interface IProjectStore extends Store<IProject, string> { | export interface IProjectStore extends Store<IProject, string> { | ||||||
|     hasProject(id: string): Promise<boolean>; |     hasProject(id: string): Promise<boolean>; | ||||||
| 
 | 
 | ||||||
| @ -109,13 +106,6 @@ export interface IProjectStore extends Store<IProject, string> { | |||||||
|         projects: string[], |         projects: string[], | ||||||
|     ): Promise<void>; |     ): Promise<void>; | ||||||
| 
 | 
 | ||||||
|     getProjectSettings(projectId: string): Promise<IProjectSettings>; |  | ||||||
|     setProjectSettings( |  | ||||||
|         projectId: string, |  | ||||||
|         defaultStickiness: string, |  | ||||||
|         mode: ProjectMode, |  | ||||||
|     ): Promise<void>; |  | ||||||
| 
 |  | ||||||
|     getDefaultStrategy( |     getDefaultStrategy( | ||||||
|         projectId: string, |         projectId: string, | ||||||
|         environment: string, |         environment: string, | ||||||
| @ -125,4 +115,6 @@ export interface IProjectStore extends Store<IProject, string> { | |||||||
|         environment: string, |         environment: string, | ||||||
|         strategy: CreateFeatureStrategySchema, |         strategy: CreateFeatureStrategySchema, | ||||||
|     ): Promise<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 { | import { | ||||||
|     IProjectHealthUpdate, |     IProjectHealthUpdate, | ||||||
|     IProjectInsert, |     IProjectInsert, | ||||||
|     IProjectSettings, |  | ||||||
|     IProjectStore, |     IProjectStore, | ||||||
|     ProjectEnvironment, |     ProjectEnvironment, | ||||||
| } from '../../lib/types/stores/project-store'; | } from '../../lib/types/stores/project-store'; | ||||||
| import { | import { IEnvironment, IProject, IProjectWithCount } from '../../lib/types'; | ||||||
|     IEnvironment, |  | ||||||
|     IProject, |  | ||||||
|     IProjectWithCount, |  | ||||||
|     ProjectMode, |  | ||||||
| } from '../../lib/types'; |  | ||||||
| import NotFoundError from '../../lib/error/notfound-error'; | import NotFoundError from '../../lib/error/notfound-error'; | ||||||
| import { | import { | ||||||
|     IEnvironmentProjectLink, |     IEnvironmentProjectLink, | ||||||
| @ -167,22 +161,6 @@ export default class FakeProjectStore implements IProjectStore { | |||||||
|         throw new Error('Method not implemented'); |         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( |     updateDefaultStrategy( | ||||||
|         // eslint-disable-next-line @typescript-eslint/no-unused-vars
 |         // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|         projectId: string, |         projectId: string, | ||||||
| @ -202,4 +180,9 @@ export default class FakeProjectStore implements IProjectStore { | |||||||
|     ): Promise<CreateFeatureStrategySchema | undefined> { |     ): Promise<CreateFeatureStrategySchema | undefined> { | ||||||
|         throw new Error('Method not implemented.'); |         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