Skip to content

Commit 4a259b3

Browse files
authored
Merge branch 'main' into rajiv-demo
2 parents 9faa02e + ebeb008 commit 4a259b3

File tree

96 files changed

+6300
-1560
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+6300
-1560
lines changed

.yarn/versions/45e15b36.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
undecided:
2+
- "@calcom/prisma"

apps/api/v1/pages/api/availability/_get.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { NextApiRequest } from "next";
22
import { z } from "zod";
33

4-
import { getUserAvailabilityService } from "@calcom/lib/di/containers/get-user-availability";
4+
import { getUserAvailabilityService } from "@calcom/lib/di/containers/GetUserAvailability";
55
import { HttpError } from "@calcom/lib/http-error";
66
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
77
import prisma from "@calcom/prisma";

apps/api/v1/pages/api/slots/_get.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
44

55
import dayjs from "@calcom/dayjs";
66
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
7-
import { getAvailableSlotsService } from "@calcom/lib/di/containers/available-slots";
7+
import { getAvailableSlotsService } from "@calcom/lib/di/containers/AvailableSlots";
88
import { HttpError } from "@calcom/lib/http-error";
99
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
1010
import { createContext } from "@calcom/trpc/server/createContext";

apps/api/v1/test/lib/bookings/_post.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,13 @@ vi.mock("@calcom/features/watchlist/operations/check-if-users-are-blocked.contro
7676
checkIfUsersAreBlocked: vi.fn().mockResolvedValue(false),
7777
}));
7878

79-
vi.mock("@calcom/lib/bookings/findQualifiedHostsWithDelegationCredentials", () => ({
80-
findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({
81-
qualifiedRRHosts: [],
82-
allFallbackRRHosts: [],
83-
fixedHosts: [],
79+
vi.mock("@calcom/lib/di/containers/QualifiedHosts", () => ({
80+
getQualifiedHostsService: vi.fn().mockReturnValue({
81+
findQualifiedHostsWithDelegationCredentials: vi.fn().mockResolvedValue({
82+
qualifiedRRHosts: [],
83+
allFallbackRRHosts: [],
84+
fixedHosts: [],
85+
}),
8486
}),
8587
}));
8688

apps/api/v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@axiomhq/winston": "^1.2.0",
3939
"@calcom/platform-constants": "*",
4040
"@calcom/platform-enums": "*",
41-
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.291",
41+
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.298",
4242
"@calcom/platform-types": "*",
4343
"@calcom/platform-utils": "*",
4444
"@calcom/prisma": "*",
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { bootstrap } from "@/app";
2+
import { AppModule } from "@/app.module";
3+
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
4+
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
5+
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
6+
import { TokensModule } from "@/modules/tokens/tokens.module";
7+
import { UsersModule } from "@/modules/users/users.module";
8+
import { INestApplication } from "@nestjs/common";
9+
import { NestExpressApplication } from "@nestjs/platform-express";
10+
import { Test } from "@nestjs/testing";
11+
import * as request from "supertest";
12+
13+
import { SUCCESS_STATUS } from "@calcom/platform-constants";
14+
import { CreatePrivateLinkInput } from "@calcom/platform-types";
15+
16+
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
17+
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
18+
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
19+
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
20+
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
21+
import { withApiAuth } from "test/utils/withApiAuth";
22+
import { randomString } from "test/utils/randomString";
23+
24+
describe("Event Types Private Links Endpoints", () => {
25+
let app: INestApplication;
26+
27+
let oAuthClient: any;
28+
let organization: any;
29+
let userRepositoryFixture: UserRepositoryFixture;
30+
let teamRepositoryFixture: TeamRepositoryFixture;
31+
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
32+
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
33+
let user: any;
34+
let eventType: any;
35+
36+
const userEmail = `private-links-user-${randomString()}@api.com`;
37+
38+
beforeAll(async () => {
39+
const moduleRef = await withApiAuth(
40+
userEmail,
41+
Test.createTestingModule({
42+
providers: [PrismaExceptionFilter, HttpExceptionFilter],
43+
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
44+
})
45+
)
46+
.overrideGuard(PermissionsGuard)
47+
.useValue({
48+
canActivate: () => true,
49+
})
50+
.compile();
51+
52+
app = moduleRef.createNestApplication();
53+
bootstrap(app as NestExpressApplication);
54+
55+
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
56+
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
57+
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
58+
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
59+
60+
organization = await teamRepositoryFixture.create({
61+
name: `private-links-organization-${randomString()}`,
62+
slug: `private-links-org-slug-${randomString()}`,
63+
});
64+
oAuthClient = await createOAuthClient(organization.id);
65+
user = await userRepositoryFixture.create({
66+
email: userEmail,
67+
name: `private-links-user-${randomString()}`,
68+
username: `private-links-user-${randomString()}`,
69+
});
70+
71+
// create an event type owned by user
72+
eventType = await eventTypesRepositoryFixture.create(
73+
{
74+
title: `private-links-event-type-${randomString()}`,
75+
slug: `private-links-event-type-${randomString()}`,
76+
length: 30,
77+
locations: [],
78+
},
79+
user.id
80+
);
81+
82+
await app.init();
83+
});
84+
85+
async function createOAuthClient(organizationId: number) {
86+
const data = {
87+
logo: "logo-url",
88+
name: "name",
89+
redirectUris: ["redirect-uri"],
90+
permissions: 32,
91+
};
92+
const secret = "secret";
93+
94+
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
95+
return client;
96+
}
97+
98+
it("POST /v2/event-types/:eventTypeId/private-links - create private link", async () => {
99+
const body: CreatePrivateLinkInput = {
100+
expiresAt: undefined,
101+
maxUsageCount: 5,
102+
};
103+
104+
const response = await request(app.getHttpServer())
105+
.post(`/api/v2/event-types/${eventType.id}/private-links`)
106+
.set("Authorization", `Bearer whatever`)
107+
.send(body)
108+
.expect(201);
109+
110+
expect(response.body.status).toBe(SUCCESS_STATUS);
111+
expect(response.body.data.linkId).toBeDefined();
112+
expect(response.body.data.maxUsageCount).toBe(5);
113+
expect(response.body.data.usageCount).toBeDefined();
114+
});
115+
116+
it("GET /v2/event-types/:eventTypeId/private-links - list private links", async () => {
117+
const response = await request(app.getHttpServer())
118+
.get(`/api/v2/event-types/${eventType.id}/private-links`)
119+
.set("Authorization", `Bearer whatever`)
120+
.expect(200);
121+
122+
expect(response.body.status).toBe(SUCCESS_STATUS);
123+
expect(Array.isArray(response.body.data)).toBe(true);
124+
expect(response.body.data.length).toBeGreaterThanOrEqual(1);
125+
});
126+
127+
it("PATCH /v2/event-types/:eventTypeId/private-links/:linkId - update private link", async () => {
128+
// create a link first
129+
const createResp = await request(app.getHttpServer())
130+
.post(`/api/v2/event-types/${eventType.id}/private-links`)
131+
.set("Authorization", `Bearer whatever`)
132+
.send({ maxUsageCount: 3 })
133+
.expect(201);
134+
135+
const linkId = createResp.body.data.linkId;
136+
137+
const response = await request(app.getHttpServer())
138+
.patch(`/api/v2/event-types/${eventType.id}/private-links/${linkId}`)
139+
.set("Authorization", `Bearer whatever`)
140+
.send({ maxUsageCount: 10 })
141+
.expect(200);
142+
143+
expect(response.body.status).toBe(SUCCESS_STATUS);
144+
expect(response.body.data.maxUsageCount).toBe(10);
145+
});
146+
147+
it("DELETE /v2/event-types/:eventTypeId/private-links/:linkId - delete private link", async () => {
148+
// create a link to delete
149+
const createResp = await request(app.getHttpServer())
150+
.post(`/api/v2/event-types/${eventType.id}/private-links`)
151+
.set("Authorization", `Bearer whatever`)
152+
.send({ maxUsageCount: 2 })
153+
.expect(201);
154+
155+
const linkId = createResp.body.data.linkId;
156+
157+
const response = await request(app.getHttpServer())
158+
.delete(`/api/v2/event-types/${eventType.id}/private-links/${linkId}`)
159+
.set("Authorization", `Bearer whatever`)
160+
.expect(200);
161+
162+
expect(response.body.status).toBe(SUCCESS_STATUS);
163+
expect(response.body.data.linkId).toBe(linkId);
164+
});
165+
166+
afterAll(async () => {
167+
// cleanup created entities
168+
try {
169+
if (eventType?.id) {
170+
const repo = new EventTypesRepositoryFixture((app as any).select(AppModule));
171+
await repo.delete(eventType.id);
172+
}
173+
} catch {}
174+
await app.close();
175+
});
176+
});
177+
178+
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
2+
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
3+
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
4+
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
5+
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
6+
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
7+
import {
8+
Body,
9+
Controller,
10+
Delete,
11+
Get,
12+
Param,
13+
ParseIntPipe,
14+
Patch,
15+
Post,
16+
UseGuards,
17+
} from "@nestjs/common";
18+
import { ApiHeader, ApiOperation, ApiTags as DocsTags, OmitType } from "@nestjs/swagger";
19+
20+
import {
21+
EVENT_TYPE_READ,
22+
EVENT_TYPE_WRITE,
23+
SUCCESS_STATUS,
24+
} from "@calcom/platform-constants";
25+
import {
26+
CreatePrivateLinkInput,
27+
CreatePrivateLinkOutput,
28+
DeletePrivateLinkOutput,
29+
GetPrivateLinksOutput,
30+
UpdatePrivateLinkInput,
31+
UpdatePrivateLinkOutput,
32+
} from "@calcom/platform-types";
33+
34+
import { PrivateLinksService } from "../services/private-links.service";
35+
36+
class UpdatePrivateLinkBody extends OmitType(UpdatePrivateLinkInput, ["linkId"] as const) {}
37+
38+
@Controller({
39+
path: "/v2/event-types/:eventTypeId/private-links",
40+
})
41+
@UseGuards(PermissionsGuard)
42+
@DocsTags("Event Types Private Links")
43+
export class EventTypesPrivateLinksController {
44+
constructor(private readonly privateLinksService: PrivateLinksService) {}
45+
46+
@Post("/")
47+
@Permissions([EVENT_TYPE_WRITE])
48+
@UseGuards(ApiAuthGuard, EventTypeOwnershipGuard)
49+
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
50+
@ApiOperation({ summary: "Create a private link for an event type" })
51+
async createPrivateLink(
52+
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
53+
@Body() body: CreatePrivateLinkInput,
54+
@GetUser("id") userId: number
55+
): Promise<CreatePrivateLinkOutput> {
56+
const privateLink = await this.privateLinksService.createPrivateLink(eventTypeId, userId, body);
57+
58+
return {
59+
status: SUCCESS_STATUS,
60+
data: privateLink,
61+
};
62+
}
63+
64+
@Get("/")
65+
@Permissions([EVENT_TYPE_READ])
66+
@UseGuards(ApiAuthGuard, EventTypeOwnershipGuard)
67+
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
68+
@ApiOperation({ summary: "Get all private links for an event type" })
69+
async getPrivateLinks(
70+
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
71+
@GetUser("id") userId: number
72+
): Promise<GetPrivateLinksOutput> {
73+
const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId, userId);
74+
75+
return {
76+
status: SUCCESS_STATUS,
77+
data: privateLinks,
78+
};
79+
}
80+
81+
@Patch("/:linkId")
82+
@Permissions([EVENT_TYPE_WRITE])
83+
@UseGuards(ApiAuthGuard, EventTypeOwnershipGuard)
84+
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
85+
@ApiOperation({ summary: "Update a private link for an event type" })
86+
async updatePrivateLink(
87+
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
88+
@Param("linkId") linkId: string,
89+
@Body() body: UpdatePrivateLinkBody,
90+
@GetUser("id") userId: number
91+
): Promise<UpdatePrivateLinkOutput> {
92+
const updateInput = { ...body, linkId };
93+
const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, userId, updateInput);
94+
95+
return {
96+
status: SUCCESS_STATUS,
97+
data: privateLink,
98+
};
99+
}
100+
101+
@Delete("/:linkId")
102+
@Permissions([EVENT_TYPE_WRITE])
103+
@UseGuards(ApiAuthGuard, EventTypeOwnershipGuard)
104+
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
105+
@ApiOperation({ summary: "Delete a private link for an event type" })
106+
async deletePrivateLink(
107+
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
108+
@Param("linkId") linkId: string,
109+
@GetUser("id") userId: number
110+
): Promise<DeletePrivateLinkOutput> {
111+
await this.privateLinksService.deletePrivateLink(eventTypeId, userId, linkId);
112+
113+
return {
114+
status: SUCCESS_STATUS,
115+
data: {
116+
linkId,
117+
message: "Private link deleted successfully",
118+
},
119+
};
120+
}
121+
}
122+
123+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Module } from "@nestjs/common";
2+
import { TokensModule } from "@/modules/tokens/tokens.module";
3+
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
4+
import { PrismaModule } from "@/modules/prisma/prisma.module";
5+
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
6+
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
7+
8+
import { EventTypesPrivateLinksController } from "./controllers/event-types-private-links.controller";
9+
import { PrivateLinksInputService } from "./services/private-links-input.service";
10+
import { PrivateLinksOutputService } from "./services/private-links-output.service";
11+
import { PrivateLinksService } from "./services/private-links.service";
12+
import { PrivateLinksRepository } from "./private-links.repository";
13+
14+
@Module({
15+
imports: [TokensModule, OAuthClientModule, PrismaModule, EventTypesModule_2024_06_14],
16+
controllers: [EventTypesPrivateLinksController],
17+
providers: [
18+
PrivateLinksService,
19+
PrivateLinksInputService,
20+
PrivateLinksOutputService,
21+
PrivateLinksRepository,
22+
EventTypeOwnershipGuard,
23+
],
24+
})
25+
export class EventTypesPrivateLinksModule {}
26+
27+

0 commit comments

Comments
 (0)