1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-26 01:17:00 +02:00

feat: archive project service (#7794)

This commit is contained in:
Mateusz Kwasniewski 2024-08-07 12:09:00 +02:00 committed by GitHub
parent c36ead4b74
commit 0450bfe6f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 95 additions and 4 deletions

View File

@ -281,6 +281,49 @@ test('should update project', async () => {
expect(updatedProject.defaultStickiness).toBe('userId');
});
test('should archive project', async () => {
const project = {
id: 'test-archive',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'default',
};
await projectService.createProject(project, user, TEST_AUDIT_USER);
await projectService.archiveProject(project.id, TEST_AUDIT_USER);
const events = await stores.eventStore.getEvents();
expect(events[0]).toMatchObject({
type: 'project-archived',
createdBy: TEST_AUDIT_USER.username,
});
});
test('should not be able to archive project with flags', async () => {
const project = {
id: 'test-archive-with-flags',
name: 'New project',
description: 'Blah',
mode: 'open' as const,
defaultStickiness: 'default',
};
await projectService.createProject(project, user, auditUser);
await stores.featureToggleStore.create(project.id, {
name: 'test-project-archive',
createdByUserId: 9999,
});
try {
await projectService.archiveProject(project.id, auditUser);
} catch (err) {
expect(err.message).toBe(
'You can not archive a project with active feature flags',
);
}
});
test('should update project without existing settings', async () => {
const project = {
id: 'test-update-legacy',

View File

@ -12,6 +12,7 @@ import { nameType } from '../../routes/util';
import { projectSchema } from '../../services/project-schema';
import NotFoundError from '../../error/notfound-error';
import {
ADMIN,
ADMIN_TOKEN_USER,
type CreateProject,
DEFAULT_PROJECT,
@ -27,6 +28,7 @@ import {
type IProjectApplications,
type IProjectHealth,
type IProjectOverview,
type IProjectOwnersReadModel,
type IProjectRoleUsage,
type IProjectStore,
type IProjectUpdate,
@ -38,6 +40,8 @@ import {
ProjectAccessGroupRolesUpdated,
ProjectAccessUserRolesDeleted,
ProjectAccessUserRolesUpdated,
ProjectArchivedEvent,
type ProjectCreated,
ProjectCreatedEvent,
ProjectDeletedEvent,
ProjectGroupAddedEvent,
@ -49,9 +53,6 @@ import {
ProjectUserUpdateRoleEvent,
RoleName,
SYSTEM_USER_ID,
type ProjectCreated,
type IProjectOwnersReadModel,
ADMIN,
} from '../../types';
import type {
IProjectAccessModel,
@ -591,6 +592,30 @@ export default class ProjectService {
await this.accessService.removeDefaultProjectRoles(user, id);
}
async archiveProject(id: string, auditUser: IAuditUser): Promise<void> {
const flags = await this.featureToggleStore.getAll({
project: id,
archived: false,
});
// TODO: allow archiving project with unused flags
if (flags.length > 0) {
throw new InvalidOperationError(
'You can not archive a project with active feature flags',
);
}
await this.projectStore.archive(id);
await this.eventService.storeEvent(
new ProjectArchivedEvent({
project: id,
auditUser,
}),
);
}
async validateId(id: string): Promise<boolean> {
await nameType.validateAsync(id);
await this.validateUniqueId(id);

View File

@ -134,4 +134,6 @@ export interface IProjectStore extends Store<IProject, string> {
getApplicationsByProject(
searchParams: IProjectApplicationsSearchParams,
): Promise<IProjectApplications>;
archive(projectId: string): Promise<void>;
}

View File

@ -396,6 +396,11 @@ class ProjectStore implements IProjectStore {
}
}
async archive(id: string): Promise<void> {
const now = new Date();
await this.db(TABLE).where({ id }).update({ archived_at: now });
}
async getProjectLinksForEnvironments(
environments: string[],
): Promise<IEnvironmentProjectLink[]> {

View File

@ -81,6 +81,7 @@ export const ROLE_DELETED = 'role-deleted';
export const PROJECT_CREATED = 'project-created' as const;
export const PROJECT_UPDATED = 'project-updated' as const;
export const PROJECT_DELETED = 'project-deleted' as const;
export const PROJECT_ARCHIVED = 'project-archived' as const;
export const PROJECT_IMPORT = 'project-import' as const;
export const PROJECT_USER_ADDED = 'project-user-added' as const;
export const PROJECT_USER_REMOVED = 'project-user-removed' as const;
@ -249,6 +250,7 @@ export const IEventTypes = [
PROJECT_CREATED,
PROJECT_UPDATED,
PROJECT_DELETED,
PROJECT_ARCHIVED,
PROJECT_IMPORT,
PROJECT_USER_ADDED,
PROJECT_USER_REMOVED,
@ -574,7 +576,19 @@ export class ProjectDeletedEvent extends BaseEvent {
project: string;
auditUser: IAuditUser;
}) {
super(PROJECT_DELETED, eventData.auditUser);
super(PROJECT_ARCHIVED, eventData.auditUser);
this.project = eventData.project;
}
}
export class ProjectArchivedEvent extends BaseEvent {
readonly project: string;
constructor(eventData: {
project: string;
auditUser: IAuditUser;
}) {
super(PROJECT_ARCHIVED, eventData.auditUser);
this.project = eventData.project;
}
}

View File

@ -214,4 +214,6 @@ export default class FakeProjectStore implements IProjectStore {
): Promise<IProjectApplications> {
throw new Error('Method not implemented.');
}
async archive(id: string): Promise<void> {}
}