feat(api): add team management module with CRUD endpoints
All checks were successful
ci/woodpecker/push/api Pipeline was successful
All checks were successful
ci/woodpecker/push/api Pipeline was successful
- TeamsModule: controller, service, DTOs, specs - POST/GET /api/workspaces/:id/teams - POST/DELETE /api/workspaces/:id/teams/:teamId/members - DELETE /api/workspaces/:id/teams/:teamId - 19 tests Refs: MS21-API-004
This commit is contained in:
@@ -44,6 +44,7 @@ import { TerminalModule } from "./terminal/terminal.module";
|
||||
import { PersonalitiesModule } from "./personalities/personalities.module";
|
||||
import { WorkspacesModule } from "./workspaces/workspaces.module";
|
||||
import { AdminModule } from "./admin/admin.module";
|
||||
import { TeamsModule } from "./teams/teams.module";
|
||||
import { RlsContextInterceptor } from "./common/interceptors/rls-context.interceptor";
|
||||
|
||||
@Module({
|
||||
@@ -111,6 +112,7 @@ import { RlsContextInterceptor } from "./common/interceptors/rls-context.interce
|
||||
PersonalitiesModule,
|
||||
WorkspacesModule,
|
||||
AdminModule,
|
||||
TeamsModule,
|
||||
],
|
||||
controllers: [AppController, CsrfController],
|
||||
providers: [
|
||||
|
||||
13
apps/api/src/teams/dto/create-team.dto.ts
Normal file
13
apps/api/src/teams/dto/create-team.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IsOptional, IsString, MaxLength, MinLength } from "class-validator";
|
||||
|
||||
export class CreateTeamDto {
|
||||
@IsString({ message: "name must be a string" })
|
||||
@MinLength(1, { message: "name must not be empty" })
|
||||
@MaxLength(255, { message: "name must not exceed 255 characters" })
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "description must be a string" })
|
||||
@MaxLength(10000, { message: "description must not exceed 10000 characters" })
|
||||
description?: string;
|
||||
}
|
||||
11
apps/api/src/teams/dto/manage-team-member.dto.ts
Normal file
11
apps/api/src/teams/dto/manage-team-member.dto.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { TeamMemberRole } from "@prisma/client";
|
||||
import { IsEnum, IsOptional, IsUUID } from "class-validator";
|
||||
|
||||
export class ManageTeamMemberDto {
|
||||
@IsUUID("4", { message: "userId must be a valid UUID" })
|
||||
userId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(TeamMemberRole, { message: "role must be a valid TeamMemberRole" })
|
||||
role?: TeamMemberRole;
|
||||
}
|
||||
150
apps/api/src/teams/teams.controller.spec.ts
Normal file
150
apps/api/src/teams/teams.controller.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { TeamMemberRole } from "@prisma/client";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { PermissionGuard, WorkspaceGuard } from "../common/guards";
|
||||
import { TeamsController } from "./teams.controller";
|
||||
import { TeamsService } from "./teams.service";
|
||||
|
||||
describe("TeamsController", () => {
|
||||
let controller: TeamsController;
|
||||
let service: TeamsService;
|
||||
|
||||
const mockTeamsService = {
|
||||
create: vi.fn(),
|
||||
findAll: vi.fn(),
|
||||
addMember: vi.fn(),
|
||||
removeMember: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
};
|
||||
|
||||
const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001";
|
||||
const mockTeamId = "550e8400-e29b-41d4-a716-446655440002";
|
||||
const mockUserId = "550e8400-e29b-41d4-a716-446655440003";
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [TeamsController],
|
||||
providers: [
|
||||
{
|
||||
provide: TeamsService,
|
||||
useValue: mockTeamsService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(AuthGuard)
|
||||
.useValue({ canActivate: vi.fn(() => true) })
|
||||
.overrideGuard(WorkspaceGuard)
|
||||
.useValue({ canActivate: vi.fn(() => true) })
|
||||
.overrideGuard(PermissionGuard)
|
||||
.useValue({ canActivate: vi.fn(() => true) })
|
||||
.compile();
|
||||
|
||||
controller = module.get<TeamsController>(TeamsController);
|
||||
service = module.get<TeamsService>(TeamsService);
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a team in a workspace", async () => {
|
||||
const createDto = {
|
||||
name: "Platform Team",
|
||||
description: "Owns platform services",
|
||||
};
|
||||
|
||||
const createdTeam = {
|
||||
id: mockTeamId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
name: createDto.name,
|
||||
description: createDto.description,
|
||||
metadata: {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockTeamsService.create.mockResolvedValue(createdTeam);
|
||||
|
||||
const result = await controller.create(createDto, mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(createdTeam);
|
||||
expect(service.create).toHaveBeenCalledWith(mockWorkspaceId, createDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should list teams in a workspace", async () => {
|
||||
const teams = [
|
||||
{
|
||||
id: mockTeamId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
name: "Platform Team",
|
||||
description: "Owns platform services",
|
||||
metadata: {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
_count: { members: 2 },
|
||||
},
|
||||
];
|
||||
|
||||
mockTeamsService.findAll.mockResolvedValue(teams);
|
||||
|
||||
const result = await controller.findAll(mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(teams);
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addMember", () => {
|
||||
it("should add a member to a team", async () => {
|
||||
const dto = {
|
||||
userId: mockUserId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
};
|
||||
|
||||
const createdTeamMember = {
|
||||
teamId: mockTeamId,
|
||||
userId: mockUserId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
joinedAt: new Date(),
|
||||
user: {
|
||||
id: mockUserId,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
},
|
||||
};
|
||||
|
||||
mockTeamsService.addMember.mockResolvedValue(createdTeamMember);
|
||||
|
||||
const result = await controller.addMember(mockTeamId, dto, mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(createdTeamMember);
|
||||
expect(service.addMember).toHaveBeenCalledWith(mockWorkspaceId, mockTeamId, dto);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeMember", () => {
|
||||
it("should remove a member from a team", async () => {
|
||||
mockTeamsService.removeMember.mockResolvedValue(undefined);
|
||||
|
||||
await controller.removeMember(mockTeamId, mockUserId, mockWorkspaceId);
|
||||
|
||||
expect(service.removeMember).toHaveBeenCalledWith(mockWorkspaceId, mockTeamId, mockUserId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("should delete a team", async () => {
|
||||
mockTeamsService.remove.mockResolvedValue(undefined);
|
||||
|
||||
await controller.remove(mockTeamId, mockWorkspaceId);
|
||||
|
||||
expect(service.remove).toHaveBeenCalledWith(mockWorkspaceId, mockTeamId);
|
||||
});
|
||||
});
|
||||
});
|
||||
51
apps/api/src/teams/teams.controller.ts
Normal file
51
apps/api/src/teams/teams.controller.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, UseGuards } from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { PermissionGuard, WorkspaceGuard } from "../common/guards";
|
||||
import { Permission, RequirePermission, Workspace } from "../common/decorators";
|
||||
import { CreateTeamDto } from "./dto/create-team.dto";
|
||||
import { ManageTeamMemberDto } from "./dto/manage-team-member.dto";
|
||||
import { TeamsService } from "./teams.service";
|
||||
|
||||
@Controller("workspaces/:workspaceId/teams")
|
||||
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
||||
export class TeamsController {
|
||||
constructor(private readonly teamsService: TeamsService) {}
|
||||
|
||||
@Post()
|
||||
@RequirePermission(Permission.WORKSPACE_ADMIN)
|
||||
async create(@Body() createTeamDto: CreateTeamDto, @Workspace() workspaceId: string) {
|
||||
return this.teamsService.create(workspaceId, createTeamDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async findAll(@Workspace() workspaceId: string) {
|
||||
return this.teamsService.findAll(workspaceId);
|
||||
}
|
||||
|
||||
@Post(":teamId/members")
|
||||
@RequirePermission(Permission.WORKSPACE_ADMIN)
|
||||
async addMember(
|
||||
@Param("teamId") teamId: string,
|
||||
@Body() dto: ManageTeamMemberDto,
|
||||
@Workspace() workspaceId: string
|
||||
) {
|
||||
return this.teamsService.addMember(workspaceId, teamId, dto);
|
||||
}
|
||||
|
||||
@Delete(":teamId/members/:userId")
|
||||
@RequirePermission(Permission.WORKSPACE_ADMIN)
|
||||
async removeMember(
|
||||
@Param("teamId") teamId: string,
|
||||
@Param("userId") userId: string,
|
||||
@Workspace() workspaceId: string
|
||||
) {
|
||||
return this.teamsService.removeMember(workspaceId, teamId, userId);
|
||||
}
|
||||
|
||||
@Delete(":teamId")
|
||||
@RequirePermission(Permission.WORKSPACE_ADMIN)
|
||||
async remove(@Param("teamId") teamId: string, @Workspace() workspaceId: string) {
|
||||
return this.teamsService.remove(workspaceId, teamId);
|
||||
}
|
||||
}
|
||||
13
apps/api/src/teams/teams.module.ts
Normal file
13
apps/api/src/teams/teams.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AuthModule } from "../auth/auth.module";
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { TeamsController } from "./teams.controller";
|
||||
import { TeamsService } from "./teams.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AuthModule],
|
||||
controllers: [TeamsController],
|
||||
providers: [TeamsService],
|
||||
exports: [TeamsService],
|
||||
})
|
||||
export class TeamsModule {}
|
||||
286
apps/api/src/teams/teams.service.spec.ts
Normal file
286
apps/api/src/teams/teams.service.spec.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { BadRequestException, ConflictException, NotFoundException } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { TeamMemberRole } from "@prisma/client";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { TeamsService } from "./teams.service";
|
||||
|
||||
describe("TeamsService", () => {
|
||||
let service: TeamsService;
|
||||
let prisma: PrismaService;
|
||||
|
||||
const mockPrismaService = {
|
||||
team: {
|
||||
create: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
workspaceMember: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
teamMember: {
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001";
|
||||
const mockTeamId = "550e8400-e29b-41d4-a716-446655440002";
|
||||
const mockUserId = "550e8400-e29b-41d4-a716-446655440003";
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
TeamsService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TeamsService>(TeamsService);
|
||||
prisma = module.get<PrismaService>(PrismaService);
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a team", async () => {
|
||||
const createDto = {
|
||||
name: "Platform Team",
|
||||
description: "Owns platform services",
|
||||
};
|
||||
|
||||
const createdTeam = {
|
||||
id: mockTeamId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
name: createDto.name,
|
||||
description: createDto.description,
|
||||
metadata: {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockPrismaService.team.create.mockResolvedValue(createdTeam);
|
||||
|
||||
const result = await service.create(mockWorkspaceId, createDto);
|
||||
|
||||
expect(result).toEqual(createdTeam);
|
||||
expect(prisma.team.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
workspaceId: mockWorkspaceId,
|
||||
name: createDto.name,
|
||||
description: createDto.description,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should list teams for a workspace", async () => {
|
||||
const teams = [
|
||||
{
|
||||
id: mockTeamId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
name: "Platform Team",
|
||||
description: "Owns platform services",
|
||||
metadata: {},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
_count: { members: 1 },
|
||||
},
|
||||
];
|
||||
|
||||
mockPrismaService.team.findMany.mockResolvedValue(teams);
|
||||
|
||||
const result = await service.findAll(mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(teams);
|
||||
expect(prisma.team.findMany).toHaveBeenCalledWith({
|
||||
where: { workspaceId: mockWorkspaceId },
|
||||
include: {
|
||||
_count: {
|
||||
select: { members: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("addMember", () => {
|
||||
it("should add a workspace member to a team", async () => {
|
||||
const dto = {
|
||||
userId: mockUserId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
};
|
||||
|
||||
const createdTeamMember = {
|
||||
teamId: mockTeamId,
|
||||
userId: mockUserId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
joinedAt: new Date(),
|
||||
user: {
|
||||
id: mockUserId,
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
},
|
||||
};
|
||||
|
||||
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
|
||||
mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ userId: mockUserId });
|
||||
mockPrismaService.teamMember.findUnique.mockResolvedValue(null);
|
||||
mockPrismaService.teamMember.create.mockResolvedValue(createdTeamMember);
|
||||
|
||||
const result = await service.addMember(mockWorkspaceId, mockTeamId, dto);
|
||||
|
||||
expect(result).toEqual(createdTeamMember);
|
||||
expect(prisma.team.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: mockTeamId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
expect(prisma.workspaceMember.findUnique).toHaveBeenCalledWith({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId: mockWorkspaceId,
|
||||
userId: mockUserId,
|
||||
},
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
expect(prisma.teamMember.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
teamId: mockTeamId,
|
||||
userId: mockUserId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should use MEMBER role when role is omitted", async () => {
|
||||
const dto = { userId: mockUserId };
|
||||
|
||||
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
|
||||
mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ userId: mockUserId });
|
||||
mockPrismaService.teamMember.findUnique.mockResolvedValue(null);
|
||||
mockPrismaService.teamMember.create.mockResolvedValue({
|
||||
teamId: mockTeamId,
|
||||
userId: mockUserId,
|
||||
role: TeamMemberRole.MEMBER,
|
||||
joinedAt: new Date(),
|
||||
});
|
||||
|
||||
await service.addMember(mockWorkspaceId, mockTeamId, dto);
|
||||
|
||||
expect(prisma.teamMember.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
role: TeamMemberRole.MEMBER,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw when team does not belong to workspace", async () => {
|
||||
mockPrismaService.team.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.addMember(mockWorkspaceId, mockTeamId, { userId: mockUserId })
|
||||
).rejects.toThrow(NotFoundException);
|
||||
expect(prisma.workspaceMember.findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw when user is not a workspace member", async () => {
|
||||
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
|
||||
mockPrismaService.workspaceMember.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.addMember(mockWorkspaceId, mockTeamId, { userId: mockUserId })
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it("should throw when user is already in the team", async () => {
|
||||
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
|
||||
mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ userId: mockUserId });
|
||||
mockPrismaService.teamMember.findUnique.mockResolvedValue({ userId: mockUserId });
|
||||
|
||||
await expect(
|
||||
service.addMember(mockWorkspaceId, mockTeamId, { userId: mockUserId })
|
||||
).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeMember", () => {
|
||||
it("should remove a member from a team", async () => {
|
||||
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
|
||||
mockPrismaService.teamMember.deleteMany.mockResolvedValue({ count: 1 });
|
||||
|
||||
await service.removeMember(mockWorkspaceId, mockTeamId, mockUserId);
|
||||
|
||||
expect(prisma.teamMember.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
teamId: mockTeamId,
|
||||
userId: mockUserId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw when team does not belong to workspace", async () => {
|
||||
mockPrismaService.team.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(service.removeMember(mockWorkspaceId, mockTeamId, mockUserId)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
expect(prisma.teamMember.deleteMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw when user is not in the team", async () => {
|
||||
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
|
||||
mockPrismaService.teamMember.deleteMany.mockResolvedValue({ count: 0 });
|
||||
|
||||
await expect(service.removeMember(mockWorkspaceId, mockTeamId, mockUserId)).rejects.toThrow(
|
||||
NotFoundException
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("should delete a team", async () => {
|
||||
mockPrismaService.team.deleteMany.mockResolvedValue({ count: 1 });
|
||||
|
||||
await service.remove(mockWorkspaceId, mockTeamId);
|
||||
|
||||
expect(prisma.team.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: mockTeamId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw when team is not found", async () => {
|
||||
mockPrismaService.team.deleteMany.mockResolvedValue({ count: 0 });
|
||||
|
||||
await expect(service.remove(mockWorkspaceId, mockTeamId)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
||||
130
apps/api/src/teams/teams.service.ts
Normal file
130
apps/api/src/teams/teams.service.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { TeamMemberRole } from "@prisma/client";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { CreateTeamDto } from "./dto/create-team.dto";
|
||||
import { ManageTeamMemberDto } from "./dto/manage-team-member.dto";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async create(workspaceId: string, createTeamDto: CreateTeamDto) {
|
||||
return this.prisma.team.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
name: createTeamDto.name,
|
||||
description: createTeamDto.description ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findAll(workspaceId: string) {
|
||||
return this.prisma.team.findMany({
|
||||
where: { workspaceId },
|
||||
include: {
|
||||
_count: {
|
||||
select: { members: true },
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
}
|
||||
|
||||
async addMember(workspaceId: string, teamId: string, dto: ManageTeamMemberDto) {
|
||||
await this.ensureTeamInWorkspace(workspaceId, teamId);
|
||||
|
||||
const workspaceMember = await this.prisma.workspaceMember.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId,
|
||||
userId: dto.userId,
|
||||
},
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
if (!workspaceMember) {
|
||||
throw new BadRequestException(
|
||||
`User ${dto.userId} must be a workspace member before being added to a team`
|
||||
);
|
||||
}
|
||||
|
||||
const existingTeamMember = await this.prisma.teamMember.findUnique({
|
||||
where: {
|
||||
teamId_userId: {
|
||||
teamId,
|
||||
userId: dto.userId,
|
||||
},
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
|
||||
if (existingTeamMember) {
|
||||
throw new ConflictException(`User ${dto.userId} is already a member of team ${teamId}`);
|
||||
}
|
||||
|
||||
return this.prisma.teamMember.create({
|
||||
data: {
|
||||
teamId,
|
||||
userId: dto.userId,
|
||||
role: dto.role ?? TeamMemberRole.MEMBER,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async removeMember(workspaceId: string, teamId: string, userId: string): Promise<void> {
|
||||
await this.ensureTeamInWorkspace(workspaceId, teamId);
|
||||
|
||||
const result = await this.prisma.teamMember.deleteMany({
|
||||
where: {
|
||||
teamId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.count === 0) {
|
||||
throw new NotFoundException(`User ${userId} is not a member of team ${teamId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(workspaceId: string, teamId: string): Promise<void> {
|
||||
const result = await this.prisma.team.deleteMany({
|
||||
where: {
|
||||
id: teamId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (result.count === 0) {
|
||||
throw new NotFoundException(`Team with ID ${teamId} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureTeamInWorkspace(workspaceId: string, teamId: string): Promise<void> {
|
||||
const team = await this.prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
workspaceId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new NotFoundException(`Team with ID ${teamId} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user