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:
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;
|
||||
}
|
||||
}
|
||||
+317
-4
@@ -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();
|
||||
});
|
||||
|
||||
+74
-20
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user