From a6fd940088334c9306a80d15025e1e96f5ca0fa7 Mon Sep 17 00:00:00 2001 From: kwasniew Date: Wed, 7 Aug 2024 11:47:44 +0200 Subject: [PATCH 1/3] feat: archived project service --- .../project/project-service.e2e.test.ts | 20 ++++++++++++ src/lib/features/project/project-service.ts | 31 +++++++++++++++++-- .../features/project/project-store-type.ts | 2 ++ src/lib/features/project/project-store.ts | 5 +++ src/lib/types/events.ts | 16 +++++++++- src/test/fixtures/fake-project-store.ts | 2 ++ 6 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/lib/features/project/project-service.e2e.test.ts b/src/lib/features/project/project-service.e2e.test.ts index 7abce289cade..43d4737ef9fb 100644 --- a/src/lib/features/project/project-service.e2e.test.ts +++ b/src/lib/features/project/project-service.e2e.test.ts @@ -281,6 +281,26 @@ 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 update project without existing settings', async () => { const project = { id: 'test-update-legacy', diff --git a/src/lib/features/project/project-service.ts b/src/lib/features/project/project-service.ts index 15bea9bc4a26..8d2045029f5a 100644 --- a/src/lib/features/project/project-service.ts +++ b/src/lib/features/project/project-service.ts @@ -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 { + 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 { await nameType.validateAsync(id); await this.validateUniqueId(id); diff --git a/src/lib/features/project/project-store-type.ts b/src/lib/features/project/project-store-type.ts index 410a90d51c35..375e368eceb0 100644 --- a/src/lib/features/project/project-store-type.ts +++ b/src/lib/features/project/project-store-type.ts @@ -134,4 +134,6 @@ export interface IProjectStore extends Store { getApplicationsByProject( searchParams: IProjectApplicationsSearchParams, ): Promise; + + archive(projectId: string): Promise; } diff --git a/src/lib/features/project/project-store.ts b/src/lib/features/project/project-store.ts index eced172ca223..feefb4c99d4a 100644 --- a/src/lib/features/project/project-store.ts +++ b/src/lib/features/project/project-store.ts @@ -396,6 +396,11 @@ class ProjectStore implements IProjectStore { } } + async archive(id: string): Promise { + const now = new Date(); + await this.db(TABLE).where({ id }).update({ archived_at: now }); + } + async getProjectLinksForEnvironments( environments: string[], ): Promise { diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 593a4d21d6ee..bfbc7ac5327b 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -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; } } diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts index b5c9dc7ec21e..deaa79e3bf1c 100644 --- a/src/test/fixtures/fake-project-store.ts +++ b/src/test/fixtures/fake-project-store.ts @@ -214,4 +214,6 @@ export default class FakeProjectStore implements IProjectStore { ): Promise { throw new Error('Method not implemented.'); } + + async archive(id: string): Promise {} } From f81c4bf1003b7e41cae2edb01d3248339529e5d8 Mon Sep 17 00:00:00 2001 From: kwasniew Date: Wed, 7 Aug 2024 11:51:46 +0200 Subject: [PATCH 2/3] feat: archived project service --- .../project/project-service.e2e.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/lib/features/project/project-service.e2e.test.ts b/src/lib/features/project/project-service.e2e.test.ts index 43d4737ef9fb..17c0b7fe578f 100644 --- a/src/lib/features/project/project-service.e2e.test.ts +++ b/src/lib/features/project/project-service.e2e.test.ts @@ -301,6 +301,29 @@ test('should archive project', async () => { }); }); +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-delete', + 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', From 9cf3280e534708fee3bdc8c81924b05f65826ab3 Mon Sep 17 00:00:00 2001 From: kwasniew Date: Wed, 7 Aug 2024 12:01:40 +0200 Subject: [PATCH 3/3] feat: archived project service --- src/lib/features/project/project-service.e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/features/project/project-service.e2e.test.ts b/src/lib/features/project/project-service.e2e.test.ts index 17c0b7fe578f..71dfe1219e42 100644 --- a/src/lib/features/project/project-service.e2e.test.ts +++ b/src/lib/features/project/project-service.e2e.test.ts @@ -311,7 +311,7 @@ test('should not be able to archive project with flags', async () => { }; await projectService.createProject(project, user, auditUser); await stores.featureToggleStore.create(project.id, { - name: 'test-project-delete', + name: 'test-project-archive', createdByUserId: 9999, });