Compare commits

...
16 Commits
Author SHA1 Message Date
GitHub Actions 6f6454df40 chore: release v5.4.1
Post release / release (push) Canceled after 0s
2025-06-17 13:27:30 +00:00
Morgan 3da9fc5500 chore: add global prefix to api v2 via env variable for selfhost (#21860) 2025-06-17 12:55:15 +00:00
Lauris Skraucis 540bf868b5 fix: event type settings atom not saving (#21864)
* chore: specify atoms version

* fix: saving event type settings
2025-06-17 12:09:37 +00:00
Anik Dhabal Babu 1674ecc585 fix: replyTo includes the correct email (#21843)
* fix: delete dilog button not visible

* fix: replyTo includes the correct email

* Update getReplyToHeader.ts
2025-06-17 10:45:44 +00:00
4920abdaf7 feat: optimize Prisma queries by replacing findFirst with findUnique where applicable (#21826)
* feat: optimize Prisma queries by replacing findFirst with findUnique where applicable

- Replace findFirst/findFirstOrThrow with findUnique/findUniqueOrThrow for queries using unique constraints
- Maintain existing functionality and error handling behavior
- Focus on queries using primary keys and unique index fields from schema
- Revert problematic changes that caused test failures to maintain stability

Co-Authored-By: benny@cal.com <benny@cal.com>

* revert: exclude API files from Prisma query optimizations per user request

- Reverted all 55 API-related files to their original state
- Kept all non-API Prisma query optimizations intact
- API files include apps/api/v1, apps/api/v2, apps/web/app/api, and packages/app-store/*/api
- Non-API optimizations remain for packages/lib, packages/features, apps/web (non-api), etc.

Co-Authored-By: benny@cal.com <benny@cal.com>

* feat: optimize membership query in attributeUtils to use findUnique with userId_teamId constraint

Co-Authored-By: benny@cal.com <benny@cal.com>

* revert: exclude test files from Prisma query optimizations per user request

Co-Authored-By: benny@cal.com <benny@cal.com>

* revert: revert attributeUtils.ts to use findFirst for test compatibility

Co-Authored-By: benny@cal.com <benny@cal.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: benny@cal.com <benny@cal.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-06-17 09:52:02 +03:00
Somay Chauhan a6b98bd35d docs: expand Google Calendar event ID description with API endpoint references (#21857) 2025-06-17 07:02:50 +02:00
Lauris Skraucis 34d4f394fc fix: v2 event type disable guests (#21753)
* fix: v2 event type disable guests

* fix: implement cubic feedback
2025-06-17 10:15:46 +05:30
Joe Au-Yeung a4ef8d4200 Auto review seeded orgs (#21856) 2025-06-17 03:40:56 +00:00
Lauris Skraucis 9d9662687e docs: v2 cancelling seated booking as a host (#21744)
* docs: cancel seated bookings as attendee or host

* test: cancelling seated booking as host
2025-06-17 09:06:17 +05:30
Anik Dhabal Babu 2d41c719c4 fix: reschedule and cancel unconfirmed event from email (#21844) 2025-06-16 17:13:38 +00:00
b69bd966a6 fix: duplicate workflow reminders caused by scanWorkflowBody (#21767)
* don't scan if there is newer task

* create seperate tasks per workflowStepId

* revert changed constant

* fix comment

* add back missing import

* code clean up

* fix PR feedback

* change function to hasNewerScanTaskForStepId

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-06-16 14:25:41 +00:00
GitHub Actions 09a49dd0fd chore: release v5.4.0
Post release / release (push) Canceled after 0s
2025-06-16 13:00:34 +00:00
MorganandAnik Dhabal Babu 3dd3372c35 fix: getBookings for team admins within orgs (#21833)
* fix: getBookings for team admins within orgs

* fixup! fix: getBookings for team admins within orgs

* chore: update getUserIdsAndEmailsWhereUserIsAdminOrOwner and remove unused orgId

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-06-16 09:34:02 -03:00
Anik Dhabal Babu 0a60864310 fix: Few flaky e2e test (#21818)
* fix: delete dilog button not visible

* fix: flaky e2e tests

* Update booking.e2e.ts

* update

* Update booking.e2e.ts

* update

* Update booking.e2e.ts

* Update booking.e2e.ts
2025-06-16 09:51:37 +00:00
Benny Joo cb24e5d0aa perf: remove 2 DB fetches by removing getFacetedValues (#21808)
* perf: remove 2 server fetches by removing getFacetedValues

* fix

* fix

* use set

* fix
2025-06-15 11:31:11 +00:00
Romit. 8c5aebdca1 ux improve (#21815) 2025-06-15 11:30:34 +00:00
147 changed files with 849 additions and 475 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": patch
---
fix saving event type settings
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+1 -1
View File
@@ -37,7 +37,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.222",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.223",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
+4
View File
@@ -81,5 +81,9 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
app.use(cookieParser());
if (process?.env?.API_GLOBAL_PREFIX) {
app.setGlobalPrefix(process?.env?.API_GLOBAL_PREFIX);
}
return app;
};
@@ -267,7 +267,14 @@ export class BookingsController_2024_08_13 {
@ApiOperation({
summary: "Cancel a booking",
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
\nCancelling seated bookings:
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
\n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
\nCancelling recurring seated bookings:
For recurring seated bookings it is not possible to cancel all of them with 1 call
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
})
@ApiBody({
@@ -14,29 +14,27 @@ import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import { DateTime } from "luxon";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import {
CancelBookingInput_2024_08_13,
CancelSeatedBookingInput_2024_08_13,
CreateRecurringSeatedBookingOutput_2024_08_13,
CreateSeatedBookingOutput_2024_08_13,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetRecurringSeatedBookingOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import {
CreateBookingInput_2024_08_13,
CreateRecurringBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
@@ -51,11 +49,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
const userEmail = `seated-bookings-user-${randomString()}@api.com`;
let user: User;
let apiKeyString: string;
let seatedEventTypeId: number;
const maxRecurrenceCount = 3;
@@ -63,6 +63,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
const emailAttendeeOne = `seated-bookings-attendee1-${randomString()}@api.com`;
const nameAttendeeOne = `Attendee One ${randomString()}`;
@@ -70,12 +71,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
const nameAttendeeTwo = `Attendee Two ${randomString()}`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
const moduleRef = await Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
@@ -87,22 +85,21 @@ describe("Bookings Endpoints 2024-08-13", () => {
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
bookingSeatRepositoryFixture = new BookingSeatRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({
name: `seated-bookings-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
apiKeyString = `cal_test_${keyString}`;
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `seated-bookings-2024-08-13-schedule-${randomString()}`,
timeZone: "Europe/Rome",
@@ -118,6 +115,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
metadata: {
disableStandardEmails: {
all: {
attendee: true,
host: true,
},
},
},
},
user.id
);
@@ -129,19 +134,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should book an event type with seats for the first time", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
@@ -333,6 +325,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
return request(app.getHttpServer())
.get("/v2/bookings?sortCreated=asc")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
@@ -409,67 +402,186 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should cancel seated booking", async () => {
const body: CancelSeatedBookingInput_2024_08_13 = {
seatUid: createdSeatedBooking.seatUid,
};
describe("cancel seated booking", () => {
describe("cancel seated booking as attendee", () => {
it("should cancel seated booking", async () => {
const body: CancelSeatedBookingInput_2024_08_13 = {
seatUid: createdSeatedBooking.seatUid,
};
return request(app.getHttpServer())
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
return request(app.getHttpServer())
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
if (responseDataIsGetSeatedBooking(responseBody.data)) {
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("cancelled");
expect(data.start).toEqual(createdSeatedBooking.start);
expect(data.end).toEqual(createdSeatedBooking.end);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeId);
expect(data.eventType).toEqual({
id: seatedEventTypeId,
slug: seatedEventSlug,
if (responseDataIsGetSeatedBooking(responseBody.data)) {
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("cancelled");
expect(data.start).toEqual(createdSeatedBooking.start);
expect(data.end).toEqual(createdSeatedBooking.end);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeId);
expect(data.eventType).toEqual({
id: seatedEventTypeId,
slug: seatedEventSlug,
});
expect(data.attendees.length).toEqual(0);
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
} else {
throw new Error("Invalid response data - expected booking but received array response");
}
});
expect(data.attendees.length).toEqual(0);
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
} else {
throw new Error("Invalid response data - expected booking but received array response");
}
});
});
describe("cancel seated booking as host", () => {
it("should book an event type with seats", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(),
eventTypeId: seatedEventTypeId,
attendee: {
name: nameAttendeeOne,
email: emailAttendeeOne,
timeZone: "Europe/Rome",
language: "it",
},
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.seatUid).toBeDefined();
const seatUid = data.seatUid;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeId);
expect(data.eventType).toEqual({
id: seatedEventTypeId,
slug: seatedEventSlug,
});
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: body.attendee.name,
email: body.attendee.email,
timeZone: body.attendee.timeZone,
language: body.attendee.language,
absent: false,
seatUid,
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
},
metadata: body.metadata,
});
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
createdSeatedBooking2 = data;
} else {
throw new Error(
"Invalid response data - expected recurring booking but received non array response"
);
}
});
});
it("should not be able to cancel without cancellation reason", async () => {
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${createdSeatedBooking2.uid}/cancel`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(400);
expect(response.body.message).toEqual("Cancellation reason is required when you are the host");
});
it("should cancel seated booking", async () => {
const body: CancelBookingInput_2024_08_13 = {
cancellationReason: "I will be travelling without internet",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${createdSeatedBooking2.uid}/cancel`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${apiKeyString}`)
.expect(200)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
if (responseDataIsGetSeatedBooking(responseBody.data)) {
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.status).toEqual("cancelled");
expect(data.start).toEqual(createdSeatedBooking2.start);
expect(data.end).toEqual(createdSeatedBooking2.end);
expect(data.duration).toEqual(60);
expect(data.eventTypeId).toEqual(seatedEventTypeId);
expect(data.eventType).toEqual({
id: seatedEventTypeId,
slug: seatedEventSlug,
});
expect(data.attendees.length).toEqual(0);
expect(data.location).toBeDefined();
expect(data.absentHost).toEqual(false);
expect(data.cancellationReason).toEqual("I will be travelling without internet");
const seats = await bookingSeatRepositoryFixture.findAllByBookingId(data.id);
expect(seats.length).toEqual(0);
} else {
throw new Error("Invalid response data - expected booking but received array response");
}
});
});
});
});
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
return data.hasOwnProperty("seatUid");
}
function responseDataIsCreateRecurringSeatedBooking(
data: any
): data is CreateRecurringSeatedBookingOutput_2024_08_13[] {
return Array.isArray(data);
}
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
}
function responseDataIsGetRecurringSeatedBooking(
data: any
): data is GetRecurringSeatedBookingOutput_2024_08_13[] {
return Array.isArray(data);
}
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
@@ -6,7 +6,7 @@ import {
getRecordingsOfCalVideoByRoomName,
getAllTranscriptsAccessLinkFromRoomName,
getDownloadLinkOfCalVideoByRecordingId,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/conferencing";
@Injectable()
export class CalVideoService {
@@ -266,6 +266,7 @@ export class OutputBookingsService_2024_08_13 {
description: databaseBooking.description,
hosts: [this.getHost(databaseBooking.user)],
status: databaseBooking.status.toLowerCase(),
cancellationReason: databaseBooking.cancellationReason || undefined,
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
start: databaseBooking.startTime,
end: databaseBooking.endTime,
@@ -34,6 +34,7 @@ import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
GuestsDefaultFieldOutput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
@@ -535,6 +536,7 @@ describe("Event types Endpoints", () => {
body.lockTimeZoneToggleOnBookingPage
);
expect(createdEventType.color).toEqual(body.color);
expect(createdEventType.disableGuests).toEqual(false);
const expectedBookingFields = [
{ ...defaultResponseBookingFieldName, ...nameBookingField },
@@ -1041,6 +1043,7 @@ describe("Event types Endpoints", () => {
expect(updatedEventType.description).toEqual(eventType.description);
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
expect(updatedEventType.locations).toEqual(eventType.locations);
expect(updatedEventType.disableGuests).toEqual(false);
const expectedBookingFields = [
{ ...defaultResponseBookingFieldName, ...nameBookingField },
@@ -1887,6 +1890,96 @@ describe("Event types Endpoints", () => {
});
});
describe("toggle disable guests", () => {
let eventTypeWithGuestsDisabledId: number;
it("should create an event type with guests disabled", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class guests disabled",
slug: "coding-class-guests-disabled",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
disableGuests: true,
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.disableGuests).toEqual(true);
const guestsBookingField = createdEventType.bookingFields.find(
(field) => field.slug === "guests"
) as GuestsDefaultFieldOutput_2024_06_14 | undefined;
expect(guestsBookingField?.hidden).toEqual(true);
eventTypeWithGuestsDisabledId = createdEventType.id;
});
});
it("should update an event type with guests enabled", async () => {
const body: UpdateEventTypeInput_2024_06_14 = {
disableGuests: false,
};
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventTypeWithGuestsDisabledId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
expect(updatedEventType.disableGuests).toEqual(false);
const guestsBookingField = updatedEventType.bookingFields.find(
(field) => field.slug === "guests"
) as GuestsDefaultFieldOutput_2024_06_14 | undefined;
expect(guestsBookingField?.hidden).toEqual(false);
eventTypeWithGuestsDisabledId = updatedEventType.id;
});
});
it("should update an event type with guests disabled", async () => {
const body: UpdateEventTypeInput_2024_06_14 = {
disableGuests: true,
};
return request(app.getHttpServer())
.patch(`/api/v2/event-types/${eventTypeWithGuestsDisabledId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
expect(updatedEventType.disableGuests).toEqual(true);
const guestsBookingField = updatedEventType.bookingFields.find(
(field) => field.slug === "guests"
) as GuestsDefaultFieldOutput_2024_06_14 | undefined;
expect(guestsBookingField?.hidden).toEqual(true);
eventTypeWithGuestsDisabledId = updatedEventType.id;
});
});
afterAll(async () => {
if (eventTypeWithGuestsDisabledId) {
await eventTypesRepositoryFixture.delete(eventTypeWithGuestsDisabledId);
}
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -32,6 +32,7 @@ import { validateCustomEventName, EventTypeMetaDataSchema } from "@calcom/platfo
import {
CreateEventTypeInput_2024_06_14,
DestinationCalendar_2024_06_14,
InputBookingField_2024_06_14,
OutputUnknownLocation_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
@@ -117,16 +118,23 @@ export class InputEventTypesService_2024_06_14 {
seats,
customName,
useDestinationCalendarEmail,
disableGuests,
...rest
} = inputEventType;
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const locationsTransformed = locations?.length ? this.transformInputLocations(locations) : undefined;
const effectiveBookingFields =
disableGuests !== undefined
? this.getBookingFieldsWithGuestsToggled(bookingFields, disableGuests)
: bookingFields;
const eventType = {
...rest,
length: lengthInMinutes,
locations: locationsTransformed,
bookingFields: this.transformInputBookingFields(bookingFields),
bookingFields: this.transformInputBookingFields(effectiveBookingFields),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -167,6 +175,7 @@ export class InputEventTypesService_2024_06_14 {
seats,
customName,
useDestinationCalendarEmail,
disableGuests,
...rest
} = inputEventType;
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithMetaData(eventTypeId);
@@ -176,11 +185,18 @@ export class InputEventTypesService_2024_06_14 {
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const effectiveBookingFields =
disableGuests !== undefined
? this.getBookingFieldsWithGuestsToggled(bookingFields, disableGuests)
: bookingFields;
const eventType = {
...rest,
length: lengthInMinutes,
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
bookingFields: effectiveBookingFields
? this.transformInputBookingFields(effectiveBookingFields)
: undefined,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -206,6 +222,27 @@ export class InputEventTypesService_2024_06_14 {
return eventType;
}
getBookingFieldsWithGuestsToggled(
bookingFields: InputBookingField_2024_06_14[] | undefined,
hideGuests: boolean
) {
const toggledGuestsBookingField: InputBookingField_2024_06_14 = { slug: "guests", hidden: hideGuests };
if (!bookingFields) {
return [toggledGuestsBookingField];
}
const bookingFieldsCopy = [...bookingFields];
const guestsBookingField = bookingFieldsCopy.find((field) => "slug" in field && field.slug === "guests");
if (guestsBookingField) {
Object.assign(guestsBookingField, { hidden: hideGuests });
return bookingFieldsCopy;
}
bookingFieldsCopy.push(toggledGuestsBookingField);
return bookingFieldsCopy;
}
transformInputLocations(inputLocations: CreateEventTypeInput_2024_06_14["locations"]) {
return transformLocationsApiToInternal(inputLocations);
}
@@ -25,7 +25,8 @@ export class CalUnifiedCalendarsController {
})
@ApiParam({
name: "eventUid",
description: "The Google Calendar event ID",
description:
"The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For team events: https://cal.com/docs/api-reference/v2/orgs-teams-bookings/get-booking-references-for-a-booking\n\n- For user events: https://cal.com/docs/api-reference/v2/bookings/get-booking-references-for-a-booking",
type: String,
})
@Get("/:calendar/event/:eventUid")
+13 -4
View File
@@ -7653,7 +7653,7 @@
"post": {
"operationId": "BookingsController_2024_08_13_cancelBooking",
"summary": "Cancel a booking",
"description": ":bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.\n For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call\n like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.",
"description": ":bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.\n \n \nCancelling seated bookings:\n It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.\n \n1. As an attendee - provide :bookingUid in the request URL `/bookings/:bookingUid/cancel` and seatUid in the request body `{\"seatUid\": \"123-123-123\"}` . This will remove this particular attendance from the booking.\n \n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL `/bookings/:bookingUid/cancel` and cancellationReason in the request body `{\"cancellationReason\": \"Will travel\"}` and `Authorization: Bearer token` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.\n \n \nCancelling recurring seated bookings:\n For recurring seated bookings it is not possible to cancel all of them with 1 call\n like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.",
"parameters": [
{
"name": "cal-api-version",
@@ -8142,7 +8142,7 @@
"name": "eventUid",
"required": true,
"in": "path",
"description": "The Google Calendar event ID",
"description": "The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For team events: https://cal.com/docs/api-reference/v2/orgs-teams-bookings/get-booking-references-for-a-booking\n\n- For user events: https://cal.com/docs/api-reference/v2/bookings/get-booking-references-for-a-booking",
"schema": {
"type": "string"
}
@@ -10695,6 +10695,16 @@
"type": "string"
}
},
{
"name": "bookingUidToReschedule",
"required": false,
"in": "query",
"description": "The unique identifier of the booking being rescheduled. When provided will ensure that the original booking time appears within the returned available slots when rescheduling.",
"example": "abc123def456",
"schema": {
"type": "string"
}
},
{
"name": "routingFormId",
"required": true,
@@ -25447,8 +25457,7 @@
"startTs",
"status",
"duration",
"shareToken",
"downloadLink"
"shareToken"
]
},
"GetBookingRecordingsOutput": {
@@ -16,4 +16,8 @@ export class BookingSeatRepositoryFixture {
async create(bookingSeat: Prisma.BookingSeatCreateInput) {
return this.prismaWriteClient.bookingSeat.create({ data: bookingSeat });
}
async findAllByBookingId(bookingId: number) {
return this.prismaReadClient.bookingSeat.findMany({ where: { bookingId } });
}
}
+2 -1
View File
@@ -24,7 +24,8 @@
"@calcom/platform-libraries/emails": ["../../../packages/platform/libraries/emails.ts"],
"@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"],
"@calcom/platform-libraries/app-store": ["../../../packages/platform/libraries/app-store.ts"],
"@calcom/platform-libraries/workflows": ["../../../packages/platform/libraries/workflows.ts"]
"@calcom/platform-libraries/workflows": ["../../../packages/platform/libraries/workflows.ts"],
"@calcom/platform-libraries/conferencing": ["../../../packages/platform/libraries/conferencing.ts"]
},
"incremental": true,
"skipLibCheck": true,
@@ -3,6 +3,7 @@ import { _generateMetadata } from "app/_utils";
import { unstable_cache } from "next/cache";
import { AttributeRepository } from "@calcom/lib/server/repository/attribute";
import { MembershipRole } from "@calcom/prisma/enums";
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
import { MembersView } from "~/members/members-view";
@@ -26,12 +27,20 @@ const getCachedAttributes = unstable_cache(
const Page = async () => {
const orgCaller = await createRouterCaller(viewerOrganizationsRouter);
const [org, teams, facetedTeamValues] = await Promise.all([
orgCaller.listCurrent(),
orgCaller.getTeams(),
orgCaller.getFacetedValues(),
]);
const [org, teams] = await Promise.all([orgCaller.listCurrent(), orgCaller.getTeams()]);
const attributes = await getCachedAttributes(org.id);
const facetedTeamValues = {
roles: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER],
teams,
attributes: attributes.map((attribute) => ({
id: attribute.id,
name: attribute.name,
options: Array.from(new Set(attribute.options.map((option) => option.value))).map((value) => ({
value,
})),
})),
};
return (
<MembersView org={org} teams={teams} facetedTeamValues={facetedTeamValues} attributes={attributes} />
);
@@ -134,7 +134,7 @@ const getEventTypes = async (userId: number, teamIds?: number[]) => {
.sort((eventTypeA, eventTypeB) => eventTypeB.position - eventTypeA.position),
}));
} else {
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
id: userId,
},
+1 -1
View File
@@ -157,7 +157,7 @@ export const handleSeatsEventTypeOnBooking = async (
} | null;
let seatAttendee: seatAttendee = null;
if (seatReferenceUid) {
seatAttendee = await prisma.bookingSeat.findFirst({
seatAttendee = await prisma.bookingSeat.findUnique({
where: {
referenceUid: seatReferenceUid,
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.3.13",
"version": "5.4.1",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+10 -1
View File
@@ -768,6 +768,13 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
* @deprecated use apiLogin instead
*/
login: async () => login({ ...(await self()), password: user.username }, store.page),
loginOnNewBrowser: async (browser: Browser) => {
const newContext = await browser.newContext();
const newPage = await newContext.newPage();
await login({ ...(await self()), password: user.username }, newPage);
// Don't forget to: newContext.close();
return [newContext, newPage] as const;
},
logout: async () => {
await page.goto("/auth/logout");
},
@@ -1074,9 +1081,11 @@ export async function apiLogin(
json: "true",
csrfToken,
};
return page.context().request.post("/api/auth/callback/credentials", {
const response = await page.context().request.post("/api/auth/callback/credentials", {
data,
});
expect(response.status()).toBe(200);
return response;
}
export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id">, slug: string, page: Page) {
+5 -3
View File
@@ -65,10 +65,12 @@ export async function moveUserToOrg({
);
}
const userWithSameUsernameInOrg = await prisma.user.findFirst({
const userWithSameUsernameInOrg = await prisma.user.findUnique({
where: {
username: targetOrgUsername,
organizationId: targetOrgId,
username_organizationId: {
username: targetOrgUsername,
organizationId: targetOrgId,
},
},
});
+7 -6
View File
@@ -18,7 +18,7 @@ test.describe("2FA Tests", async () => {
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
test("should allow a user to enable 2FA and login using 2FA", async ({ page, users }) => {
test("should allow a user to enable 2FA and login using 2FA", async ({ page, users, browser }) => {
// log in trail user
const user = await test.step("Enable 2FA", async () => {
const user = await users.create();
@@ -60,7 +60,7 @@ test.describe("2FA Tests", async () => {
});
await test.step("Login with 2FA enabled", async () => {
await user.login();
const [secondContext, secondPage] = await user.loginOnNewBrowser(browser);
const userWith2FaSecret = await prisma.user.findFirst({
where: {
id: user.id,
@@ -74,13 +74,14 @@ test.describe("2FA Tests", async () => {
process.env.CALENDSO_ENCRYPTION_KEY!
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await fillOtp({ page, secret: secret! });
await fillOtp({ page: secondPage, secret: secret! });
await Promise.all([
page.press('input[name="2fa6"]', "Enter"),
page.waitForResponse("**/api/auth/callback/credentials**"),
secondPage.press('input[name="2fa6"]', "Enter"),
secondPage.waitForResponse("**/api/auth/callback/credentials**"),
]);
const shellLocator = page.locator(`[data-testid=dashboard-shell]`);
const shellLocator = secondPage.locator(`[data-testid=dashboard-shell]`);
await expect(shellLocator).toBeVisible();
await secondContext.close();
});
});
+19 -15
View File
@@ -368,10 +368,11 @@ test.describe("Bookings", () => {
);
});
test("Round robin event type with confirmation required handles cancellation and rebooking correctly with the same details and slot", async ({
test("Round robin event type with confirmation required handles rejection and rebooking correctly with the same details and slot", async ({
page,
users,
orgs,
browser,
}) => {
const org = await orgs.create({
name: "TestOrg",
@@ -421,38 +422,41 @@ test.describe("Bookings", () => {
const bookingUid = page.url().split("/booking/")[1];
expect(bookingUid).not.toBeNull();
// Login as the host to cancel the booking
await page.goto("/auth/logout");
const allUsers = await users.get();
const allUsers = users.get();
const hostUser = allUsers.find((mate) => mate.name === firstHost);
if (!hostUser) throw new Error("Host not found");
await hostUser.apiLogin();
const [secondContext, secondPage] = await hostUser.apiLoginOnNewBrowser(browser);
// Cancel the booking
await page.goto(`/booking/${bookingUid}`);
await page.getByTestId("cancel").click();
await page.getByTestId("confirm_cancel").click();
await page.waitForResponse((response) => response.url().includes("/api/cancel"));
// Reject the booking
await secondPage.goto("/bookings/upcoming");
await secondPage.click('[data-testid="reject"]');
await submitAndWaitForResponse(secondPage, "/api/trpc/bookings/confirm?batch=1", {
action: () => secondPage.click('[data-testid="rejection-confirm"]'),
});
// Logout and go back to booking page
await page.goto("/auth/logout");
await page.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`);
await secondPage.goto("/auth/logout");
await secondPage.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`);
// Rebook with the same details
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
await selectFirstAvailableTimeSlotNextMonth(secondPage);
await bookTimeSlot(secondPage);
await expect(secondPage.getByTestId("success-page")).toBeVisible();
// Verify a new host is assigned
const newHost = await page.getByTestId("booking-host-name").textContent();
const newHost = await secondPage.getByTestId("booking-host-name").textContent();
expect(newHost).not.toBeNull();
expect(newHost).toBe(firstHost);
// Verify the booking was successful by checking the new booking UID
const newBookingUid = page.url().split("/booking/")[1];
const newBookingUid = secondPage.url().split("/booking/")[1];
expect(newBookingUid).not.toBeNull();
expect(newBookingUid).not.toBe(bookingUid);
await secondContext.close();
}
);
});
@@ -602,7 +602,7 @@ async function addWebhooks(webhooks: InputWebhook[]) {
async function addWorkflowsToDb(workflows: InputWorkflow[]) {
await Promise.all(
workflows.map(async (workflow) => {
const team = await prismock.team.findFirst({
const team = await prismock.team.findUnique({
where: {
id: workflow.teamId ?? 0,
},
+13 -3
View File
@@ -7321,7 +7321,7 @@
"post": {
"operationId": "BookingsController_2024_08_13_cancelBooking",
"summary": "Cancel a booking",
"description": ":bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.\n For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call\n like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.",
"description": ":bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.\n \n \nCancelling seated bookings:\n It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.\n \n1. As an attendee - provide :bookingUid in the request URL `/bookings/:bookingUid/cancel` and seatUid in the request body `{\"seatUid\": \"123-123-123\"}` . This will remove this particular attendance from the booking.\n \n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL `/bookings/:bookingUid/cancel` and cancellationReason in the request body `{\"cancellationReason\": \"Will travel\"}` and `Authorization: Bearer token` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.\n \n \nCancelling recurring seated bookings:\n For recurring seated bookings it is not possible to cancel all of them with 1 call\n like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.",
"parameters": [
{
"name": "cal-api-version",
@@ -7792,7 +7792,7 @@
"name": "eventUid",
"required": true,
"in": "path",
"description": "The Google Calendar event ID",
"description": "The Google Calendar event ID. You can retrieve this by getting booking references from the following endpoints:\n\n- For team events: https://cal.com/docs/api-reference/v2/orgs-teams-bookings/get-booking-references-for-a-booking\n\n- For user events: https://cal.com/docs/api-reference/v2/bookings/get-booking-references-for-a-booking",
"schema": {
"type": "string"
}
@@ -10205,6 +10205,16 @@
"type": "string"
}
},
{
"name": "bookingUidToReschedule",
"required": false,
"in": "query",
"description": "The unique identifier of the booking being rescheduled. When provided will ensure that the original booking time appears within the returned available slots when rescheduling.",
"example": "abc123def456",
"schema": {
"type": "string"
}
},
{
"name": "routingFormId",
"required": true,
@@ -23342,7 +23352,7 @@
"example": "Error message"
}
},
"required": ["id", "roomName", "startTs", "status", "duration", "shareToken", "downloadLink"]
"required": ["id", "roomName", "startTs", "status", "duration", "shareToken"]
},
"GetBookingRecordingsOutput": {
"type": "object",
@@ -31,7 +31,7 @@ export class PaymentService implements IAbstractPaymentService {
bookingId: Booking["id"]
) {
try {
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
select: {
uid: true,
title: true,
@@ -41,7 +41,7 @@ export class PaymentService implements IAbstractPaymentService {
bookerEmail: string
) {
try {
const booking: PaidBooking | null = await prisma.booking.findFirst({
const booking: PaidBooking | null = await prisma.booking.findUnique({
where: {
id: bookingId,
},
@@ -10,7 +10,7 @@ export class PaymentService implements IAbstractPaymentService {
bookingId: Booking["id"]
) {
try {
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
select: {
uid: true,
title: true,
@@ -71,7 +71,7 @@ export class PaymentService implements IAbstractPaymentService {
paymentOption: PaymentOption
): Promise<Payment> {
try {
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
select: {
uid: true,
title: true,
@@ -37,7 +37,7 @@ export class PaymentService implements IAbstractPaymentService {
bookingId: Booking["id"]
) {
try {
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
select: {
uid: true,
title: true,
@@ -113,7 +113,7 @@ export class PaymentService implements IAbstractPaymentService {
throw new Error("Payment option is not compatible with create method");
}
try {
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
select: {
uid: true,
title: true,
@@ -26,7 +26,7 @@ export const getServerSideProps = async function getServerSideProps(
const isEmbed = appPages[1] === "embed";
const form = await prisma.app_RoutingForms_Form.findFirst({
const form = await prisma.app_RoutingForms_Form.findUnique({
where: {
id: formId,
},
@@ -235,7 +235,7 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt
inputFields: InputFields
) {
for (const [, connectedForm] of Object.entries(serializedForm.connectedForms)) {
const connectedFormDb = await prisma.app_RoutingForms_Form.findFirst({
const connectedFormDb = await prisma.app_RoutingForms_Form.findUnique({
where: {
id: connectedForm.id,
},
@@ -28,7 +28,7 @@ const getInCompleteBookingSettingsHandler = async (options: GetIncompleteBooking
formId: input.formId,
},
}),
prisma.app_RoutingForms_Form.findFirst({
prisma.app_RoutingForms_Form.findUnique({
where: {
id: input.formId,
},
@@ -51,7 +51,7 @@ const getInCompleteBookingSettingsHandler = async (options: GetIncompleteBooking
if (teamId) {
// Need to get the credentials for the team and org
const orgQuery = await prisma.team.findFirst({
const orgQuery = await prisma.team.findUnique({
where: {
id: teamId,
},
@@ -1356,7 +1356,7 @@ export default class SalesforceCRMService implements CRM {
if (!organizerEmail) {
this.log.warn(`No organizer email found for bookingUid ${bookingUid}`);
}
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
where: { uid: bookingUid },
select: { createdAt: true },
});
@@ -29,7 +29,7 @@ export async function assignmentReasonHandler({
}
async function handleAccountLookupFieldReason(routingFormResponseId: number, teamMemberEmail: string) {
const routingFormResponse = await prisma.app_RoutingForms_FormResponse.findFirst({
const routingFormResponse = await prisma.app_RoutingForms_FormResponse.findUnique({
where: {
id: routingFormResponseId,
},
+1 -1
View File
@@ -67,7 +67,7 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
if (bookingToReschedule && bookingToReschedule.eventTypeId && bookingToReschedule.user) {
const userOwner = bookingToReschedule.user;
const event = await prisma.eventType.findFirstOrThrow({
const event = await prisma.eventType.findUniqueOrThrow({
select: {
title: true,
},
@@ -69,7 +69,7 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
if (bookingToReschedule && bookingToReschedule.eventTypeId && bookingToReschedule.user) {
const userOwner = bookingToReschedule.user;
const event = await prisma.eventType.findFirstOrThrow({
const event = await prisma.eventType.findUniqueOrThrow({
select: {
title: true,
schedulingType: true,
@@ -24,7 +24,6 @@ export const AttendeeRequestEmail = (props: React.ComponentProps<typeof Attendee
title: props.calEvent.title,
date,
})}
callToAction={null}
{...props}
/>
);
@@ -19,7 +19,8 @@ export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
to: toAddresses.join(","),
...getReplyToHeader(
this.calEvent,
this.calEvent.attendees.map(({ email }) => email)
this.calEvent.attendees.map(({ email }) => email),
true
),
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
@@ -26,7 +26,8 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
...getReplyToHeader(
this.calEvent,
this.calEvent.attendees.map(({ email }) => email)
this.calEvent.attendees.map(({ email }) => email),
true
),
subject: `${this.t("download_recording_subject", {
title: this.calEvent.title,
@@ -38,7 +38,8 @@ export default class OrganizerDailyVideoDownloadTranscriptEmail extends BaseEmai
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
...getReplyToHeader(
this.calEvent,
this.calEvent.attendees.map(({ email }) => email)
this.calEvent.attendees.map(({ email }) => email),
true
),
subject: `${this.t("download_transcript_email_subject", {
title: this.calEvent.title,
@@ -19,7 +19,8 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai
to: toAddresses.join(","),
...getReplyToHeader(
this.calEvent,
this.calEvent.attendees.map(({ email }) => email)
this.calEvent.attendees.map(({ email }) => email),
true
),
subject: `${this.t("location_changed_event_type_subject", {
eventType: this.calEvent.type,
@@ -24,7 +24,8 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
to: toAddresses.join(","),
...getReplyToHeader(
this.calEvent,
this.calEvent.attendees.map(({ email }) => email)
this.calEvent.attendees.map(({ email }) => email),
true
),
subject: `${this.t("awaiting_approval")}: ${this.calEvent.title}`,
html: await this.getHtmlRequestEmail(template, this.calEvent, this.calEvent.organizer),
@@ -13,7 +13,8 @@ export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail
to: toAddresses.join(","),
...getReplyToHeader(
this.calEvent,
this.calEvent.attendees.map(({ email }) => email)
this.calEvent.attendees.map(({ email }) => email),
true
),
subject: `${this.t("event_awaiting_approval_subject", {
title: this.calEvent.title,
@@ -19,7 +19,8 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
to: toAddresses.join(","),
...getReplyToHeader(
this.calEvent,
this.calEvent.attendees.map(({ email }) => email)
this.calEvent.attendees.map(({ email }) => email),
true
),
subject: `${this.calEvent.organizer.language.translate("event_type_has_been_rescheduled_on_time_date", {
title: this.calEvent.title,
@@ -52,7 +52,8 @@ export default class OrganizerScheduledEmail extends BaseEmail {
to: toAddresses.join(","),
...getReplyToHeader(
this.calEvent,
this.calEvent.attendees.map(({ email }) => email)
this.calEvent.attendees.map(({ email }) => email),
true
),
subject: `${this.newSeat ? `${this.t("new_attendee")}: ` : ""}${this.calEvent.title}`,
html: await this.getHtml(
@@ -438,7 +438,7 @@ export const getOptions = ({
encode: async ({ token, maxAge, secret }) => {
log.debug("jwt:encode", safeStringify({ token, maxAge }));
if (token?.sub && isNumber(token.sub)) {
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: { id: Number(token.sub) },
select: { metadata: true },
});
@@ -489,7 +489,7 @@ export const getOptions = ({
} as JWT;
}
const autoMergeIdentities = async () => {
const existingUser = await prisma.user.findFirst({
const existingUser = await prisma.user.findUnique({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
where: { email: token.email! },
select: {
@@ -855,7 +855,7 @@ export const getOptions = ({
// If the email address doesn't match, check if an account already exists
// with the new email address. If it does, for now we return an error. If
// not, update the email of their account and log them in.
const userWithNewEmail = await prisma.user.findFirst({
const userWithNewEmail = await prisma.user.findUnique({
where: { email: user.email },
});
@@ -875,12 +875,9 @@ export const getOptions = ({
// a new account. If an account already exists with the incoming email
// address return an error for now.
const existingUserWithEmail = await prisma.user.findFirst({
const existingUserWithEmail = await prisma.user.findUnique({
where: {
email: {
equals: user.email,
mode: "insensitive",
},
email: user.email,
},
include: {
password: true,
@@ -19,7 +19,7 @@ export default async function isAuthorized(token: string, requiredScopes: string
}
if (decodedToken.userId) {
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
id: decodedToken.userId,
},
@@ -35,7 +35,7 @@ export default async function isAuthorized(token: string, requiredScopes: string
}
if (decodedToken.teamId) {
const team = await prisma.team.findFirst({
const team = await prisma.team.findUnique({
where: {
id: decodedToken.teamId,
},
+5 -4
View File
@@ -83,11 +83,12 @@ const usernameCheck = async (usernameRaw: string) => {
const username = slugify(usernameRaw);
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
username,
// Simply remove it when we drop organizationId column
organizationId: null,
username_organizationId: {
username,
organizationId: null,
},
},
select: {
username: true,
@@ -36,7 +36,7 @@ export const prefillAvatar = async ({ email }: IPrefillAvatar) => {
if (!base64Image) return;
const avatar = await resizeBase64Image(base64Image);
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: { email: email },
});
+1 -1
View File
@@ -4,7 +4,7 @@ import { validateAndGetCorrectedUsernameInTeam } from "@calcom/lib/validateUsern
import { prisma } from "@calcom/prisma";
export async function findTokenByToken({ token }: { token: string }) {
const foundToken = await prisma.verificationToken.findFirst({
const foundToken = await prisma.verificationToken.findUnique({
where: {
token,
},
@@ -46,7 +46,7 @@ function getResponsesFromOldBooking(
}
async function getBooking(prisma: PrismaClient, uid: string, isSeatedEvent?: boolean) {
const rawBooking = await prisma.booking.findFirst({
const rawBooking = await prisma.booking.findUnique({
where: {
uid,
},
@@ -125,7 +125,7 @@ export default getBooking;
export const getBookingForReschedule = async (uid: string, userId?: number) => {
let rescheduleUid: string | null = null;
const theBooking = await prisma.booking.findFirst({
const theBooking = await prisma.booking.findUnique({
where: {
uid,
},
@@ -226,7 +226,7 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => {
* @returns booking with masked attendee emails
*/
export const getBookingForSeatedEvent = async (uid: string) => {
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
where: {
uid,
},
@@ -253,7 +253,7 @@ export const getBookingForSeatedEvent = async (uid: string) => {
if (!booking || booking.eventTypeId === null) return null;
// Validate booking event type has seats enabled
const eventType = await prisma.eventType.findFirst({
const eventType = await prisma.eventType.findUnique({
where: {
id: booking.eventTypeId,
},
@@ -1,7 +1,7 @@
import prisma from "@calcom/prisma";
const getUserBooking = async (uid: string) => {
const bookingInfo = await prisma.booking.findFirst({
const bookingInfo = await prisma.booking.findUnique({
where: {
uid: uid,
},
@@ -154,7 +154,7 @@ async function handler(input: CancelBookingInput) {
const webhooks = await getWebhooks(subscriberOptions);
const organizer = await prisma.user.findFirstOrThrow({
const organizer = await prisma.user.findUniqueOrThrow({
where: {
id: bookingToDelete.userId,
},
@@ -500,7 +500,7 @@ export async function handleConfirmation(args: {
orgId,
oAuthClientId: platformClientParams?.platformClientId,
});
const bookingWithPayment = await prisma.booking.findFirst({
const bookingWithPayment = await prisma.booking.findUnique({
where: {
id: bookingId,
},
@@ -136,7 +136,7 @@ const handleDeleteCredential = async ({
credential.app?.categories.includes(AppCategories.calendar) &&
eventType.destinationCalendar?.credential?.appId === credential.appId
) {
const destinationCalendar = await prisma.destinationCalendar.findFirst({
const destinationCalendar = await prisma.destinationCalendar.findUnique({
where: {
id: eventType.destinationCalendar?.id,
},
@@ -29,7 +29,7 @@ async function checkLicense(
let licenseKey = process.env.CALCOM_LICENSE_KEY;
if (!licenseKey) {
/** We try to check on DB only if env is undefined */
const deployment = await prisma.deployment.findFirst({ where: { id: 1 } });
const deployment = await prisma.deployment.findUnique({ where: { id: 1 } });
licenseKey = deployment?.licenseKey ?? undefined;
}
if (!licenseKey) return false;
@@ -33,7 +33,7 @@ async function syncCustomAttributesToUser({
};
directoryId: string;
}) {
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
email: userEmail,
},
@@ -62,7 +62,7 @@ const handleUserEvents = async (event: DirectorySyncEvent, organizationId: numbe
const eventData = event.data as User;
const userEmail = eventData.email;
// Check if user exists in DB
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
email: userEmail,
},
@@ -25,10 +25,12 @@ export const getServerSideProps = async ({ req }: GetServerSidePropsContext) =>
}
// Check if logged in user has OWNER/ADMIN role in organization
const membership = await prisma.membership.findFirst({
const membership = await prisma.membership.findUnique({
where: {
userId: session?.user.id,
teamId: session?.user.profile.organizationId,
userId_teamId: {
userId: session?.user.id,
teamId: session?.user.profile.organizationId,
},
},
select: {
role: true,
@@ -20,7 +20,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const { uid } = querySchema.parse(context.query);
const { currentOrgDomain } = orgDomainConfig(context.req);
const rawPayment = await prisma.payment.findFirst({
const rawPayment = await prisma.payment.findUnique({
where: {
uid,
},
@@ -35,7 +35,7 @@ export default class AssignmentReasonRecorder {
teamId: number;
}) {
// Get the routing form data
const routingFormResponse = await prisma.app_RoutingForms_FormResponse.findFirst({
const routingFormResponse = await prisma.app_RoutingForms_FormResponse.findUnique({
where: {
id: routingFormResponseId,
},
@@ -186,7 +186,7 @@ export default class AssignmentReasonRecorder {
reassignReason?: string;
reassignmentType: RRReassignmentType;
}) {
const reassignedBy = await prisma.user.findFirst({
const reassignedBy = await prisma.user.findUnique({
where: {
id: reassignById,
},
@@ -239,7 +239,7 @@ export const getMessageBody = async (referenceId: string) => {
async function isLockedForSMSSending(userId?: number | null, teamId?: number | null) {
if (teamId) {
const team = await prisma.team.findFirst({
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
@@ -269,7 +269,7 @@ async function isLockedForSMSSending(userId?: number | null, teamId?: number | n
return true;
}
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
id: userId,
},
@@ -119,7 +119,7 @@ const processWorkflowStep = async (
const limitGuestsDate = new Date("2025-01-13");
if (workflow.userId) {
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
id: workflow.userId,
},
@@ -339,58 +339,54 @@ const Locations: React.FC<LocationsProps> = ({
)}
</div>
{isCalVideo && (
{isCalVideo && !isPlatform && (
<div className="bg-muted mt-2 space-y-2 rounded-lg p-4">
<div className="w-full">
<div className="flex flex-col gap-2">
{!isPlatform && (
<Controller
name="calVideoSettings.disableRecordingForGuests"
defaultValue={!!eventType.calVideoSettings?.disableRecordingForGuests}
render={({ field: { onChange, value } }) => {
return (
<SettingsToggle
title={t("disable_recording_for_guests")}
labelClassName="text-sm"
checked={value}
onCheckedChange={onChange}
/>
);
}}
/>
)}
{!isPlatform && (
<Controller
name="calVideoSettings.disableRecordingForOrganizer"
defaultValue={!!eventType.calVideoSettings?.disableRecordingForOrganizer}
render={({ field: { onChange, value } }) => {
return (
<SettingsToggle
title={t("disable_recording_for_organizer")}
labelClassName="text-sm"
checked={value}
onCheckedChange={onChange}
/>
);
}}
/>
)}
{!isPlatform && (
<Controller
name="calVideoSettings.enableAutomaticTranscription"
defaultValue={!!eventType.calVideoSettings?.enableAutomaticTranscription}
render={({ field: { onChange, value } }) => {
return (
<SettingsToggle
title={t("enable_automatic_transcription")}
labelClassName="text-sm"
checked={value}
onCheckedChange={onChange}
/>
);
}}
/>
)}
<Controller
name="calVideoSettings.disableRecordingForGuests"
defaultValue={!!eventType.calVideoSettings?.disableRecordingForGuests}
render={({ field: { onChange, value } }) => {
return (
<SettingsToggle
title={t("disable_recording_for_guests")}
labelClassName="text-sm"
checked={value}
onCheckedChange={onChange}
/>
);
}}
/>
<Controller
name="calVideoSettings.disableRecordingForOrganizer"
defaultValue={!!eventType.calVideoSettings?.disableRecordingForOrganizer}
render={({ field: { onChange, value } }) => {
return (
<SettingsToggle
title={t("disable_recording_for_organizer")}
labelClassName="text-sm"
checked={value}
onCheckedChange={onChange}
/>
);
}}
/>
<Controller
name="calVideoSettings.enableAutomaticTranscription"
defaultValue={!!eventType.calVideoSettings?.enableAutomaticTranscription}
render={({ field: { onChange, value } }) => {
return (
<SettingsToggle
title={t("enable_automatic_transcription")}
labelClassName="text-sm"
checked={value}
onCheckedChange={onChange}
/>
);
}}
/>
<TextField
label={t("enter_redirect_url_on_exit_description")}
@@ -910,6 +910,8 @@ export const EventAdvancedTab = ({
disabled={seatsLocked.disabled}
//For old events if value > MAX_SEATS_PER_TIME_SLOT
value={value > MAX_SEATS_PER_TIME_SLOT ? MAX_SEATS_PER_TIME_SLOT : value ?? 1}
step={1}
placeholder="1"
min={1}
max={MAX_SEATS_PER_TIME_SLOT}
containerClassName={classNames(
@@ -921,8 +923,7 @@ export const EventAdvancedTab = ({
labelClassName={customClassNames?.seatsOptions?.seatsInput?.label}
addOnSuffix={t("seats")}
onChange={(e) => {
let enteredValue = Number(e.target.value);
if (enteredValue < 1) enteredValue = 1;
const enteredValue = parseInt(e.target.value);
onChange(Math.min(enteredValue, MAX_SEATS_PER_TIME_SLOT));
}}
data-testid="seats-per-time-slot"
@@ -170,10 +170,12 @@ export class FeaturesRepository implements IFeaturesRepository {
async checkIfTeamHasFeature(teamId: number, featureId: keyof AppFlags): Promise<boolean> {
try {
// Early return if team has feature directly assigned
const teamHasFeature = await db.teamFeatures.findFirst({
const teamHasFeature = await db.teamFeatures.findUnique({
where: {
teamId,
featureId,
teamId_featureId: {
teamId,
featureId,
},
},
});
if (teamHasFeature) return true;
+10 -6
View File
@@ -625,10 +625,12 @@ class EventsInsights {
sessionUserId: number;
teamId: number;
}) => {
const isOwnerAdminOfTeam = await prisma.membership.findFirst({
const isOwnerAdminOfTeam = await prisma.membership.findUnique({
where: {
userId: sessionUserId,
teamId,
userId_teamId: {
userId: sessionUserId,
teamId,
},
accepted: true,
role: {
in: ["OWNER", "ADMIN"],
@@ -659,10 +661,12 @@ class EventsInsights {
return false;
}
const isOwnerAdminOfParentTeam = await prisma.membership.findFirst({
const isOwnerAdminOfParentTeam = await prisma.membership.findUnique({
where: {
userId: sessionUserId,
teamId: team.parentId,
userId_teamId: {
userId: sessionUserId,
teamId: team.parentId,
},
accepted: true,
role: {
in: ["OWNER", "ADMIN"],
@@ -16,7 +16,7 @@ export default async function handleSendingAttendeeNoShowDataToApps(
attendees: NoShowAttendees
) {
// Get event type metadata
const eventTypeQuery = await prisma.booking.findFirst({
const eventTypeQuery = await prisma.booking.findUnique({
where: {
uid: bookingUid,
},
@@ -62,7 +62,7 @@ async function handleCRMNoShow(
) {
// Handle checking if no she in CrmService
const credential = await prisma.credential.findFirst({
const credential = await prisma.credential.findUnique({
where: {
id: appData.credentialId,
},
+21
View File
@@ -3,6 +3,7 @@ import { type Prisma } from "@prisma/client";
import db from "@calcom/prisma";
import { type TaskTypes } from "./tasker";
import { scanWorkflowBodySchema } from "./tasks/scanWorkflowBody";
const whereSucceeded: Prisma.TaskWhereInput = {
succeededAt: { not: null },
@@ -187,4 +188,24 @@ export class Task {
// },
// });
}
static async hasNewerScanTaskForStepId(workflowStepId: number, createdAt: string) {
const tasks = await db.$queryRaw<{ payload: string }[]>`
SELECT "payload"
FROM "Task"
WHERE "type" = 'scanWorkflowBody'
AND "succeededAt" IS NULL
AND (payload::jsonb ->> 'workflowStepId')::int = ${workflowStepId}
`;
return tasks.some((task) => {
try {
const parsed = scanWorkflowBodySchema.parse(JSON.parse(task.payload));
if (!parsed.createdAt) return false;
return new Date(parsed.createdAt) > new Date(createdAt);
} catch {
return false;
}
});
}
}
@@ -5,7 +5,7 @@ import prisma from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
const buildCalendarEvent: (bookingUid: string) => Promise<CalendarEvent> = async (bookingUid: string) => {
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
where: {
uid: bookingUid,
},
@@ -2,6 +2,7 @@ import z from "zod";
import { getTemplateBodyForAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import compareReminderBodyToTemplate from "@calcom/features/ee/workflows/lib/compareReminderBodyToTemplate";
import { Task } from "@calcom/features/tasker/repository";
import { lockUser, LockReason } from "@calcom/lib/autoLock";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
@@ -11,18 +12,39 @@ import { scheduleWorkflowNotifications } from "@calcom/trpc/server/routers/viewe
export const scanWorkflowBodySchema = z.object({
userId: z.number(),
workflowStepIds: z.array(z.number()),
// deprecated: use workflowStepId instead
workflowStepIds: z.array(z.number()).optional(),
workflowStepId: z.number().optional(),
createdAt: z.string().optional(),
});
const log = logger.getSubLogger({ prefix: ["[tasker] scanWorkflowBody"] });
export async function scanWorkflowBody(payload: string) {
const { workflowStepIds, userId } = scanWorkflowBodySchema.parse(JSON.parse(payload));
const { workflowStepIds, userId, createdAt, workflowStepId } = scanWorkflowBodySchema.parse(
JSON.parse(payload)
);
const stepIdsToScan: number[] = workflowStepIds ? workflowStepIds : [];
if (workflowStepId && !workflowStepIds) {
if (createdAt) {
const hasNewerTask = await Task.hasNewerScanTaskForStepId(workflowStepId, createdAt);
if (hasNewerTask) return;
stepIdsToScan.push(workflowStepId);
} else {
stepIdsToScan.push(workflowStepId);
}
}
if (stepIdsToScan.length === 0) return;
const workflowSteps = await prisma.workflowStep.findMany({
where: {
id: {
in: workflowStepIds,
in: stepIdsToScan,
},
},
include: {
@@ -116,7 +138,7 @@ export async function scanWorkflowBody(payload: string) {
await prisma.workflowStep.updateMany({
where: {
id: {
in: workflowStepIds,
in: stepIdsToScan,
},
},
data: {
@@ -130,7 +152,7 @@ export async function scanWorkflowBody(payload: string) {
steps: {
some: {
id: {
in: workflowStepIds,
in: stepIdsToScan,
},
},
},
@@ -142,7 +164,7 @@ export async function scanWorkflowBody(payload: string) {
});
if (!workflow) {
log.warn(`Workflow with steps ${workflowStepIds} not found`);
log.warn(`Workflow with steps ${stepIdsToScan} not found`);
return;
}
@@ -30,6 +30,7 @@ import {
} from "@calcom/lib/csvUtils";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
import type { RouterOutputs } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
@@ -109,8 +110,18 @@ function reducer(state: UserTableState, action: UserTableAction): UserTableState
export type UserListTableProps = {
org: RouterOutputs["viewer"]["organizations"]["listCurrent"];
teams: RouterOutputs["viewer"]["organizations"]["getTeams"];
facetedTeamValues?: RouterOutputs["viewer"]["organizations"]["getFacetedValues"];
attributes?: RouterOutputs["viewer"]["attributes"]["list"];
facetedTeamValues?: {
roles: MembershipRole[];
teams: RouterOutputs["viewer"]["organizations"]["getTeams"];
attributes: {
id: string;
name: string;
options: {
value: string;
}[];
}[];
};
};
export function UserListTable(props: UserListTableProps) {
+2 -2
View File
@@ -186,7 +186,7 @@ export default class EventManager {
// Fallback to cal video if no location is set
if (!evt.location) {
// See if cal video is enabled & has keys
const calVideo = await prisma.app.findFirst({
const calVideo = await prisma.app.findUnique({
where: {
slug: "daily-video",
},
@@ -461,7 +461,7 @@ export default class EventManager {
}
// Get details of existing booking.
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
where: {
uid: rescheduleUid,
},
@@ -14,7 +14,7 @@ export const bulkUpdateTeamEventsToDefaultLocation = async ({
teamId: number;
prisma: PrismaClient;
}) => {
const team = await prisma.team.findFirst({
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { metadata: true },
});
+1 -1
View File
@@ -45,7 +45,7 @@ function getResponsesFromOldBooking(
}
async function getBooking(prisma: PrismaClient, uid: string) {
const rawBooking = await prisma.booking.findFirst({
const rawBooking = await prisma.booking.findUnique({
where: {
uid,
},
+13 -2
View File
@@ -3,6 +3,17 @@ import type { CalendarEvent } from "@calcom/types/Calendar";
/**
* Returns the reply-to email address to use, with fallback to organizer's email
*/
export const getReplyToEmail = (calEvent: Pick<CalendarEvent, "customReplyToEmail" | "organizer">) => {
return calEvent.customReplyToEmail || calEvent.organizer.email;
export const getReplyToEmail = (
calEvent: Pick<CalendarEvent, "customReplyToEmail" | "organizer">,
excludeOrganizerEmail?: boolean
) => {
if (calEvent.customReplyToEmail) {
return calEvent.customReplyToEmail;
}
if (excludeOrganizerEmail) {
return null;
}
return calEvent.organizer.email;
};
+24 -7
View File
@@ -2,15 +2,32 @@ import type { CalendarEvent } from "@calcom/types/Calendar";
import { getReplyToEmail } from "./getReplyToEmail";
export function getReplyToHeader(calEvent: CalendarEvent, additionalEmails?: string | string[]) {
export function getReplyToHeader(
calEvent: CalendarEvent,
additionalEmails?: string | string[],
excludeOrganizerEmail?: boolean
) {
if (calEvent.hideOrganizerEmail) return {};
const replyToEmail = getReplyToEmail(calEvent);
const replyTo = additionalEmails
? Array.isArray(additionalEmails)
? [...additionalEmails, replyToEmail]
: [additionalEmails, replyToEmail]
: replyToEmail;
const replyToEmail = getReplyToEmail(calEvent, excludeOrganizerEmail);
const emailArray: string[] = [];
if (additionalEmails) {
if (Array.isArray(additionalEmails)) {
emailArray.push(...additionalEmails);
} else {
emailArray.push(additionalEmails);
}
}
if (replyToEmail) {
emailArray.push(replyToEmail);
}
if (emailArray.length === 0) {
return {};
}
const replyTo = emailArray.length === 1 ? emailArray[0] : emailArray;
return { replyTo };
}
+1 -1
View File
@@ -15,7 +15,7 @@ export async function getTeamIdFromEventType({
// If it's a managed event we need to find the teamId for it from the parent
if (eventType?.parentId) {
const managedEvent = await prisma.eventType.findFirst({
const managedEvent = await prisma.eventType.findUnique({
where: {
id: eventType.parentId,
},
+1 -1
View File
@@ -6,7 +6,7 @@ export const getBrand = async (orgId: number | null) => {
if (!orgId) {
return null;
}
const org = await prisma.team.findFirst({
const org = await prisma.team.findUnique({
where: {
id: orgId,
},
@@ -59,7 +59,7 @@ async function getAttributeRoutingConfig(
}
const { routingFormResponseId, eventTypeId } = data;
const routingFormResponseQuery = await prisma.app_RoutingForms_FormResponse.findFirst({
const routingFormResponseQuery = await prisma.app_RoutingForms_FormResponse.findUnique({
where: {
id: routingFormResponseId,
},
@@ -26,10 +26,12 @@ export async function isOrganisationOwner(userId: number, orgId: number) {
}
export async function isOrganisationMember(userId: number, orgId: number) {
return !!(await prisma.membership.findFirst({
return !!(await prisma.membership.findUnique({
where: {
userId,
teamId: orgId,
userId_teamId: {
userId,
teamId: orgId,
},
},
}));
}
+2 -2
View File
@@ -123,7 +123,7 @@ export class BookingRepository {
/** Determines if the user is the organizer, team admin, or org admin that the booking was created under */
static async doesUserIdHaveAccessToBooking({ userId, bookingId }: { userId: number; bookingId: number }) {
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findUnique({
where: {
id: bookingId,
},
@@ -165,7 +165,7 @@ export class BookingRepository {
}
static async findReschedulerByUid({ uid }: { uid: string }) {
return await prisma.booking.findFirst({
return await prisma.booking.findUnique({
where: {
uid,
},
+2 -2
View File
@@ -45,7 +45,7 @@ export class CredentialRepository {
* Doesn't retrieve key field as that has credentials
*/
static async findFirstByIdWithUser({ id }: { id: number }) {
const credential = await prisma.credential.findFirst({ where: { id }, select: safeCredentialSelect });
const credential = await prisma.credential.findUnique({ where: { id }, select: safeCredentialSelect });
return buildNonDelegationCredential(credential);
}
@@ -53,7 +53,7 @@ export class CredentialRepository {
* Includes 'key' field which is sensitive data.
*/
static async findFirstByIdWithKeyAndUser({ id }: { id: number }) {
const credential = await prisma.credential.findFirst({
const credential = await prisma.credential.findUnique({
where: { id },
select: { ...safeCredentialSelect, key: true },
});
+5 -3
View File
@@ -233,10 +233,12 @@ export class MembershipRepository {
}
static async findFirstByUserIdAndTeamId({ userId, teamId }: { userId: number; teamId: number }) {
return await prisma.membership.findFirst({
return await prisma.membership.findUnique({
where: {
userId,
teamId,
userId_teamId: {
userId,
teamId,
},
},
});
}
@@ -247,11 +247,11 @@ export class OrganizationRepository {
}
static async findCurrentOrg({ userId, orgId }: { userId: number; orgId: number }) {
const membership = await prisma.membership.findFirst({
const membership = await prisma.membership.findUnique({
where: {
userId,
team: {
id: orgId,
userId_teamId: {
userId,
teamId: orgId,
},
},
include: {
+10 -6
View File
@@ -364,10 +364,12 @@ export class ProfileRepository {
if (!organizationId) {
return null;
}
const profile = await prisma.profile.findFirst({
const profile = await prisma.profile.findUnique({
where: {
userId,
organizationId,
userId_organizationId: {
userId,
organizationId,
},
},
include: {
organization: {
@@ -401,10 +403,12 @@ export class ProfileRepository {
organizationId: number;
username: string;
}) {
const profile = await prisma.profile.findFirst({
const profile = await prisma.profile.findUnique({
where: {
username,
organizationId,
username_organizationId: {
username,
organizationId,
},
},
include: {
organization: {
+7 -5
View File
@@ -636,7 +636,7 @@ export class UserRepository {
return user;
}
static async getUserAdminTeams(userId: number) {
return prisma.user.findFirst({
return prisma.user.findUnique({
where: {
id: userId,
},
@@ -713,10 +713,12 @@ export class UserRepository {
return !!teams.length;
}
static async isAdminOrOwnerOfTeam({ userId, teamId }: { userId: number; teamId: number }) {
const isAdminOrOwnerOfTeam = await prisma.membership.findFirst({
const isAdminOrOwnerOfTeam = await prisma.membership.findUnique({
where: {
userId,
teamId,
userId_teamId: {
userId,
teamId,
},
role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
accepted: true,
},
@@ -876,7 +878,7 @@ export class UserRepository {
}
static async getUserStats({ userId }: { userId: number }) {
const user = await prisma.user.findFirst({
const user = await prisma.user.findUnique({
where: {
id: userId,
},
+1 -1
View File
@@ -6,7 +6,7 @@ import { EventTypeMetaDataSchema, EventTypeAppMetadataSchema } from "@calcom/pri
export class EventTypeService {
static async getEventTypeAppDataFromId(eventTypeId: number, appSlug: keyof typeof appDataSchemas) {
const eventType = await prisma.eventType.findFirst({
const eventType = await prisma.eventType.findUnique({
where: {
id: eventTypeId,
},
+1 -1
View File
@@ -5,7 +5,7 @@ import { WorkflowRepository } from "../repository/workflow";
// TODO (Sean): Move most of the logic migrated in 16861 to this service
export class WorkflowService {
static async deleteWorkflowRemindersOfRemovedTeam(teamId: number) {
const team = await prisma.team.findFirst({
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
+1 -1
View File
@@ -85,7 +85,7 @@ export const validateAndGetCorrectedUsernameInTeam = async (
isSignup: boolean
) => {
try {
const team = await prisma.team.findFirst({
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
+1 -1
View File
@@ -92,7 +92,7 @@ const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEv
};
try {
// Check to see if video app is enabled
const enabledApp = await prisma.app.findFirst({
const enabledApp = await prisma.app.findUnique({
where: {
slug: credential.appId,
},
+1 -1
View File
@@ -4,7 +4,7 @@
"type": "module",
"description": "Customizable UI components to integrate scheduling into your product.",
"authors": "Cal.com, Inc.",
"version": "1.0.52",
"version": "1.0.105",
"scripts": {
"dev": "yarn vite build --watch & npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify --watch",
"build": "NODE_OPTIONS='--max_old_space_size=16384' rm -rf dist && yarn vite build && npx tailwindcss -i ./globals.css -o ./globals.min.css --postcss --minify && mkdir ./dist/packages/prisma-client && cp -rf ../../../node_modules/.prisma/client/*.d.ts ./dist/packages/prisma-client",
@@ -0,0 +1,5 @@
export {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
getAllTranscriptsAccessLinkFromRoomName,
} from "@calcom/lib/videoClient";
-6
View File
@@ -74,12 +74,6 @@ export { getTranslation };
export { roundRobinReassignment } from "@calcom/features/ee/round-robin/roundRobinReassignment";
export { roundRobinManualReassignment } from "@calcom/features/ee/round-robin/roundRobinManualReassignment";
export {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
getAllTranscriptsAccessLinkFromRoomName,
} from "@calcom/lib/videoClient";
export { ErrorCode } from "@calcom/lib/errorCodes";
export { validateCustomEventName } from "@calcom/lib/event";
+8
View File
@@ -63,6 +63,11 @@
"import": "./dist/slots.js",
"require": "./dist/slots.cjs",
"types": "./dist/slots.d.ts"
},
"./conferencing": {
"import": "./dist/conferencing.js",
"require": "./dist/conferencing.cjs",
"types": "./dist/conferencing.d.ts"
}
},
"typesVersions": {
@@ -84,6 +89,9 @@
],
"workflows": [
"dist/workflows.d.ts"
],
"conferencing": [
"dist/conferencing.d.ts"
]
}
}
@@ -24,6 +24,7 @@ export default defineConfig({
"app-store": resolve(__dirname, "./app-store.ts"),
workflows: resolve(__dirname, "./workflows.ts"),
slots: resolve(__dirname, "./slots.ts"),
conferencing: resolve(__dirname, "./conferencing.ts"),
},
name: "calcom-lib",
fileName: "calcom-lib",

Some files were not shown because too many files have changed in this diff Show More