Skip to content

Commit

Permalink
Merge branch 'main' into feat/automatic-no-show
Browse files Browse the repository at this point in the history
  • Loading branch information
zomars committed Oct 10, 2024
2 parents e74154f + cc147d0 commit b9f3774
Show file tree
Hide file tree
Showing 51 changed files with 2,441 additions and 219 deletions.
2 changes: 1 addition & 1 deletion apps/api/v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.44",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.45",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
Expand Down
22 changes: 22 additions & 0 deletions apps/api/v2/src/modules/conferencing/conferencing.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ConferencingController } from "@/modules/conferencing/controllers/conferencing.controller";
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";

@Module({
imports: [PrismaModule],
providers: [
ConferencingService,
ConferencingRepository,
GoogleMeetService,
CredentialsRepository,
UsersRepository,
],
exports: [],
controllers: [ConferencingController],
})
export class ConferencingModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { ConferencingAppsOutputDto } from "@/modules/conferencing/outputs/get-conferencing-apps.output";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withApiAuth } from "test/utils/withApiAuth";

import {
ERROR_STATUS,
GOOGLE_CALENDAR_ID,
GOOGLE_CALENDAR_TYPE,
GOOGLE_MEET,
GOOGLE_MEET_TYPE,
SUCCESS_STATUS,
} from "@calcom/platform-constants";
import { ApiErrorResponse, ApiSuccessResponse } from "@calcom/platform-types";

describe("Conferencing Endpoints", () => {
describe("conferencing controller e2e tests", () => {
let app: INestApplication;

let userRepositoryFixture: UserRepositoryFixture;
let credentialsRepositoryFixture: CredentialsRepositoryFixture;

const userEmail = "conferencing-controller-user-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);
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);

user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});

app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);

await app.init();
});

it("should get all the conferencing apps of the auth user", async () => {
return request(app.getHttpServer())
.get(`/v2/conferencing`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toEqual([]);
});
});

it("should fail to connect google meet if google calendar is not connected ", async () => {
return request(app.getHttpServer())
.post(`/v2/conferencing/google-meet/connect`)
.expect(400)
.then(async () => {
await credentialsRepositoryFixture.create(GOOGLE_CALENDAR_TYPE, {}, user.id, GOOGLE_CALENDAR_ID);
});
});

it("should connect google meet if google calendar is connected ", async () => {
return request(app.getHttpServer())
.post(`/v2/conferencing/google-meet/connect`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
});
});

it("should set google meet as default conferencing app", async () => {
return request(app.getHttpServer())
.post(`/v2/conferencing/google-meet/default`)
.expect(200)
.then(async () => {
const updatedUser = await userRepositoryFixture.get(user.id);

expect(updatedUser).toBeDefined();

if (updatedUser) {
const metadata = updatedUser.metadata as { defaultConferencingApp?: { appSlug?: string } };
expect(metadata?.defaultConferencingApp?.appSlug).toEqual(GOOGLE_MEET);
}
});
});

it("should get all the conferencing apps of the auth user, and contain google meet", async () => {
return request(app.getHttpServer())
.get(`/v2/conferencing`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const googleMeet = responseBody.data.find((app) => app.type === GOOGLE_MEET_TYPE);
expect(googleMeet?.userId).toEqual(user.id);
});
});

it("should disconnect google meet", async () => {
return request(app.getHttpServer()).delete(`/v2/conferencing/google-meet/disconnect`).expect(200);
});

it("should get all the conferencing apps of the auth user, and not contain google meet", async () => {
return request(app.getHttpServer())
.get(`/v2/conferencing`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<ConferencingAppsOutputDto[]> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
const googleMeet = responseBody.data.find((app) => app.type === GOOGLE_MEET_TYPE);
expect(googleMeet).toBeUndefined();
});
});

afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await app.close();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import {
ConferencingAppsOutputResponseDto,
ConferencingAppOutputResponseDto,
ConferencingAppsOutputDto,
} from "@/modules/conferencing/outputs/get-conferencing-apps.output";
import { SetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/set-default-conferencing-app.output";
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
import {
Controller,
Get,
HttpCode,
HttpStatus,
Logger,
UseGuards,
Post,
Param,
BadRequestException,
Delete,
} from "@nestjs/common";
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToInstance } from "class-transformer";

import { CONFERENCING_APPS, GOOGLE_MEET, SUCCESS_STATUS } from "@calcom/platform-constants";

@Controller({
path: "/v2/conferencing",
version: API_VERSIONS_VALUES,
})
@DocsTags("Platform / Conferencing")
export class ConferencingController {
private readonly logger = new Logger("Platform Gcal Provider");

constructor(
private readonly conferencingService: ConferencingService,
private readonly googleMeetService: GoogleMeetService
) {}

@Post("/:app/connect")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Connect your conferencing application" })
async connect(
@GetUser("id") userId: number,
@Param("app") app: string
): Promise<ConferencingAppOutputResponseDto> {
switch (app) {
case GOOGLE_MEET:
const credential = await this.googleMeetService.connectGoogleMeetApp(userId);

return { status: SUCCESS_STATUS, data: plainToInstance(ConferencingAppsOutputDto, credential) };

default:
throw new BadRequestException(
"Invalid conferencing app, available apps are: ",
CONFERENCING_APPS.join(", ")
);
}
}

@Get("/")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "List your conferencing applications" })
async listConferencingApps(@GetUser("id") userId: number): Promise<ConferencingAppsOutputResponseDto> {
const conferencingApps = await this.conferencingService.getConferencingApps(userId);

const data = conferencingApps.map((conferencingApps) =>
plainToInstance(ConferencingAppsOutputDto, conferencingApps)
);

return { status: SUCCESS_STATUS, data };
}

@Post("/:app/default")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Set your default conferencing application" })
async default(
@GetUser("id") userId: number,
@Param("app") app: string
): Promise<SetDefaultConferencingAppOutputResponseDto> {
switch (app) {
case GOOGLE_MEET:
await this.googleMeetService.setDefault(userId);

return { status: SUCCESS_STATUS };

default:
throw new BadRequestException(
"Invalid conferencing app, available apps are: ",
CONFERENCING_APPS.join(", ")
);
}
}

@Delete("/:app/disconnect")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard)
@ApiOperation({ summary: "Disconnect your conferencing application" })
async disconnect(
@GetUser("id") userId: number,
@Param("app") app: string
): Promise<ConferencingAppOutputResponseDto> {
switch (app) {
case GOOGLE_MEET:
const credential = await this.googleMeetService.disconnectGoogleMeetApp(userId);

return { status: SUCCESS_STATUS, data: plainToInstance(ConferencingAppsOutputDto, credential) };

default:
throw new BadRequestException(
"Invalid conferencing app, available apps are: ",
CONFERENCING_APPS.join(", ")
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsString, ValidateNested, IsEnum, IsNumber, IsOptional, IsBoolean } from "class-validator";

import { ERROR_STATUS, GOOGLE_MEET_TYPE, SUCCESS_STATUS } from "@calcom/platform-constants";

export class ConferencingAppsOutputDto {
@Expose()
@IsNumber()
@ApiProperty({ description: "Id of the conferencing app credentials" })
id!: number;

@ApiProperty({ example: GOOGLE_MEET_TYPE, description: "Type of conferencing app" })
@Expose()
@IsString()
type!: string;

@ApiProperty({ description: "Id of the user associated to the conferencing app" })
@Expose()
@IsNumber()
userId!: number;

@ApiProperty({ example: true, description: "Whether if the connection is working or not." })
@Expose()
@IsBoolean()
@IsOptional()
invalid?: boolean | null;
}

export class ConferencingAppsOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;

@Expose()
@ValidateNested()
@Type(() => ConferencingAppsOutputDto)
data!: ConferencingAppsOutputDto[];
}

export class ConferencingAppOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;

@Expose()
@ValidateNested()
@Type(() => ConferencingAppsOutputDto)
data!: ConferencingAppsOutputDto;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum } from "class-validator";

import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";

export class SetDefaultConferencingAppOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";

import { GOOGLE_MEET_TYPE } from "@calcom/platform-constants";

@Injectable()
export class ConferencingRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}

async findConferencingApps(userId: number) {
return this.dbRead.prisma.credential.findMany({
where: {
userId,
type: { endsWith: "_video" },
},
});
}

async findGoogleMeet(userId: number) {
return this.dbRead.prisma.credential.findFirst({
where: { userId, type: GOOGLE_MEET_TYPE },
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
import { Logger } from "@nestjs/common";
import { Injectable } from "@nestjs/common";

@Injectable()
export class ConferencingService {
private logger = new Logger("ConferencingService");

constructor(private readonly conferencingRepository: ConferencingRepository) {}

async getConferencingApps(userId: number) {
return this.conferencingRepository.findConferencingApps(userId);
}
}
Loading

0 comments on commit b9f3774

Please sign in to comment.