feat: organizations teams endpoints apiv2 (#15586)

* feat: organizations teams endpoints apiv2

* fixup! Merge branch 'main' into feat-api-v2-org-teams-endpoints

* chore: add roles guard to endpoints

* add tests

* fixup! Merge branch 'main' into feat-api-v2-org-teams-endpoints

* fixup! fixup! Merge branch 'main' into feat-api-v2-org-teams-endpoints

* fixup! fixup! fixup! Merge branch 'main' into feat-api-v2-org-teams-endpoints
This commit is contained in:
Morgan authored and GitHub committed 2024-06-27 18:04:37 +02:00
1 parent 835443a0d1
commit e260809b08
11 files changed
+870 -41

No files matched your search

@@ -1,12 +1,18 @@
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
NotFoundException,
} from "@nestjs/common";
import { Request } from "express";
import { Team } from "@calcom/prisma/client";
@Injectable()
export class IsTeamInOrg implements CanActivate {
constructor(private organizationsRepository: OrganizationsRepository) {}
constructor(private organizationsTeamsRepository: OrganizationsTeamsRepository) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
@@ -21,13 +27,17 @@ export class IsTeamInOrg implements CanActivate {
throw new ForbiddenException("No team id found in request params.");
}
const team = await this.organizationsRepository.findOrgTeam(Number(orgId), Number(teamId));
const team = await this.organizationsTeamsRepository.findOrgTeam(Number(orgId), Number(teamId));
if (team && !team.isOrganization && team.parentId === Number(orgId)) {
request.team = team;
return true;
}
if (!team) {
throw new NotFoundException(`Team (${teamId}) not found.`);
}
return false;
}
}
@@ -1,5 +1,6 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organization-team.input";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
@@ -18,7 +19,7 @@ import { ApiSuccessResponse } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
describe("Organizations Team Endpoints", () => {
describe("User Authentication", () => {
describe("User Authentication - User is Org Admin", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
@@ -28,8 +29,10 @@ describe("Organizations Team Endpoints", () => {
let org: Team;
let team: Team;
let team2: Team;
let teamCreatedViaApi: Team;
const userEmail = "org-teams-controller-e2e@api.com";
const userEmail = "org-admin-teams-controller-e2e@api.com";
let user: User;
beforeAll(async () => {
@@ -67,6 +70,12 @@ describe("Organizations Team Endpoints", () => {
parent: { connect: { id: org.id } },
});
team2 = await teamsRepositoryFixture.create({
name: "Test org team 2",
isOrganization: false,
parent: { connect: { id: org.id } },
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -87,7 +96,19 @@ describe("Organizations Team Endpoints", () => {
.then((response) => {
const responseBody: ApiSuccessResponse<Team[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toEqual([]);
expect(responseBody.data[0].id).toEqual(team.id);
expect(responseBody.data[1].id).toEqual(team2.id);
});
});
it("should get all the teams of the org paginated", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams?skip=1&take=1`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<Team[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data[0].id).toEqual(team2.id);
});
});
@@ -107,12 +128,304 @@ describe("Organizations Team Endpoints", () => {
});
});
it("should create the team of the org", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.send({
name: "Team created via API",
} satisfies CreateOrgTeamDto)
.expect(201)
.then((response) => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
teamCreatedViaApi = responseBody.data;
expect(teamCreatedViaApi.name).toEqual("Team created via API");
expect(teamCreatedViaApi.parentId).toEqual(org.id);
});
});
it("should update the team of the org", async () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${teamCreatedViaApi.id}`)
.send({
name: "Team created via API Updated",
} satisfies CreateOrgTeamDto)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
teamCreatedViaApi = responseBody.data;
expect(teamCreatedViaApi.name).toEqual("Team created via API Updated");
expect(teamCreatedViaApi.parentId).toEqual(org.id);
});
});
it("should delete the team of the org we created via api", async () => {
return request(app.getHttpServer())
.delete(`/v2/organizations/${org.id}/teams/${teamCreatedViaApi.id}`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<Team> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data.id).toEqual(teamCreatedViaApi.id);
expect(responseBody.data.parentId).toEqual(teamCreatedViaApi.parentId);
});
});
it("should fail to get the team of the org we just deleted", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${teamCreatedViaApi.id}`)
.expect(404);
});
it("should fail if the team does not exist", async () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/123132145`).expect(403);
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/123132145`).expect(404);
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await teamsRepositoryFixture.delete(team.id);
await teamsRepositoryFixture.delete(team2.id);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
});
describe("Organizations Team Endpoints", () => {
describe("User Authentication - User is Org Member", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let org: Team;
let team: Team;
let team2: Team;
const userEmail = "org-member-teams-controller-e2e@api.com";
let user: User;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
isOrganization: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});
team = await teamsRepositoryFixture.create({
name: "Test org team",
isOrganization: false,
parent: { connect: { id: org.id } },
});
team2 = await teamsRepositoryFixture.create({
name: "Test org team 2",
isOrganization: false,
parent: { connect: { id: org.id } },
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(organizationsRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
it("should deny get all the teams of the org", async () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams`).expect(403);
});
it("should deny get all the teams of the org paginated", async () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams?skip=1&take=1`).expect(403);
});
it("should deny get the team of the org", async () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/${team.id}`).expect(403);
});
it("should deny create the team of the org", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.send({
name: "Team created via API",
} satisfies CreateOrgTeamDto)
.expect(403);
});
it("should deny update the team of the org", async () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}`)
.send({
name: "Team created via API Updated",
} satisfies CreateOrgTeamDto)
.expect(403);
});
it("should deny delete the team of the org we created via api", async () => {
return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/teams/${team2.id}`).expect(403);
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await teamsRepositoryFixture.delete(team.id);
await teamsRepositoryFixture.delete(team2.id);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
});
});
describe("Organizations Team Endpoints", () => {
describe("User Authentication - User is Team Owner", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let organizationsRepositoryFixture: TeamRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let org: Team;
let team: Team;
let team2: Team;
const userEmail = "org-member-teams-owner-controller-e2e@api.com";
let user: User;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
organizationsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
org = await organizationsRepositoryFixture.create({
name: "Test Organization",
isOrganization: true,
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: user.id } },
team: { connect: { id: org.id } },
});
team = await teamsRepositoryFixture.create({
name: "Test org team",
isOrganization: false,
parent: { connect: { id: org.id } },
});
team2 = await teamsRepositoryFixture.create({
name: "Test org team 2",
isOrganization: false,
parent: { connect: { id: org.id } },
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: user.id } },
team: { connect: { id: team.id } },
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: user.id } },
team: { connect: { id: team2.id } },
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(organizationsRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
expect(org).toBeDefined();
});
it("should deny get all the teams of the org", async () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams`).expect(403);
});
it("should deny get all the teams of the org paginated", async () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams?skip=1&take=1`).expect(403);
});
it("should get the team of the org for which the user is team owner", async () => {
return request(app.getHttpServer()).get(`/v2/organizations/${org.id}/teams/${team.id}`).expect(200);
});
it("should deny create the team of the org", async () => {
return request(app.getHttpServer())
.post(`/v2/organizations/${org.id}/teams`)
.send({
name: "Team created via API",
} satisfies CreateOrgTeamDto)
.expect(403);
});
it("should deny update the team of the org", async () => {
return request(app.getHttpServer())
.patch(`/v2/organizations/${org.id}/teams/${team.id}`)
.send({
name: "Team created via API Updated",
} satisfies CreateOrgTeamDto)
.expect(403);
});
it("should deny delete the team of the org we created via api", async () => {
return request(app.getHttpServer()).delete(`/v2/organizations/${org.id}/teams/${team2.id}`).expect(403);
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await teamsRepositoryFixture.delete(team.id);
await teamsRepositoryFixture.delete(team2.id);
await organizationsRepositoryFixture.delete(org.id);
await app.close();
});
@@ -1,52 +1,106 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GetOrg } from "@/modules/auth/decorators/get-org/get-org.decorator";
import { GetTeam } from "@/modules/auth/decorators/get-team/get-team.decorator";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organization-team.input";
import { OrgTeamOutputDto } from "@/modules/organizations/outputs/organization-team.output";
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
import {
Controller,
UseGuards,
Get,
Param,
ParseIntPipe,
Query,
Delete,
Patch,
Post,
Body,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
import { ApiResponse, SkipTakePagination } from "@calcom/platform-types";
import { Team } from "@calcom/prisma/client";
@Controller({
path: "/v2/organizations/:orgId/teams",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, IsOrgGuard)
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard)
@DocsTags("Organizations Teams")
export class OrganizationsTeamsController {
constructor(private organizationsTeamsService: OrganizationsTeamsService) {}
@Get()
@UseGuards()
@Roles("ORG_ADMIN")
async getAllTeams(
@Param("orgId", ParseIntPipe) orgId: number,
@GetOrg() organization: Team,
@GetOrg("name") orgName: string
): Promise<ApiResponse<Team[]>> {
console.log(orgId, organization, orgName);
@Query() queryParams: SkipTakePagination
): Promise<ApiResponse<OrgTeamOutputDto[]>> {
const { skip, take } = queryParams;
const teams = await this.organizationsTeamsService.getPaginatedOrgTeams(orgId, skip ?? 0, take ?? 250);
return {
status: SUCCESS_STATUS,
data: [],
data: teams.map((team) => plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" })),
};
}
@UseGuards(IsTeamInOrg, RolesGuard)
@Roles("ORG_ADMIN")
@UseGuards(IsTeamInOrg)
@Roles("TEAM_ADMIN")
@Get("/:teamId")
async getTeam(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("teamId", ParseIntPipe) teamId: number,
@GetOrg() organization: Team,
@GetTeam() team: Team,
@GetOrg("name") orgName: string
): Promise<ApiResponse<Team>> {
console.log(teamId, orgId, organization, team, orgName);
async getTeam(@GetTeam() team: Team): Promise<ApiResponse<OrgTeamOutputDto>> {
return {
status: SUCCESS_STATUS,
data: team,
data: plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" }),
};
}
@UseGuards(IsTeamInOrg)
@Roles("ORG_ADMIN")
@Delete("/:teamId")
async deleteTeam(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("teamId", ParseIntPipe) teamId: number
): Promise<ApiResponse<OrgTeamOutputDto>> {
const team = await this.organizationsTeamsService.deleteOrgTeam(orgId, teamId);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" }),
};
}
@UseGuards(IsTeamInOrg)
@Roles("ORG_ADMIN")
@Patch("/:teamId")
async updateTeam(
@Param("orgId", ParseIntPipe) orgId: number,
@Param("teamId", ParseIntPipe) teamId: number,
@Body() body: CreateOrgTeamDto
): Promise<ApiResponse<OrgTeamOutputDto>> {
const team = await this.organizationsTeamsService.updateOrgTeam(orgId, teamId, body);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" }),
};
}
@Post()
@UseGuards()
@Roles("ORG_ADMIN")
async createTeam(
@Param("orgId", ParseIntPipe) orgId: number,
@Body() body: CreateOrgTeamDto
): Promise<ApiResponse<OrgTeamOutputDto>> {
const team = await this.organizationsTeamsService.createOrgTeam(orgId, body);
return {
status: SUCCESS_STATUS,
data: plainToClass(OrgTeamOutputDto, team, { strategy: "excludeAll" }),
};
}
}
@@ -0,0 +1,75 @@
import { IsBoolean, IsOptional, IsString, IsUrl, Length } from "class-validator";
export class CreateOrgTeamDto {
@IsString()
@Length(1)
readonly name!: string;
@IsOptional()
@IsString()
readonly slug?: string;
@IsOptional()
@IsUrl()
readonly logoUrl?: string;
@IsOptional()
@IsUrl()
readonly calVideoLogo?: string;
@IsOptional()
@IsUrl()
readonly appLogo?: string;
@IsOptional()
@IsUrl()
readonly appIconLogo?: string;
@IsOptional()
@IsString()
readonly bio?: string;
@IsOptional()
@IsBoolean()
readonly hideBranding?: boolean = false;
@IsOptional()
@IsBoolean()
readonly isPrivate?: boolean;
@IsOptional()
@IsBoolean()
readonly hideBookATeamMember?: boolean;
@IsOptional()
@IsString()
readonly metadata?: string; // Assuming metadata is a JSON string. Adjust accordingly if it's a nested object.
@IsOptional()
@IsString()
readonly theme?: string;
@IsOptional()
@IsString()
readonly brandColor?: string;
@IsOptional()
@IsString()
readonly darkBrandColor?: string;
@IsOptional()
@IsUrl()
readonly bannerUrl?: string;
@IsOptional()
@IsString()
readonly timeFormat?: number;
@IsOptional()
@IsString()
readonly timeZone?: string = "Europe/London";
@IsOptional()
@IsString()
readonly weekStart?: string = "Sunday";
}
@@ -1,6 +1,8 @@
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/organizations-teams.controller";
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { OrganizationsTeamsService } from "@/modules/organizations/services/organizations-teams.service";
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
@@ -8,8 +10,14 @@ import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, StripeModule],
providers: [OrganizationsRepository, OrganizationsService, MembershipsRepository],
exports: [OrganizationsService, OrganizationsRepository],
providers: [
OrganizationsRepository,
OrganizationsTeamsRepository,
OrganizationsService,
OrganizationsTeamsService,
MembershipsRepository,
],
exports: [OrganizationsService, OrganizationsRepository, OrganizationsTeamsRepository],
controllers: [OrganizationsTeamsController],
})
export class OrganizationsModule {}
@@ -70,17 +70,6 @@ export class OrganizationsRepository {
},
});
}
async findOrgTeam(organizationId: number, teamId: number) {
return this.dbRead.prisma.team.findUnique({
where: {
id: teamId,
isOrganization: false,
parentId: organizationId,
},
});
}
async findOrgUser(organizationId: number, userId: number) {
return this.dbRead.prisma.user.findUnique({
where: {
@@ -0,0 +1,107 @@
import { Expose } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, IsString, IsUrl, Length } from "class-validator";
export class OrgTeamOutputDto {
@IsInt()
@Expose()
readonly id!: number;
@IsInt()
@IsOptional()
@Expose()
readonly parentId?: number;
@IsString()
@Length(1)
@Expose()
readonly name!: string;
@IsOptional()
@IsString()
@Expose()
readonly slug?: string;
@IsOptional()
@IsUrl()
@Expose()
readonly logoUrl?: string;
@IsOptional()
@IsUrl()
@Expose()
readonly calVideoLogo?: string;
@IsOptional()
@IsUrl()
@Expose()
readonly appLogo?: string;
@IsOptional()
@IsUrl()
@Expose()
readonly appIconLogo?: string;
@IsOptional()
@IsString()
@Expose()
readonly bio?: string;
@IsOptional()
@IsBoolean()
@Expose()
readonly hideBranding?: boolean;
@IsBoolean()
@Expose()
readonly isOrganization?: boolean;
@IsOptional()
@IsBoolean()
@Expose()
readonly isPrivate?: boolean;
@IsOptional()
@IsBoolean()
@Expose()
readonly hideBookATeamMember?: boolean = false;
@IsOptional()
@IsString()
@Expose()
readonly metadata?: string;
@IsOptional()
@IsString()
@Expose()
readonly theme?: string;
@IsOptional()
@IsString()
@Expose()
readonly brandColor?: string;
@IsOptional()
@IsString()
@Expose()
readonly darkBrandColor?: string;
@IsOptional()
@IsUrl()
@Expose()
readonly bannerUrl?: string;
@IsOptional()
@IsString()
@Expose()
readonly timeFormat?: number;
@IsOptional()
@IsString()
@Expose()
readonly timeZone?: string = "Europe/London";
@IsOptional()
@IsString()
@Expose()
readonly weekStart?: string = "Sunday";
}
@@ -0,0 +1,58 @@
import { PlatformPlan } from "@/modules/billing/types";
import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organization-team.input";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { StripeService } from "@/modules/stripe/stripe.service";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrganizationsTeamsRepository {
constructor(
private readonly dbRead: PrismaReadService,
private readonly dbWrite: PrismaWriteService,
private readonly stripeService: StripeService
) {}
async findOrgTeam(organizationId: number, teamId: number) {
return this.dbRead.prisma.team.findUnique({
where: {
id: teamId,
isOrganization: false,
parentId: organizationId,
},
});
}
async deleteOrgTeam(organizationId: number, teamId: number) {
return this.dbRead.prisma.team.delete({
where: {
id: teamId,
isOrganization: false,
parentId: organizationId,
},
});
}
async createOrgTeam(organizationId: number, data: CreateOrgTeamDto) {
return this.dbRead.prisma.team.create({
data: { ...data, parentId: organizationId },
});
}
async updateOrgTeam(organizationId: number, teamId: number, data: CreateOrgTeamDto) {
return this.dbRead.prisma.team.update({
data: { ...data },
where: { id: teamId, parentId: organizationId, isOrganization: false },
});
}
async findOrgTeamsPaginated(organizationId: number, skip: number, take: number) {
return this.dbRead.prisma.team.findMany({
where: {
parentId: organizationId,
},
skip,
take,
});
}
}
@@ -0,0 +1,28 @@
import { CreateOrgTeamDto } from "@/modules/organizations/inputs/create-organization-team.input";
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrganizationsTeamsService {
constructor(private readonly organizationsTeamRepository: OrganizationsTeamsRepository) {}
async getPaginatedOrgTeams(organizationId: number, skip = 0, take = 250) {
const teams = await this.organizationsTeamRepository.findOrgTeamsPaginated(organizationId, skip, take);
return teams;
}
async deleteOrgTeam(organizationId: number, teamId: number) {
const team = await this.organizationsTeamRepository.deleteOrgTeam(organizationId, teamId);
return team;
}
async updateOrgTeam(organizationId: number, teamId: number, data: CreateOrgTeamDto) {
const team = await this.organizationsTeamRepository.updateOrgTeam(organizationId, teamId, data);
return team;
}
async createOrgTeam(organizationId: number, data: CreateOrgTeamDto) {
const team = await this.organizationsTeamRepository.createOrgTeam(organizationId, data);
return team;
}
}
+172
View File
@@ -834,11 +834,68 @@
"tags": [
"Organizations Teams"
]
},
"post": {
"operationId": "OrganizationsTeamsController_createTeam",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateOrgTeamDto"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"tags": [
"Organizations Teams"
]
}
},
"/v2/organizations/{orgId}/teams/{teamId}": {
"get": {
"operationId": "OrganizationsTeamsController_getTeam",
"parameters": [],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"tags": [
"Organizations Teams"
]
},
"delete": {
"operationId": "OrganizationsTeamsController_deleteTeam",
"parameters": [
{
"name": "orgId",
@@ -872,6 +929,52 @@
"tags": [
"Organizations Teams"
]
},
"patch": {
"operationId": "OrganizationsTeamsController_updateTeam",
"parameters": [
{
"name": "orgId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "teamId",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateOrgTeamDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
},
"tags": [
"Organizations Teams"
]
}
},
"/v2/schedules": {
@@ -3359,6 +3462,75 @@
"data"
]
},
"CreateOrgTeamDto": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string",
"minimum": 1
},
"slug": {
"type": "string",
"minimum": 1
},
"logoUrl": {
"type": "string"
},
"calVideoLogo": {
"type": "string"
},
"appLogo": {
"type": "string"
},
"appIconLogo": {
"type": "string"
},
"bio": {
"type": "string"
},
"hideBranding": {
"type": "boolean"
},
"isPrivate": {
"type": "boolean"
},
"hideBookATeamMember": {
"type": "boolean"
},
"metadata": {
"type": "string"
},
"theme": {
"type": "string"
},
"brandColor": {
"type": "string"
},
"darkBrandColor": {
"type": "string"
},
"bannerUrl": {
"type": "string"
},
"timeFormat": {
"type": "number"
},
"timeZone": {
"type": "string",
"default": "Europe/London"
},
"weekStart": {
"type": "string",
"default": "Sunday"
}
},
"required": [
"name"
]
},
"CreateAvailabilityInput_2024_04_15": {
"type": "object",
"properties": {
+15
View File
@@ -55,3 +55,18 @@ export class Pagination {
@IsOptional()
offset?: number | null;
}
export class SkipTakePagination {
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(1)
@Max(250)
@IsOptional()
take?: number;
@Transform(({ value }: { value: string }) => value && parseInt(value))
@IsNumber()
@Min(0)
@IsOptional()
skip?: number;
}