Skip to content

Commit 511210a

Browse files
Johan BookJohan Book
Johan Book
authored and
Johan Book
committed
feat(api): support deleting organization
1 parent 4d7fa32 commit 511210a

11 files changed

+98
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export class DeleteCurrentOrganizationCommand {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { TestSuite } from "src/test";
2+
3+
import { DeleteCurrentOrganizationHandler } from "./delete-current-organization.handler";
4+
5+
describe(DeleteCurrentOrganizationHandler.name, () => {
6+
let commandHandler: DeleteCurrentOrganizationHandler;
7+
8+
let testSuite: TestSuite;
9+
10+
beforeEach(() => {
11+
testSuite = new TestSuite();
12+
13+
commandHandler = new DeleteCurrentOrganizationHandler(
14+
testSuite.currentOrganizationService,
15+
testSuite.organizationService,
16+
);
17+
});
18+
19+
describe("can update organizations", () => {
20+
it("should save changes to organization", async () => {
21+
await commandHandler.execute();
22+
23+
expect(testSuite.organizations.delete).toHaveBeenCalled();
24+
});
25+
});
26+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { ForbiddenException } from "@nestjs/common";
2+
import { CommandHandler, ICommandHandler } from "@nestjs/cqrs";
3+
4+
import { CurrentOrganizationService } from "src/core/organizations/domain/services/current-organization.service";
5+
import { OrganizationService } from "src/core/organizations/domain/services/organization.service";
6+
7+
import { DeleteCurrentOrganizationCommand } from "../../contracts/commands/delete-current-organization.command";
8+
9+
@CommandHandler(DeleteCurrentOrganizationCommand)
10+
export class DeleteCurrentOrganizationHandler
11+
implements ICommandHandler<DeleteCurrentOrganizationCommand, void>
12+
{
13+
constructor(
14+
private readonly currentOrganizationService: CurrentOrganizationService,
15+
private readonly organizationService: OrganizationService,
16+
) {}
17+
18+
async execute() {
19+
const currentOrganization =
20+
await this.currentOrganizationService.fetchCurrentOrganization();
21+
22+
if (currentOrganization.personal) {
23+
throw new ForbiddenException("Cannot delete personal organization");
24+
}
25+
26+
await this.organizationService.deleteOrganization(currentOrganization.id);
27+
}
28+
}

services/api/src/core/organizations/application/handlers/command-handlers/switch-organization.handler.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UnauthorizedException } from "@nestjs/common";
1+
import { ForbiddenException } from "@nestjs/common";
22
import { CommandHandler, ICommandHandler } from "@nestjs/cqrs";
33

44
import { CurrentOrganizationService } from "src/core/organizations/domain/services/current-organization.service";
@@ -27,7 +27,7 @@ export class SwitchOrganizationHandler
2727
);
2828

2929
if (!hasAccess) {
30-
throw new UnauthorizedException();
30+
throw new ForbiddenException();
3131
}
3232

3333
await this.currentOrganizationService.switchCurrentOrganization(

services/api/src/core/organizations/application/handlers/command-handlers/update-organization.handler.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ export class UpdateOrganizationHandler
2020

2121
currentOrganization.name = command.name;
2222

23-
this.organizationService.updateOrganization(currentOrganization);
23+
await this.organizationService.updateOrganization(currentOrganization);
2424
}
2525
}

services/api/src/core/organizations/client/controllers/current-organization.controller.ts

+11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { RequiresOrganizationPermissions } from "src/core/authorization";
1515

1616
import { AddMemberToOrganizationViaEmailCommand } from "../../application/contracts/commands/add-member-to-organization-via-email.command";
1717
import { AddMemberToOrganizationCommand } from "../../application/contracts/commands/add-member-to-organization.command";
18+
import { DeleteCurrentOrganizationCommand } from "../../application/contracts/commands/delete-current-organization.command";
1819
import { LeaveCurrentOrganizationCommand } from "../../application/contracts/commands/leave-current-organization.command";
1920
import { RemoveMemberFromCurrentOrganizationCommand } from "../../application/contracts/commands/remove-member-from-current-organization.command";
2021
import { UpdateMemberRoleCommand } from "../../application/contracts/commands/update-member-role.command";
@@ -40,6 +41,16 @@ export class CurrentOrganizationController {
4041
return await this.queryBus.execute(query);
4142
}
4243

44+
@Delete()
45+
@RequiresOrganizationPermissions(
46+
organizationPermissions.CurrentOrganization.Delete,
47+
)
48+
async deleteCurrentOrganization(
49+
@Query() command: DeleteCurrentOrganizationCommand,
50+
): Promise<null> {
51+
return await this.commandBus.execute(command);
52+
}
53+
4354
@Delete("/leave")
4455
async leaveCurrentOrganization(
4556
@Query() command: LeaveCurrentOrganizationCommand,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export class OrganizationDeletedEvent {
2+
public readonly id!: number;
3+
}

services/api/src/core/organizations/domain/services/organization.service.ts

+15
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { OrganizationMembership } from "../../infrastructure/entities/organizati
1010
import { Organization } from "../../infrastructure/entities/organization.entity";
1111
import { MemberAddedToOrganizationEvent } from "../events/member-added-to-organization.event";
1212
import { OrganizationCreatedEvent } from "../events/organization-created.event";
13+
import { OrganizationDeletedEvent } from "../events/organization-deleted.event";
1314
import { MembershipService } from "./membership.service";
1415

1516
interface CreateOrganizationProps {
@@ -91,6 +92,20 @@ export class OrganizationService {
9192
return createdOrganization.id;
9293
}
9394

95+
async deleteOrganization(organizationId: number): Promise<void> {
96+
const { affected } = await this.organizations.delete(organizationId);
97+
98+
if (affected === 0) {
99+
return;
100+
}
101+
102+
const event = map(OrganizationDeletedEvent, {
103+
id: organizationId,
104+
});
105+
106+
this.eventBus.publish(event);
107+
}
108+
94109
async checkIfPersonalOrganizationExists(profileId: number): Promise<boolean> {
95110
return await this.organizations.exist({
96111
where: {

services/api/src/core/organizations/organization.permissions.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { OrganizationRole } from "src/core/authorization";
22

33
export const organizationPermissions = {
44
CurrentOrganization: {
5+
Delete: [OrganizationRole.Admin],
56
Read: [OrganizationRole.Admin, OrganizationRole.Member],
67
Members: {
78
Add: [OrganizationRole.Admin],

services/api/src/test/mocks/repository.mock.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class MockRepository<T extends ObjectLiteral> {
1515
}
1616

1717
this.data.splice(index, 1);
18+
19+
return { affected: 1 };
1820
});
1921

2022
exist = jest.fn((element: T) =>

services/api/src/test/test-suite.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@ export class TestSuite {
3535
constructor() {
3636
this.eventBus = createEventBusMock();
3737

38-
this.activeOrganizations = createMockRepository<ActiveOrganization>();
38+
this.activeOrganizations = createMockRepository<ActiveOrganization>([
39+
{
40+
organizationId: "my-organization-id",
41+
profileId: "my-profile-id",
42+
} as unknown as ActiveOrganization,
43+
]);
3944
this.memberships = createMockRepository<OrganizationMembership>();
4045
this.organizations = createMockRepository<Organization>([
41-
{ id: "my-organization-id" } as any,
46+
{ id: "my-organization-id" } as unknown as Organization,
4247
]);
4348
this.profiles = createMockRepository<Profile>([
44-
{ id: "my-profile-id" } as any,
49+
{ id: "my-profile-id" } as unknown as Profile,
4550
]);
4651

4752
const userIdService = createUserIdServiceMock();

0 commit comments

Comments
 (0)