diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index 0d66d4e09a..f51e5461b8 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -14,6 +14,7 @@ import { IProjectInsert, IProjectQuery, IProjectSettings, + IProjectEnterpriseSettingsUpdate, IProjectStore, ProjectEnvironment, } from '../types/stores/project-store'; @@ -244,10 +245,6 @@ class ProjectStore implements IProjectStore { project: project.id, project_mode: project.mode, default_stickiness: project.defaultStickiness, - feature_limit: project.featureLimit, - feature_naming_pattern: project.featureNaming?.pattern, - feature_naming_example: project.featureNaming?.example, - feature_naming_description: project.featureNaming?.description, }) .returning('*'); return this.mapRow({ ...row[0], ...settingsRow[0] }); @@ -272,9 +269,30 @@ class ProjectStore implements IProjectStore { await this.db(SETTINGS_TABLE) .where({ project: data.id }) .update({ - project_mode: data.mode, default_stickiness: data.defaultStickiness, feature_limit: data.featureLimit, + }); + } else { + await this.db(SETTINGS_TABLE).insert({ + project: data.id, + default_stickiness: data.defaultStickiness, + feature_limit: data.featureLimit, + }); + } + } catch (err) { + this.logger.error('Could not update project, error: ', err); + } + } + + async updateProjectEnterpriseSettings( + data: IProjectEnterpriseSettingsUpdate, + ): Promise { + try { + if (await this.hasProjectSettings(data.id)) { + await this.db(SETTINGS_TABLE) + .where({ project: data.id }) + .update({ + project_mode: data.mode, feature_naming_pattern: data.featureNaming?.pattern, feature_naming_example: data.featureNaming?.example, feature_naming_description: @@ -284,15 +302,16 @@ class ProjectStore implements IProjectStore { await this.db(SETTINGS_TABLE).insert({ project: data.id, project_mode: data.mode, - default_stickiness: data.defaultStickiness, - feature_limit: data.featureLimit, feature_naming_pattern: data.featureNaming?.pattern, feature_naming_example: data.featureNaming?.example, feature_naming_description: data.featureNaming?.description, }); } } catch (err) { - this.logger.error('Could not update project, error: ', err); + this.logger.error( + 'Could not update project settings, error: ', + err, + ); } } diff --git a/src/lib/features/export-import-toggles/export-import.e2e.test.ts b/src/lib/features/export-import-toggles/export-import.e2e.test.ts index dd8c0e44b0..a12164e2b1 100644 --- a/src/lib/features/export-import-toggles/export-import.e2e.test.ts +++ b/src/lib/features/export-import-toggles/export-import.e2e.test.ts @@ -113,13 +113,16 @@ const createProjects = async ( type: 'production', }); for (const project of projects) { - await db.stores.projectStore.create({ + const storedProject = { name: project, description: '', id: project, mode: 'open' as const, featureLimit, - }); + }; + await db.stores.projectStore.create(storedProject); + await db.stores.projectStore.update(storedProject); + await app.linkProjectToEnvironment(project, DEFAULT_ENV); } }; @@ -884,10 +887,8 @@ test('validate import data', async () => { // note: this must be done after creating the feature on the earlier lines, // to prevent the pattern from blocking the creation. - await projectStore.update({ + await projectStore.updateProjectEnterpriseSettings({ id: DEFAULT_PROJECT, - name: 'default', - description: '', mode: 'open', featureNaming: { pattern: 'testpattern.+' }, }); @@ -996,6 +997,9 @@ test(`should give errors with flag names if the flags don't match the project pa description: '', id: project, mode: 'open' as const, + }); + await db.stores.projectStore.updateProjectEnterpriseSettings({ + id: project, featureNaming: { pattern }, }); await app.linkProjectToEnvironment(project, DEFAULT_ENV); diff --git a/src/lib/openapi/spec/__snapshots__/project-overview-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/project-overview-schema.test.ts.snap new file mode 100644 index 0000000000..fde0ce3695 --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/project-overview-schema.test.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`updateProjectEnterpriseSettings schema 1`] = ` +{ + "errors": [ + { + "instancePath": "", + "keyword": "required", + "message": "must have required property 'version'", + "params": { + "missingProperty": "version", + }, + "schemaPath": "#/required", + }, + ], + "schema": "#/components/schemas/projectOverviewSchema", +} +`; diff --git a/src/lib/openapi/spec/project-overview-schema.test.ts b/src/lib/openapi/spec/project-overview-schema.test.ts new file mode 100644 index 0000000000..339b926a18 --- /dev/null +++ b/src/lib/openapi/spec/project-overview-schema.test.ts @@ -0,0 +1,22 @@ +import { ProjectOverviewSchema } from './project-overview-schema'; +import { validateSchema } from '../validate'; + +test('updateProjectEnterpriseSettings schema', () => { + const data: ProjectOverviewSchema = { + name: 'project', + version: 3, + featureNaming: { + description: 'naming description', + example: 'a', + pattern: '[aZ]', + }, + }; + + expect( + validateSchema('#/components/schemas/projectOverviewSchema', data), + ).toBeUndefined(); + + expect( + validateSchema('#/components/schemas/projectOverviewSchema', {}), + ).toMatchSnapshot(); +}); diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index ba2c76677a..016bbc1f8e 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -41,7 +41,11 @@ import { IFeatureNaming, CreateProject, } from '../types'; -import { IProjectQuery, IProjectStore } from '../types/stores/project-store'; +import { + IProjectQuery, + IProjectEnterpriseSettingsUpdate, + IProjectStore, +} from '../types/stores/project-store'; import { IProjectAccessModel, IRoleDescriptor, @@ -83,7 +87,7 @@ interface ICalculateStatus { } export default class ProjectService { - private store: IProjectStore; + private projectStore: IProjectStore; private accessService: AccessService; @@ -113,6 +117,8 @@ export default class ProjectService { private flagResolver: IFlagResolver; + private isEnterprise: boolean; + constructor( { projectStore, @@ -141,7 +147,7 @@ export default class ProjectService { favoriteService: FavoritesService, privateProjectChecker: IPrivateProjectChecker, ) { - this.store = projectStore; + this.projectStore = projectStore; this.environmentStore = environmentStore; this.featureEnvironmentStore = featureEnvironmentStore; this.accessService = accessService; @@ -156,13 +162,17 @@ export default class ProjectService { this.projectStatsStore = projectStatsStore; this.logger = config.getLogger('services/project-service.js'); this.flagResolver = config.flagResolver; + this.isEnterprise = config.isEnterprise; } async getProjects( query?: IProjectQuery, userId?: number, ): Promise { - const projects = await this.store.getProjectsWithCounts(query, userId); + const projects = await this.projectStore.getProjectsWithCounts( + query, + userId, + ); if (this.flagResolver.isEnabled('privateProjects') && userId) { const projectAccess = await this.privateProjectChecker.getUserAccessibleProjects( @@ -181,7 +191,7 @@ export default class ProjectService { } async getProject(id: string): Promise { - return this.store.get(id); + return this.projectStore.get(id); } private validateAndProcessFeatureNamingPattern = ( @@ -214,14 +224,11 @@ export default class ProjectService { newProject: CreateProject, user: IUser, ): Promise { - const data = await projectSchema.validateAsync(newProject); + const validatedData = await projectSchema.validateAsync(newProject); + const data = this.removeModeForNonEnterprise(validatedData); await this.validateUniqueId(data.id); - if (data.featureNaming) { - this.validateAndProcessFeatureNamingPattern(data.featureNaming); - } - - await this.store.create(data); + await this.projectStore.create(data); const enabledEnvironments = await this.environmentStore.getAll({ enabled: true, @@ -250,7 +257,24 @@ export default class ProjectService { } async updateProject(updatedProject: IProject, user: User): Promise { - const preData = await this.store.get(updatedProject.id); + const preData = await this.projectStore.get(updatedProject.id); + + await this.projectStore.update(updatedProject); + + await this.eventStore.store({ + type: PROJECT_UPDATED, + project: updatedProject.id, + createdBy: getCreatedBy(user), + data: updatedProject, + preData, + }); + } + + async updateProjectEnterpriseSettings( + updatedProject: IProjectEnterpriseSettingsUpdate, + user: User, + ): Promise { + const preData = await this.projectStore.get(updatedProject.id); if (updatedProject.featureNaming) { this.validateAndProcessFeatureNamingPattern( @@ -258,7 +282,7 @@ export default class ProjectService { ); } - await this.store.update(updatedProject); + await this.projectStore.updateProjectEnterpriseSettings(updatedProject); await this.eventStore.store({ type: PROJECT_UPDATED, @@ -276,7 +300,7 @@ export default class ProjectService { const featureEnvs = await this.featureEnvironmentStore.getAll({ feature_name: feature.name, }); - const newEnvs = await this.store.getEnvironmentsForProject( + const newEnvs = await this.projectStore.getEnvironmentsForProject( newProjectId, ); return arraysHaveSameItems( @@ -289,7 +313,7 @@ export default class ProjectService { project: string, environment: string, ): Promise { - await this.store.addEnvironmentToProject(project, environment); + await this.projectStore.addEnvironmentToProject(project, environment); } async changeProject( @@ -355,7 +379,7 @@ export default class ProjectService { ); } - await this.store.delete(id); + await this.projectStore.delete(id); await this.eventStore.store({ type: PROJECT_DELETED, @@ -373,7 +397,7 @@ export default class ProjectService { } async validateUniqueId(id: string): Promise { - const exists = await this.store.hasProject(id); + const exists = await this.projectStore.hasProject(id); if (exists) { throw new NameExistsError('A project with this id already exists.'); } @@ -872,7 +896,7 @@ export default class ProjectService { } async getMembers(projectId: string): Promise { - return this.store.getMembersCountByProject(projectId); + return this.projectStore.getMembersCountByProject(projectId); } async getProjectUsers( @@ -903,7 +927,7 @@ export default class ProjectService { } async getProjectsByUser(userId: number): Promise { - return this.store.getProjectsByUser(userId); + return this.projectStore.getProjectsByUser(userId); } async getProjectRoleUsage(roleId: number): Promise { @@ -911,7 +935,7 @@ export default class ProjectService { } async statusJob(): Promise { - const projects = await this.store.getAll(); + const projects = await this.projectStore.getAll(); const statusUpdates = await Promise.all( projects.map((project) => this.getStatusUpdates(project.id)), @@ -990,7 +1014,7 @@ export default class ProjectService { ); const projectMembersAddedCurrentWindow = - await this.store.getMembersCountByProjectAfterDate( + await this.projectStore.getMembersCountByProjectAfterDate( projectId, dateMinusThirtyDays, ); @@ -1023,14 +1047,14 @@ export default class ProjectService { favorite, projectStats, ] = await Promise.all([ - this.store.get(projectId), - this.store.getEnvironmentsForProject(projectId), + this.projectStore.get(projectId), + this.projectStore.getEnvironmentsForProject(projectId), this.featureToggleService.getFeatureOverview({ projectId, archived, userId, }), - this.store.getMembersCountByProject(projectId), + this.projectStore.getMembersCountByProject(projectId), userId ? this.favoritesService.isFavoriteProject({ project: projectId, @@ -1058,4 +1082,13 @@ export default class ProjectService { version: 1, }; } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + removeModeForNonEnterprise(data): any { + if (this.isEnterprise) { + return data; + } + const { mode, ...proData } = data; + return proData; + } } diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 8918376557..9179697574 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -420,7 +420,7 @@ export type CreateProject = Pick & { export interface IProject { id: string; name: string; - description: string; + description?: string; health?: number; createdAt?: Date; updatedAt?: Date; diff --git a/src/lib/types/stores/project-store.ts b/src/lib/types/stores/project-store.ts index da6e8cfeb1..f084dcc361 100644 --- a/src/lib/types/stores/project-store.ts +++ b/src/lib/types/stores/project-store.ts @@ -16,14 +16,20 @@ import { CreateFeatureStrategySchema } from '../../openapi'; export interface IProjectInsert { id: string; name: string; - description: string; + description?: string; updatedAt?: Date; changeRequestsEnabled?: boolean; - mode: ProjectMode; + mode?: ProjectMode; featureLimit?: number; featureNaming?: IFeatureNaming; } +export interface IProjectEnterpriseSettingsUpdate { + id: string; + mode?: ProjectMode; + featureNaming?: IFeatureNaming; +} + export interface IProjectSettings { mode: ProjectMode; defaultStickiness: string; @@ -57,6 +63,10 @@ export interface IProjectStore extends Store { update(update: IProjectInsert): Promise; + updateProjectEnterpriseSettings( + update: IProjectEnterpriseSettingsUpdate, + ): Promise; + importProjects( projects: IProjectInsert[], environments?: IEnvironment[], diff --git a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts index 766034be32..7947d643f4 100644 --- a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts +++ b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts @@ -9,7 +9,12 @@ import { } from '../../../lib/services'; import { FeatureStrategySchema } from '../../../lib/openapi'; import User from '../../../lib/types/user'; -import { IConstraint, IVariant, SKIP_CHANGE_REQUEST } from '../../../lib/types'; +import { + IConstraint, + IUnleashStores, + IVariant, + SKIP_CHANGE_REQUEST, +} from '../../../lib/types'; import EnvironmentService from '../../../lib/services/environment-service'; import { ForbiddenError, @@ -20,7 +25,7 @@ import { ISegmentService } from '../../../lib/segments/segment-service-interface import { ChangeRequestAccessReadModel } from '../../../lib/features/change-request-access-service/sql-change-request-access-read-model'; import { createPrivateProjectChecker } from '../../../lib/features/private-project/createPrivateProjectChecker'; -let stores; +let stores: IUnleashStores; let db; let service: FeatureToggleService; let segmentService: ISegmentService; @@ -677,10 +682,13 @@ describe('flag name validation', () => { name: projectId, mode: 'open' as const, defaultStickiness: 'default', - featureNaming, }; await stores.projectStore.create(project); + await stores.projectStore.updateProjectEnterpriseSettings({ + id: projectId, + featureNaming, + }); const validFeatures = ['testpattern-feature', 'testpattern-feature2']; const invalidFeatures = ['a', 'b', 'c']; diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 773f55bd69..08b8bded34 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -135,7 +135,6 @@ test('should create new project', async () => { id: 'test', name: 'New project', description: 'Blah', - mode: 'protected' as const, defaultStickiness: 'default', }; @@ -145,7 +144,6 @@ test('should create new project', async () => { expect(project.name).toEqual(ret.name); expect(project.description).toEqual(ret.description); expect(ret.createdAt).toBeTruthy(); - expect(ret.mode).toEqual('protected'); }); test('should create new private project', async () => { @@ -153,7 +151,6 @@ test('should create new private project', async () => { id: 'testPrivate', name: 'New private project', description: 'Blah', - mode: 'private' as const, defaultStickiness: 'default', }; @@ -163,7 +160,6 @@ test('should create new private project', async () => { expect(project.name).toEqual(ret.name); expect(project.description).toEqual(ret.description); expect(ret.createdAt).toBeTruthy(); - expect(ret.mode).toEqual('private'); }); test('should delete project', async () => { @@ -1829,12 +1825,14 @@ describe('feature flag naming patterns', () => { await projectService.createProject(project, user.id); + await projectService.updateProjectEnterpriseSettings(project, user); + expect( (await projectService.getProject(project.id)).featureNaming, ).toMatchObject(featureNaming); const newPattern = 'new-pattern.+'; - await projectService.updateProject( + await projectService.updateProjectEnterpriseSettings( { ...project, featureNaming: { pattern: newPattern }, diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts index 28eae24098..2322e1f28d 100644 --- a/src/test/fixtures/fake-project-store.ts +++ b/src/test/fixtures/fake-project-store.ts @@ -190,4 +190,9 @@ export default class FakeProjectStore implements IProjectStore { getProjectModeCounts(): Promise { return Promise.resolve([]); } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateProjectEnterpriseSettings(update: IProjectInsert): Promise { + throw new Error('Method not implemented.'); + } }