1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: generate project ids if they're missing (#7003)

This PR updates the project service to automatically create a project id
if it is not provided. The feature is behind a flag. If an ID is
provided, it will still attempt to use that ID instead.
This commit is contained in:
Thomas Heartman 2024-05-08 12:45:11 +02:00 committed by GitHub
parent 02440dfed2
commit 95ac2e6b8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 130 additions and 5 deletions

View File

@ -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();
},
);
});
});

View File

@ -302,6 +302,15 @@ export default class ProjectService {
return id;
}
async generateUniqueProjectId(name: string): Promise<string> {
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<ProjectCreated> {
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,
}),
);

View File

@ -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<IProject, 'id' | 'name'> & {
export type CreateProject = Pick<IProject, 'name'> & {
id?: string;
mode?: ProjectMode;
defaultStickiness?: string;
environments?: string[];