diff --git a/src/lib/features/project/project-service.e2e.test.ts b/src/lib/features/project/project-service.e2e.test.ts index df9a87cbaa..a5a4f298d8 100644 --- a/src/lib/features/project/project-service.e2e.test.ts +++ b/src/lib/features/project/project-service.e2e.test.ts @@ -2616,3 +2616,100 @@ describe('create project with environments', () => { ); }); }); + +describe('automatic ID generation for create project', () => { + test('if no ID is included in the creation argument, it gets generated based on the project name', async () => { + const project = await projectService.createProject( + { + name: 'New name', + }, + user, + auditUser, + ); + + expect(project.id).toMatch(/^new-name-/); + }); + + test('two projects with the same name get different ids', async () => { + const createProject = async () => + projectService.createProject( + { name: 'some name' }, + user, + auditUser, + ); + + const project1 = await createProject(); + const project2 = await createProject(); + + expect(project1.id).toMatch(/^some-name-/); + expect(project2.id).toMatch(/^some-name-/); + expect(project1.id).not.toBe(project2.id); + }); + + test.each(['', undefined, ' '])( + 'An id with the value `%s` is treated as missing (and the id is based on the name)', + async (id) => { + const name = randomId(); + const project = await projectService.createProject( + { name, id }, + user, + auditUser, + ); + + expect(project.id).toMatch(new RegExp(`^${name}-`)); + }, + ); + + describe('backwards compatibility', () => { + const featureFlag = 'createProjectWithEnvironmentConfig'; + + test.each([true, false])( + 'if the ID is present in the input, it is used as the ID regardless of the feature flag states. Flag state: %s', + async (flagState) => { + const id = randomId(); + // @ts-expect-error - we're just checking that the same + // thing happens regardless of flag state + projectService.flagResolver.isEnabled = ( + flagToCheck: string, + ) => { + if (flagToCheck === featureFlag) { + return flagState; + } else { + return false; + } + }; + const project = await projectService.createProject( + { + name: id, + id, + }, + user, + auditUser, + ); + + expect(project.id).toBe(id); + }, + ); + + test.each(['', undefined, ' '])( + 'if the flag to enable auto ID generation is off, not providing a valid ID (testing `%s`) throws an error', + async (id) => { + // @ts-expect-error + projectService.flagResolver.isEnabled = () => { + return false; + }; + + const createProject = () => + projectService.createProject( + { + name: randomId(), + id, + }, + user, + auditUser, + ); + expect(createProject).rejects.toThrow(); + }, + ); + }); +}); diff --git a/src/lib/features/project/project-service.ts b/src/lib/features/project/project-service.ts index f8e818d505..5dfdcddfb1 100644 --- a/src/lib/features/project/project-service.ts +++ b/src/lib/features/project/project-service.ts @@ -302,6 +302,15 @@ export default class ProjectService { return id; } + async generateUniqueProjectId(name: string): Promise { + const id = this.generateProjectId(name); + if (await this.projectStore.hasProject(id)) { + return await this.generateUniqueProjectId(name); + } else { + return id; + } + } + async createProject( newProject: CreateProject, user: IUser, @@ -314,11 +323,29 @@ export default class ProjectService { return []; }, ): Promise { - await this.validateProjectEnvironments(newProject.environments); + const validateData = async () => { + await this.validateProjectEnvironments(newProject.environments); - const validatedData = await projectSchema.validateAsync(newProject); + if ( + !newProject.id?.trim() && + this.flagResolver.isEnabled( + 'createProjectWithEnvironmentConfig', + ) + ) { + newProject.id = await this.generateUniqueProjectId( + newProject.name, + ); + return await projectSchema.validateAsync(newProject); + } else { + const validatedData = + await projectSchema.validateAsync(newProject); + await this.validateUniqueId(validatedData.id); + return validatedData; + } + }; + + const validatedData = await validateData(); const data = this.removePropertiesForNonEnterprise(validatedData); - await this.validateUniqueId(data.id); await this.projectStore.create(data); @@ -362,7 +389,7 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectCreatedEvent({ data, - project: newProject.id, + project: data.id, auditUser, }), ); diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index e3a2eb4ead..06ceefda52 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -487,7 +487,8 @@ export interface IImportData extends ImportCommon { // Create project aligns with #/components/schemas/createProjectSchema // joi is providing default values when the optional inputs are not provided // const data = await projectSchema.validateAsync(newProject); -export type CreateProject = Pick & { +export type CreateProject = Pick & { + id?: string; mode?: ProjectMode; defaultStickiness?: string; environments?: string[];