Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cfc63e6e3 | ||
|
|
4f66f3d2c6 | ||
|
|
ca3e7bd97d | ||
|
|
cafa3f9cf3 | ||
|
|
e04faffcbb | ||
|
|
6f1556a2fd | ||
|
|
d72a954896 | ||
|
|
abeb20373c | ||
|
|
422ef2f5b8 | ||
|
|
ec920eb2a8 | ||
|
|
795eecf300 | ||
|
|
8ae3d6d55b | ||
|
|
85070574da | ||
|
|
d1c2872f6e | ||
|
|
42d78f8099 | ||
|
|
f0d7a757d4 | ||
|
|
0b2f09ab7d | ||
|
|
355aaa7010 | ||
|
|
c27b73aa9a | ||
|
|
a622c7b624 | ||
|
|
19bd3e5849 | ||
|
|
7827028e0a | ||
|
|
ebc3ced687 | ||
|
|
b56824e626 | ||
|
|
460e511459 | ||
|
|
b4fd6f3442 | ||
|
|
0a7a0e3687 | ||
|
|
595fffbea0 | ||
|
|
218c9152da | ||
|
|
77fe23da49 | ||
|
|
a583db3fd4 | ||
|
|
cb1f803d0a | ||
|
|
479d6b5961 | ||
|
|
4ab3ec73c0 | ||
|
|
8dda418127 | ||
|
|
a7f24e777b | ||
|
|
f78fca594a | ||
|
|
e6a7ae7da3 | ||
|
|
ad5d92dff0 | ||
|
|
8bf46ab8b6 | ||
|
|
34b0753f8d | ||
|
|
0bd7227150 | ||
|
|
4d192ee328 | ||
|
|
63013d402e | ||
|
|
6b3ccbc61a | ||
|
|
2af5cba0d7 | ||
|
|
ae7bbaf414 | ||
|
|
4df4383e1e | ||
|
|
a0706d9b51 | ||
|
|
2eb385af48 | ||
|
|
a0f986f949 | ||
|
|
b0094bad2d | ||
|
|
c732c06835 | ||
|
|
c1334475c5 | ||
|
|
b35dc43011 | ||
|
|
c7dc3ecc49 | ||
|
|
a613c975d3 | ||
|
|
5fad7c9478 | ||
|
|
318470d866 | ||
|
|
a0ce42e375 | ||
|
|
5293a5cc54 | ||
|
|
42aefc9032 | ||
|
|
14acf4e9bc | ||
|
|
80815bd8a5 | ||
|
|
9ed8face03 | ||
|
|
18a107d081 | ||
|
|
04d7537e2f | ||
|
|
423b17a819 | ||
|
|
0466b7ebd6 | ||
|
|
09329707c1 | ||
|
|
1773aba378 | ||
|
|
f8be2b3833 | ||
|
|
79d488faca | ||
|
|
da2c57c302 | ||
|
|
abaf03adde | ||
|
|
5b353c3d0c | ||
|
|
1f4cda31ac | ||
|
|
73d1a6f468 | ||
|
|
f72748b6f2 | ||
|
|
d12fc78fea | ||
|
|
3d26332443 | ||
|
|
bdb490c894 | ||
|
|
01dbc686b9 | ||
|
|
376961f30d | ||
|
|
18a0eb9736 | ||
|
|
84fb4f68b2 | ||
|
|
b5ab6fe3fa | ||
|
|
645985c783 | ||
|
|
a84513eda5 | ||
|
|
b97331aeb3 | ||
|
|
851d96423b | ||
|
|
ef830f11ec | ||
|
|
1ab4ec9c22 | ||
|
|
656a5bf423 | ||
|
|
8a6450e854 | ||
|
|
51841e5e9c | ||
|
|
59dfcaa2fa | ||
|
|
0d24706380 | ||
|
|
87b6e0b49c | ||
|
|
4244ca3117 | ||
|
|
8d0f0d4899 | ||
|
|
6463983d55 | ||
|
|
c8661563c8 | ||
|
|
2262d56491 | ||
|
|
6b1c747c0e | ||
|
|
04036c32a7 | ||
|
|
49070b96ae | ||
|
|
237fea0a41 | ||
|
|
a7e7561efa | ||
|
|
e366c10521 | ||
|
|
2ace1d1017 | ||
|
|
fbe31ad82d | ||
|
|
ee3c08918a | ||
|
|
b6d6b7cdb4 | ||
|
|
1b82552e76 | ||
|
|
13a90aba30 | ||
|
|
de0de64a16 |
+6
-4
@@ -107,6 +107,12 @@ NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
NEXT_PUBLIC_POSTHOG_HOST=
|
||||
|
||||
# plain.com config
|
||||
|
||||
PLAIN_API_KEY=
|
||||
PLAIN_API_URL=https://api.plain.com/v1
|
||||
PLAIN_HMAC_SECRET_KEY=
|
||||
|
||||
# Zendesk Config
|
||||
NEXT_PUBLIC_ZENDESK_KEY=
|
||||
|
||||
@@ -355,13 +361,9 @@ APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
APP_ROUTER_APPS_CATEGORIES_ENABLED=0
|
||||
# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
|
||||
APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_BOOKING_ENABLED=0
|
||||
APP_ROUTER_BOOKINGS_STATUS_ENABLED=0
|
||||
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
|
||||
APP_ROUTER_APPS_ENABLED=0
|
||||
APP_ROUTER_TEAM_ENABLED=0
|
||||
APP_ROUTER_TEAMS_ENABLED=0
|
||||
APP_ROUTER_AVAILABILITY_ENABLED=0
|
||||
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGIN_ENABLED=0
|
||||
APP_ROUTER_AUTH_LOGOUT_ENABLED=0
|
||||
|
||||
@@ -17,7 +17,6 @@ env:
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
@@ -51,7 +50,8 @@ env:
|
||||
jobs:
|
||||
analyze:
|
||||
if: always()
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
@@ -60,6 +60,7 @@ jobs:
|
||||
- name: Analyze bundle
|
||||
run: |
|
||||
cd apps/web
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||
|
||||
- name: Upload bundle
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
printf '{
|
||||
"pr_number": ${{ github.event.pull_request.number }}
|
||||
}' >> context.json
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: context.json
|
||||
path: ./
|
||||
|
||||
Vendored
Vendored
Vendored
Vendored
+8
@@ -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"
|
||||
Vendored
+8
@@ -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"
|
||||
Vendored
+8
@@ -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"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
+8
@@ -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"
|
||||
Vendored
+8
@@ -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"
|
||||
Vendored
+8
@@ -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"
|
||||
@@ -61,6 +61,8 @@ describe("rateLimitApiKey middleware", () => {
|
||||
reset: Date.now(),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(checkRateLimitAndThrowError as any).mockImplementationOnce(({ onRateLimiterResponse }) => {
|
||||
onRateLimiterResponse(rateLimiterResponse);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { checkIfEmailInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
import { checkIfEmailIsBlockedInWatchlistController } from "@calcom/features/watchlist/operations/check-if-email-in-watchlist.controller";
|
||||
|
||||
export async function isLockedOrBlocked(req: NextApiRequest) {
|
||||
const user = req.user;
|
||||
if (!user?.email) return false;
|
||||
return user.locked || (await checkIfEmailInWatchlistController(user.email));
|
||||
return user.locked || (await checkIfEmailIsBlockedInWatchlistController(user.email));
|
||||
}
|
||||
|
||||
@@ -9,11 +9,25 @@ export const schemaSelectedCalendarBaseBodyParams = SelectedCalendar;
|
||||
|
||||
export const schemaSelectedCalendarPublic = SelectedCalendar.omit({});
|
||||
|
||||
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams.partial({
|
||||
userId: true,
|
||||
});
|
||||
export const schemaSelectedCalendarBodyParams = schemaSelectedCalendarBaseBodyParams
|
||||
.partial({
|
||||
userId: true,
|
||||
})
|
||||
.omit({
|
||||
// id will be set by the database
|
||||
id: true,
|
||||
// No eventTypeId support in API v1
|
||||
eventTypeId: true,
|
||||
});
|
||||
|
||||
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams.partial();
|
||||
export const schemaSelectedCalendarUpdateBodyParams = schemaSelectedCalendarBaseBodyParams
|
||||
.omit({
|
||||
// id is decided by DB
|
||||
id: true,
|
||||
// No eventTypeId support in API v1
|
||||
eventTypeId: true,
|
||||
})
|
||||
.partial();
|
||||
|
||||
export const selectedCalendarIdSchema = schemaQueryIdAsString.transform((v, ctx) => {
|
||||
/** We can assume the first part is the userId since it's an integer */
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"start": "PORT=3003 next start",
|
||||
"docker-start-api": "PORT=80 next start",
|
||||
"type-check": "tsc --pretty --noEmit"
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/tsconfig": "*",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import type { UserWithCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { getConnectedDestinationCalendars } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/lib/getConnectedDestinationCalendars";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { extractUserIdsFromQuery } from "~/lib/utils/extractUserIdsFromQuery";
|
||||
@@ -104,12 +105,8 @@ async function getHandler(req: NextApiRequest) {
|
||||
|
||||
const userIds = req.query.userId ? extractUserIdsFromQuery(req) : [userId];
|
||||
|
||||
const usersWithCalendars = await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
include: {
|
||||
selectedCalendars: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
const usersWithCalendars = await UserRepository.findManyByIdsIncludeDestinationAndSelectedCalendars({
|
||||
ids: userIds,
|
||||
});
|
||||
|
||||
return await getConnectedCalendars(usersWithCalendars);
|
||||
@@ -117,11 +114,12 @@ async function getHandler(req: NextApiRequest) {
|
||||
|
||||
async function getConnectedCalendars(users: UserWithCalendars[]) {
|
||||
const connectedDestinationCalendarsPromises = users.map((user) =>
|
||||
getConnectedDestinationCalendars(user, false, prisma).then((connectedCalendarsResult) =>
|
||||
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
|
||||
userId: user.id,
|
||||
...calendar,
|
||||
}))
|
||||
getConnectedDestinationCalendarsAndEnsureDefaultsInDb({ user, onboarding: false, prisma }).then(
|
||||
(connectedCalendarsResult) =>
|
||||
connectedCalendarsResult.connectedCalendars.map((calendar) => ({
|
||||
userId: user.id,
|
||||
...calendar,
|
||||
}))
|
||||
)
|
||||
);
|
||||
const connectedDestinationCalendars = await Promise.all(connectedDestinationCalendarsPromises);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
|
||||
@@ -49,7 +49,9 @@ import { selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
await prisma.selectedCalendar.delete({ where: { userId_integration_externalId } });
|
||||
await SelectedCalendarRepository.deleteUserLevel({
|
||||
where: userId_integration_externalId,
|
||||
});
|
||||
return { message: `Selected Calendar with id: ${query.id} deleted successfully` };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/validations/selected-calendar";
|
||||
|
||||
@@ -49,8 +49,8 @@ import { schemaSelectedCalendarPublic, selectedCalendarIdSchema } from "~/lib/va
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
const data = await prisma.selectedCalendar.findUniqueOrThrow({
|
||||
where: { userId_integration_externalId },
|
||||
const data = await SelectedCalendarRepository.findUserLevelUniqueOrThrow({
|
||||
where: userId_integration_externalId,
|
||||
});
|
||||
return { selected_calendar: schemaSelectedCalendarPublic.parse(data) };
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import type { UpdateArguments } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
@@ -56,7 +58,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
const { query, isSystemWideAdmin } = req;
|
||||
const userId_integration_externalId = selectedCalendarIdSchema.parse(query);
|
||||
const { userId: bodyUserId, ...data } = schemaSelectedCalendarUpdateBodyParams.parse(req.body);
|
||||
const args: Prisma.SelectedCalendarUpdateArgs = { where: { userId_integration_externalId }, data };
|
||||
const args: UpdateArguments = { where: { ...userId_integration_externalId }, data };
|
||||
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
@@ -67,7 +69,7 @@ export async function patchHandler(req: NextApiRequest) {
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
const result = await prisma.selectedCalendar.update(args);
|
||||
const result = await SelectedCalendarRepository.updateUserLevel(args);
|
||||
return { selected_calendar: schemaSelectedCalendarPublic.parse(result) };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { FindManyArgs } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
|
||||
import { schemaSelectedCalendarPublic } from "~/lib/validations/selected-calendar";
|
||||
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";
|
||||
@@ -34,7 +34,7 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
/* Admin gets all selected calendar by default, otherwise only the user's ones */
|
||||
const args: Prisma.SelectedCalendarFindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
const args: FindManyArgs = isSystemWideAdmin ? {} : { where: { userId } };
|
||||
|
||||
/** Only admins can query other users */
|
||||
if (!isSystemWideAdmin && req.query.userId)
|
||||
@@ -46,7 +46,7 @@ async function getHandler(req: NextApiRequest) {
|
||||
if (Array.isArray(query.userId)) args.orderBy = { userId: "asc" };
|
||||
}
|
||||
|
||||
const data = await prisma.selectedCalendar.findMany(args);
|
||||
const data = await SelectedCalendarRepository.findManyUserLevel(args);
|
||||
return { selected_calendars: data.map((v) => schemaSelectedCalendarPublic.parse(v)) };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import {
|
||||
@@ -52,7 +53,11 @@ import {
|
||||
async function postHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const { userId: bodyUserId, ...body } = schemaSelectedCalendarBodyParams.parse(req.body);
|
||||
const args: Prisma.SelectedCalendarCreateArgs = { data: { ...body, userId } };
|
||||
const args: {
|
||||
data: Prisma.SelectedCalendarUncheckedCreateInput;
|
||||
} = {
|
||||
data: { ...body, userId },
|
||||
};
|
||||
|
||||
if (!isSystemWideAdmin && bodyUserId)
|
||||
throw new HttpError({ statusCode: 403, message: `ADMIN required for userId` });
|
||||
@@ -63,7 +68,7 @@ async function postHandler(req: NextApiRequest) {
|
||||
args.data.userId = bodyUserId;
|
||||
}
|
||||
|
||||
const data = await prisma.selectedCalendar.create(args);
|
||||
const data = await SelectedCalendarRepository.create(args.data);
|
||||
|
||||
return {
|
||||
selected_calendar: schemaSelectedCalendarPublic.parse(data),
|
||||
|
||||
@@ -52,6 +52,8 @@ describe("POST /api/attendees", () => {
|
||||
bookingId: 1,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
prismaMock.attendee.create.mockResolvedValue(attendeeData);
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = 123;
|
||||
@@ -84,7 +86,8 @@ describe("POST /api/attendees", () => {
|
||||
timeZone: "UTC",
|
||||
bookingId: 1,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
prismaMock.attendee.create.mockResolvedValue(attendeeData);
|
||||
|
||||
req.userId = 123;
|
||||
|
||||
@@ -11,7 +11,7 @@ import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Booking } from "@calcom/prisma";
|
||||
import type { Booking } from "@calcom/prisma/client";
|
||||
|
||||
import handler from "../../../pages/api/bookings/_post";
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ describe("DELETE /api/event-types/[id]", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
// Mocking membership.findFirst
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
prismaMock.membership.findFirst.mockImplementation(({ where }) => {
|
||||
const { userId, teamId, accepted, role } = where;
|
||||
const mockData = [
|
||||
|
||||
@@ -72,6 +72,7 @@ describe("POST /api/event-types", () => {
|
||||
},
|
||||
});
|
||||
req.userId = memberUserId;
|
||||
// @ts-expect-error - Return type is wrong
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
|
||||
|
||||
await handler(req, res);
|
||||
@@ -114,6 +115,7 @@ describe("POST /api/event-types", () => {
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = adminUserId;
|
||||
|
||||
// @ts-expect-error - Return type is wrong
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => false);
|
||||
|
||||
await handler(req, res);
|
||||
@@ -138,6 +140,7 @@ describe("POST /api/event-types", () => {
|
||||
req.isSystemWideAdmin = true;
|
||||
req.userId = adminUserId;
|
||||
|
||||
// @ts-expect-error - Return type is wrong
|
||||
vi.mocked(canUserAccessTeamWithRole).mockImplementationOnce(async () => true);
|
||||
vi.mocked(checkParentEventOwnership).mockImplementationOnce(async () => undefined);
|
||||
vi.mocked(checkUserMembership).mockImplementationOnce(async () => undefined);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { ILicenseKeyService } from "@calcom/ee/common/server/LicenseKeyService";
|
||||
import LicenseKeyService from "@calcom/ee/common/server/LicenseKeyService";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||
@@ -21,7 +22,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("Verify API key", () => {
|
||||
let service: LicenseKeyService;
|
||||
let service: ILicenseKeyService;
|
||||
|
||||
beforeEach(async () => {
|
||||
service = await LicenseKeyService.create();
|
||||
|
||||
@@ -79,6 +79,15 @@ describe("POST /api/selected-calendars", () => {
|
||||
integration: "google",
|
||||
externalId: "ext123",
|
||||
userId: 444444,
|
||||
id: "xxx-xxx",
|
||||
eventTypeId: null,
|
||||
domainWideDelegationCredentialId: null,
|
||||
googleChannelId: null,
|
||||
googleChannelKind: null,
|
||||
googleChannelResourceId: null,
|
||||
googleChannelResourceUri: null,
|
||||
googleChannelExpiration: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
@@ -104,10 +113,19 @@ describe("POST /api/selected-calendars", () => {
|
||||
req.userId = 333333;
|
||||
|
||||
prismaMock.selectedCalendar.create.mockResolvedValue({
|
||||
id: "xxx-xxx",
|
||||
credentialId: 1,
|
||||
integration: "google",
|
||||
externalId: "ext123",
|
||||
userId: 333333,
|
||||
googleChannelId: null,
|
||||
googleChannelKind: null,
|
||||
googleChannelResourceId: null,
|
||||
googleChannelResourceUri: null,
|
||||
googleChannelExpiration: null,
|
||||
domainWideDelegationCredentialId: null,
|
||||
eventTypeId: null,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
@@ -2,6 +2,8 @@ import prismock from "../../../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { describe, expect, it, beforeEach } from "vitest";
|
||||
|
||||
import { WatchlistSeverity } from "@calcom/prisma/enums";
|
||||
|
||||
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
|
||||
|
||||
describe("isLockedOrBlocked", () => {
|
||||
@@ -16,6 +18,7 @@ describe("isLockedOrBlocked", () => {
|
||||
{
|
||||
type: "DOMAIN",
|
||||
value: "blocked.com",
|
||||
severity: WatchlistSeverity.CRITICAL,
|
||||
createdById: 1,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine3.20 as build
|
||||
FROM node:18-alpine as build
|
||||
|
||||
ARG DATABASE_DIRECT_URL
|
||||
ARG DATABASE_URL
|
||||
@@ -6,6 +6,7 @@ ARG DATABASE_URL
|
||||
WORKDIR /calcom
|
||||
|
||||
RUN set -eux;
|
||||
RUN ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
ENV NODE_OPTIONS="--max-old-space-size=8192"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.70",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.78",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -30,6 +30,8 @@ const loadConfig = (): AppConfig => {
|
||||
stripe: {
|
||||
apiKey: getEnv("STRIPE_API_KEY"),
|
||||
webhookSecret: getEnv("STRIPE_WEBHOOK_SECRET"),
|
||||
teamMonthlyPriceId: getEnv("STRIPE_TEAM_MONTHLY_PRICE_ID", "set-team-monthly-price-in-your-env"),
|
||||
isTeamBillingEnabled: getEnv("IS_TEAM_BILLING_ENABLED", true),
|
||||
},
|
||||
app: {
|
||||
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
||||
|
||||
@@ -21,6 +21,8 @@ export type AppConfig = {
|
||||
stripe: {
|
||||
apiKey: string;
|
||||
webhookSecret: string;
|
||||
teamMonthlyPriceId: string;
|
||||
isTeamBillingEnabled: boolean;
|
||||
};
|
||||
app: {
|
||||
baseUrl: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
@@ -13,53 +14,66 @@ import {
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
optionValue!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value!: string;
|
||||
}
|
||||
|
||||
class Response {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
guests!: string[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Location)
|
||||
@ApiPropertyOptional({ type: Location })
|
||||
location?: Location;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class CreateBookingInput_2024_04_15 {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
end?: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
start!: string;
|
||||
|
||||
@IsNumber()
|
||||
@ApiProperty()
|
||||
eventTypeId!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
eventTypeSlug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
rescheduleUid?: string;
|
||||
|
||||
@IsTimeZone()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@Transform(({ value }: { value: string | string[] }) => {
|
||||
@@ -68,38 +82,48 @@ export class CreateBookingInput_2024_04_15 {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
user?: string[];
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
language!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
bookingUid?: string;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
metadata!: Record<string, string>;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hasHashedBookingLink?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hashedLink!: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
seatReferenceUid?: string;
|
||||
|
||||
@Type(() => Response)
|
||||
@ApiProperty({ type: Response })
|
||||
responses!: Response;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
orgSlug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
locationUrl?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
@@ -6,23 +7,29 @@ import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
noEmail?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiPropertyOptional()
|
||||
recurringCount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
appsStatus?: AppsStatus[] | undefined;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
allRecurringDates?: Record<string, string>[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiPropertyOptional()
|
||||
currentRecurringIndex?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
recurringEventId?: string;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { ApiPropertyOptional, ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsOptional, IsArray, IsEmail, IsBoolean, ValidateNested } from "class-validator";
|
||||
|
||||
class Attendee {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
noShow!: boolean;
|
||||
}
|
||||
|
||||
export class MarkNoShowInput_2024_04_15 {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
noShowHost?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Attendee] })
|
||||
attendees?: Attendee[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -17,145 +17,185 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Metadata {
|
||||
@IsUrl()
|
||||
@ApiProperty()
|
||||
videoCallUrl!: string;
|
||||
}
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
optionValue!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value!: string;
|
||||
}
|
||||
|
||||
class Response {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
notes!: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
guests!: string[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Location)
|
||||
@ApiProperty({ type: Location })
|
||||
location!: Location;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
class Attendee {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
class EventType {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
eventName!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
slug!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
timeZone!: string | null;
|
||||
}
|
||||
|
||||
class GetBookingData_2024_04_15 {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
uid!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
customInputs!: any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
smsReminderNumber!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
recurringEventId!: string | null;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
startTime!: Date;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
endTime!: Date;
|
||||
|
||||
@IsUrl()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
location!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
status!: string;
|
||||
|
||||
@Type(() => Metadata)
|
||||
@ApiProperty({ type: Metadata })
|
||||
metadata!: Metadata | any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
cancellationReason!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Response)
|
||||
@ApiProperty({ type: Response })
|
||||
responses!: Response | any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
rejectionReason!: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
userPrimaryEmail!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => User)
|
||||
@ApiProperty({ type: User, nullable: true })
|
||||
user!: User | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Attendee] })
|
||||
attendees!: Attendee[];
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
eventTypeId!: number | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EventType)
|
||||
@ApiProperty({ type: EventType, nullable: true })
|
||||
eventType!: EventType | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -26,179 +26,230 @@ enum Status {
|
||||
|
||||
class Attendee {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
locale!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
bookingId!: number | null;
|
||||
}
|
||||
|
||||
class EventType {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional()
|
||||
id?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
eventName?: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
price!: number;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
recurringEvent?: any;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
currency!: string;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty()
|
||||
metadata!: any;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
seatsShowAttendees?: boolean | undefined | null;
|
||||
@ApiPropertyOptional({ type: Boolean, nullable: true })
|
||||
seatsShowAttendees?: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
seatsShowAvailabilityCount?: boolean | undefined | null;
|
||||
@ApiPropertyOptional({ type: Boolean, nullable: true })
|
||||
seatsShowAvailabilityCount?: boolean | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
team?: any | null;
|
||||
}
|
||||
|
||||
class Reference {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
uid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
meetingId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
thirdPartyRecurringEventId?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
meetingPassword!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
meetingUrl?: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
bookingId!: number | null;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
externalCalendarId!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
deleted?: any;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
credentialId!: number | null;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
}
|
||||
|
||||
class GetBookingsDataEntry {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
userPrimaryEmail?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
customInputs!: object | any;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
startTime!: string;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
endTime!: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Attendee] })
|
||||
attendees!: Attendee[];
|
||||
|
||||
@ApiProperty()
|
||||
metadata!: any;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
uid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
recurringEventId!: string | null;
|
||||
|
||||
@IsUrl()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
location!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EventType)
|
||||
@ApiProperty({ type: EventType })
|
||||
eventType!: EventType;
|
||||
|
||||
@IsEnum(Status)
|
||||
status!: "CANCELLED" | "REJECTED" | "ACCEPTED" | "PENDING" | "AWAITING_HOST";
|
||||
@ApiProperty({ enum: Status, type: String })
|
||||
status!: Status;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean })
|
||||
paid!: boolean;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
payment!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Reference)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Reference] })
|
||||
references!: Reference[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean })
|
||||
isRecorded!: boolean;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
seatsReferences!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => User)
|
||||
@ApiProperty({ type: User })
|
||||
user!: User | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
rescheduled?: any;
|
||||
}
|
||||
|
||||
@@ -206,17 +257,20 @@ class GetBookingsData_2024_04_15 {
|
||||
@ValidateNested()
|
||||
@Type(() => GetBookingsDataEntry)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [GetBookingsDataEntry] })
|
||||
bookings!: GetBookingsDataEntry[];
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
recurringInfo!: any[];
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
nextCursor!: number | null;
|
||||
}
|
||||
|
||||
export class GetBookingsOutput_2024_04_15 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS], type: String })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsString, IsEnum, IsOptional, ValidateNested, IsArray, IsEmail, IsBoolean } from "class-validator";
|
||||
|
||||
@@ -6,24 +6,29 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Attendee {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
noShow!: boolean;
|
||||
}
|
||||
|
||||
class HandleMarkNoShowData_2024_04_15 {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
message!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
noShowHost?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Attendee] })
|
||||
attendees?: Attendee[];
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.eventTypeId).toEqual(eventTypeId);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
|
||||
@@ -181,6 +181,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -201,6 +202,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.recurringBookingUid).toBeDefined();
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -256,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -277,6 +280,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.recurringBookingUid).toBeDefined();
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -296,6 +300,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(thirdBooking.recurringBookingUid).toBeDefined();
|
||||
expect(thirdBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
|
||||
@@ -205,6 +205,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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,
|
||||
@@ -278,6 +279,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const firstAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeOne);
|
||||
expect(firstAttendee).toEqual({
|
||||
name: createdSeatedBooking.attendees[0].name,
|
||||
email: createdSeatedBooking.attendees[0].email,
|
||||
timeZone: createdSeatedBooking.attendees[0].timeZone,
|
||||
language: createdSeatedBooking.attendees[0].language,
|
||||
absent: false,
|
||||
@@ -291,6 +293,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const secondAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeTwo);
|
||||
expect(secondAttendee).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -368,6 +371,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.attendees.length).toEqual(1);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -402,6 +406,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.attendees.length).toEqual(1);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -576,6 +581,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const attendee = createdSeatedBooking.attendees.find((a) => a.seatUid === body.seatUid);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: attendee?.name,
|
||||
email: attendee?.email,
|
||||
timeZone: attendee?.timeZone,
|
||||
language: attendee?.language,
|
||||
absent: false,
|
||||
@@ -835,6 +841,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.attendees.length).toEqual(1);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -869,6 +876,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.attendees.length).toEqual(1);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -946,6 +954,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.attendees.length).toEqual(2);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -958,6 +967,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(firstBooking.attendees[1]).toEqual({
|
||||
name: nameAttendeeTwo,
|
||||
email: emailAttendeeTwo,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -992,6 +1002,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.attendees.length).toEqual(2);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -1004,6 +1015,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(secondBooking.attendees[1]).toEqual({
|
||||
name: nameAttendeeTwo,
|
||||
email: emailAttendeeTwo,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
|
||||
@@ -307,6 +307,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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,
|
||||
@@ -359,12 +360,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.attendees.length).toEqual(2);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.attendees[1]).toEqual({
|
||||
name: teamUser2.name,
|
||||
email: teamUser2.email,
|
||||
timeZone: teamUser2.timeZone,
|
||||
language: teamUser2.locale,
|
||||
absent: false,
|
||||
|
||||
@@ -364,6 +364,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -434,6 +435,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringEventTypeId);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -455,6 +457,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(secondBooking.recurringBookingUid).toBeDefined();
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
@@ -475,6 +478,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(thirdBooking.recurringBookingUid).toBeDefined();
|
||||
expect(thirdBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
|
||||
@@ -340,7 +340,10 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body);
|
||||
await handleCancelBooking(bookingRequest);
|
||||
const res = await handleCancelBooking(bookingRequest);
|
||||
if (!res.onlyRemovedAttendee) {
|
||||
await this.billingService.cancelUsageByBookingUid(res.bookingUid);
|
||||
}
|
||||
|
||||
if ("cancelSubsequentBookings" in body && body.cancelSubsequentBookings) {
|
||||
return this.getAllRecurringBookingsByIndividualUid(bookingUid);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import {
|
||||
bookingResponsesSchema,
|
||||
seatedBookingResponsesSchema,
|
||||
seatedBookingDataSchema,
|
||||
} from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
@@ -340,7 +340,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Seat with uid=${inputBooking.seatUid} does not exist.`);
|
||||
}
|
||||
|
||||
const { responses: bookingResponses } = seatedBookingResponsesSchema.parse(seat.data);
|
||||
const { responses: bookingResponses } = seatedBookingDataSchema.parse(seat.data);
|
||||
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
|
||||
if (!attendee) {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import {
|
||||
defaultBookingMetadata,
|
||||
defaultBookingResponses,
|
||||
defaultSeatedBookingData,
|
||||
defaultSeatedBookingMetadata,
|
||||
} from "@/lib/safe-parse/default-responses-booking";
|
||||
import { safeParse } from "@/lib/safe-parse/safe-parse";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import { DateTime } from "luxon";
|
||||
@@ -30,9 +37,10 @@ export const bookingResponsesSchema = z
|
||||
guests: z.array(z.string()).optional(),
|
||||
rescheduleReason: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
.describe("BookingResponses");
|
||||
|
||||
export const seatedBookingResponsesSchema = z
|
||||
export const seatedBookingDataSchema = z
|
||||
.object({
|
||||
responses: z
|
||||
.object({
|
||||
@@ -47,7 +55,10 @@ export const seatedBookingResponsesSchema = z
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
.passthrough();
|
||||
.passthrough()
|
||||
.describe("SeatedBookingData");
|
||||
|
||||
const seatedBookingMetadataSchema = z.object({}).catchall(z.string()).describe("SeatedBookingMetadata");
|
||||
|
||||
type DatabaseUser = { id: number; name: string | null; email: string; username: string | null };
|
||||
|
||||
@@ -72,8 +83,6 @@ type BookingWithUser = Booking & { user: DatabaseUser | null };
|
||||
|
||||
type DatabaseMetadata = z.infer<typeof bookingMetadataSchema>;
|
||||
|
||||
const seatedBookingMetadataSchema = z.object({}).catchall(z.string());
|
||||
|
||||
@Injectable()
|
||||
export class OutputBookingsService_2024_08_13 {
|
||||
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
||||
@@ -82,8 +91,12 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const bookingResponses = safeParse(
|
||||
bookingResponsesSchema,
|
||||
databaseBooking.responses,
|
||||
defaultBookingResponses
|
||||
);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
@@ -167,8 +180,12 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const bookingResponses = safeParse(
|
||||
bookingResponsesSchema,
|
||||
databaseBooking.responses,
|
||||
defaultBookingResponses
|
||||
);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
@@ -225,7 +242,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
@@ -254,7 +271,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData
|
||||
);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
@@ -267,7 +288,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata);
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
@@ -320,7 +345,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
const metadata = bookingMetadataSchema.parse(databaseBooking.metadata);
|
||||
const metadata = safeParse(bookingMetadataSchema, databaseBooking.metadata, defaultBookingMetadata);
|
||||
const location = metadata?.videoCallUrl || databaseBooking.location;
|
||||
|
||||
const booking = {
|
||||
@@ -353,7 +378,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
|
||||
const { responses } = safeParse(
|
||||
seatedBookingDataSchema,
|
||||
attendee.bookingSeat?.data,
|
||||
defaultSeatedBookingData
|
||||
);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
@@ -366,7 +395,11 @@ export class OutputBookingsService_2024_08_13 {
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata);
|
||||
attendeeParsed.metadata = safeParse(
|
||||
seatedBookingMetadataSchema,
|
||||
attendee.bookingSeat?.metadata,
|
||||
defaultSeatedBookingMetadata
|
||||
);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
return attendeeParsed;
|
||||
|
||||
@@ -86,18 +86,16 @@ export class CalendarsController {
|
||||
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@Get("/busy-times")
|
||||
@ApiOperation({ summary: "Get busy times" })
|
||||
@ApiOperation({
|
||||
summary: "Get busy times",
|
||||
description:
|
||||
"Get busy times from a calendar. Example request URL is `https://api.cal.com/v2/calendars/busy-times?loggedInUsersTz=Europe%2FMadrid&dateFrom=2024-12-18&dateTo=2024-12-18&calendarsToLoad[0][credentialId]=135&calendarsToLoad[0][externalId]=skrauciz%40gmail.com`",
|
||||
})
|
||||
async getBusyTimes(
|
||||
@Query() queryParams: CalendarBusyTimesInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<GetBusyTimesOutput> {
|
||||
const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams;
|
||||
if (!dateFrom || !dateTo) {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
const busyTimes = await this.calendarsService.getBusyTimes(
|
||||
calendarsToLoad,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
IsBoolean,
|
||||
@@ -50,11 +50,10 @@ export class CreateIcsFeedInputDto {
|
||||
urls!: string[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
@ApiPropertyOptional({
|
||||
example: false,
|
||||
description: "Whether to allowing writing to the calendar or not",
|
||||
type: "boolean",
|
||||
required: false,
|
||||
default: true,
|
||||
})
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsDate, IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
@@ -6,13 +6,16 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class BusyTimesOutput {
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
start!: Date;
|
||||
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
end!: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
source?: string | null;
|
||||
}
|
||||
|
||||
@@ -24,5 +27,6 @@ export class GetBusyTimesOutput {
|
||||
@ValidateNested()
|
||||
@Type(() => BusyTimesOutput)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [BusyTimesOutput] })
|
||||
data!: BusyTimesOutput[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
@@ -18,195 +18,246 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
class Integration {
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ApiPropertyOptional({ type: Object, nullable: true })
|
||||
appData?: object | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
dirName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
__template?: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
description!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
installed?: boolean;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
variant!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
category?: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
categories!: string[];
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
logo!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
publisher!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
slug!: string;
|
||||
|
||||
@IsUrl()
|
||||
@ApiProperty()
|
||||
url!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object, nullable: true })
|
||||
locationOption!: object | null;
|
||||
}
|
||||
|
||||
class Primary {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
externalId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
integration?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean, nullable: true })
|
||||
primary!: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
readOnly!: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isSelected!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
export class Calendar {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
externalId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
integration?: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, nullable: true })
|
||||
primary?: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
readOnly!: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isSelected!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
export class ConnectedCalendar {
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Integration })
|
||||
integration!: Integration;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
credentialId!: number;
|
||||
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Primary })
|
||||
primary?: Primary;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Calendar] })
|
||||
calendars?: Calendar[];
|
||||
}
|
||||
|
||||
class DestinationCalendar {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
integration!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
externalId!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
primaryEmail!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ nullable: true })
|
||||
userId!: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
eventTypeId!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
credentialId!: number | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
name?: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
primary?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readOnly?: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
integrationTitle?: string;
|
||||
}
|
||||
|
||||
export class ConnectedCalendarsData {
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [ConnectedCalendar] })
|
||||
connectedCalendars!: ConnectedCalendar[];
|
||||
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@ApiProperty({ type: DestinationCalendar })
|
||||
destinationCalendar!: DestinationCalendar;
|
||||
}
|
||||
|
||||
export class ConnectedCalendarsOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
@@ -214,5 +265,6 @@ export class ConnectedCalendarsOutput {
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => ConnectedCalendarsData)
|
||||
@ApiProperty({ type: ConnectedCalendarsData })
|
||||
data!: ConnectedCalendarsData;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { APPS_TYPE_ID_MAPPING } from "@calcom/platform-constants";
|
||||
import { getConnectedDestinationCalendars, getBusyCalendarTimes } from "@calcom/platform-libraries";
|
||||
import {
|
||||
getConnectedDestinationCalendarsAndEnsureDefaultsInDb,
|
||||
getBusyCalendarTimes,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { Calendar } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@@ -41,12 +44,18 @@ export class CalendarsService {
|
||||
if (!userWithCalendars) {
|
||||
throw new NotFoundException("User not found");
|
||||
}
|
||||
|
||||
return getConnectedDestinationCalendars(
|
||||
userWithCalendars,
|
||||
false,
|
||||
this.dbWrite.prisma as unknown as PrismaClient
|
||||
);
|
||||
return getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
||||
user: {
|
||||
...userWithCalendars,
|
||||
allSelectedCalendars: userWithCalendars.selectedCalendars,
|
||||
userLevelSelectedCalendars: userWithCalendars.selectedCalendars.filter(
|
||||
(calendar) => !calendar.eventTypeId
|
||||
),
|
||||
},
|
||||
onboarding: false,
|
||||
eventTypeId: null,
|
||||
prisma: this.dbWrite.prisma as unknown as PrismaClient,
|
||||
});
|
||||
}
|
||||
|
||||
async getBusyTimes(
|
||||
@@ -64,7 +73,6 @@ export class CalendarsService {
|
||||
);
|
||||
try {
|
||||
const calendarBusyTimes = await getBusyCalendarTimes(
|
||||
"",
|
||||
credentials,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
|
||||
@@ -116,7 +116,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
) {
|
||||
// User chose not to authorize your app or didn't authorize your app
|
||||
// redirect directly without oauth code
|
||||
if (!code) {
|
||||
if (!code || code === "undefined") {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ export class OutlookService implements OAuthCalendarApp {
|
||||
redir?: string
|
||||
) {
|
||||
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
|
||||
if (!code) {
|
||||
if (!code || code === "undefined") {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
|
||||
+18
-7
@@ -1,5 +1,5 @@
|
||||
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -36,7 +36,10 @@ export class CreateEventTypeInput_2024_04_15 {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: CREATE_EVENT_DESCRIPTION_EXAMPLE,
|
||||
})
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@@ -48,30 +51,38 @@ export class CreateEventTypeInput_2024_04_15 {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiPropertyOptional({
|
||||
type: [EventTypeLocation_2024_04_15],
|
||||
})
|
||||
locations?: EventTypeLocation_2024_04_15[];
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
disableGuests?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
slotInterval?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
minimumBookingNotice?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
beforeEventBuffer?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
afterEventBuffer?: number;
|
||||
|
||||
// @ApiHideProperty()
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiHideProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validator";
|
||||
|
||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||
@@ -6,7 +6,7 @@ import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validato
|
||||
|
||||
export class EventTypeLocation_2024_04_15 {
|
||||
@IsString()
|
||||
@DocsProperty({ example: "link" })
|
||||
@ApiProperty({ example: "link" })
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@@ -16,7 +16,7 @@ export class EventTypeLocation_2024_04_15 {
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@DocsProperty({ example: "https://masterchief.com/argentina/flan/video/9129412" })
|
||||
@ApiPropertyOptional({ example: "https://masterchief.com/argentina/flan/video/9129412" })
|
||||
link?: string;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
||||
|
||||
@@ -6,11 +6,11 @@ export class GetPublicEventTypeQueryParams_2024_04_15 {
|
||||
@Transform(({ value }: { value: string }) => value === "true")
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiProperty({ required: false })
|
||||
@ApiPropertyOptional()
|
||||
isTeamEvent?: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
org?: string | null;
|
||||
}
|
||||
|
||||
+66
@@ -2,6 +2,7 @@ import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/e
|
||||
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
|
||||
import { Frequency } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/frequency";
|
||||
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -21,206 +22,259 @@ import {
|
||||
|
||||
class Option {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
editUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
fieldRequired?: boolean;
|
||||
}
|
||||
|
||||
class View {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
class OptionsInput {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Type of the field, can be one of "address", "text", or "phone".',
|
||||
enum: ["address", "text", "phone"],
|
||||
example: "text",
|
||||
})
|
||||
type!: "address" | "text" | "phone";
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class VariantField {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
class Variant {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => VariantField)
|
||||
@ApiProperty({ type: [VariantField] })
|
||||
fields!: VariantField[];
|
||||
}
|
||||
|
||||
class VariantsConfig {
|
||||
@ApiProperty({ type: Object })
|
||||
variants!: Record<string, Variant>;
|
||||
}
|
||||
|
||||
export class BookingField_2024_04_15 {
|
||||
@IsEnum(BaseField)
|
||||
@ApiProperty({ enum: BaseField })
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Option)
|
||||
@ApiPropertyOptional({ type: [Option] })
|
||||
options?: Option[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OptionsInput)
|
||||
@ApiPropertyOptional()
|
||||
optionsInputs?: Record<string, OptionsInput>;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
variant?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => VariantsConfig)
|
||||
@ApiPropertyOptional({ type: VariantsConfig })
|
||||
variantsConfig?: VariantsConfig;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => View)
|
||||
@ApiPropertyOptional({ type: [View] })
|
||||
views?: View[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
hidden?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(Editable)
|
||||
@ApiPropertyOptional()
|
||||
editable?: Editable;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
@ApiPropertyOptional({ type: [Source] })
|
||||
sources?: Source[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
disableOnPrefill?: boolean;
|
||||
}
|
||||
|
||||
export class RecurringEvent_2024_04_15 {
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Date })
|
||||
dtstart?: Date;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
interval!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
count!: number;
|
||||
|
||||
@IsEnum(Frequency)
|
||||
@ApiProperty({ enum: Frequency })
|
||||
freq!: Frequency;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Date })
|
||||
until?: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
tzid?: string;
|
||||
}
|
||||
|
||||
export class IntervalLimits_2024_04_15 {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_DAY?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_WEEK?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_MONTH?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_YEAR?: number;
|
||||
}
|
||||
|
||||
@@ -228,27 +282,33 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
length?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
slug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
description?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hidden?: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation_2024_04_15)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [EventTypeLocation_2024_04_15] })
|
||||
locations?: EventTypeLocation_2024_04_15[];
|
||||
|
||||
// @IsInt()
|
||||
@@ -283,6 +343,7 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField_2024_04_15)
|
||||
@ApiPropertyOptional({ type: [BookingField_2024_04_15] })
|
||||
bookingFields?: BookingField_2024_04_15[];
|
||||
|
||||
// @IsString()
|
||||
@@ -328,6 +389,7 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
disableGuests?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
@@ -337,16 +399,19 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
minimumBookingNotice?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
beforeEventBuffer?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
afterEventBuffer?: number;
|
||||
|
||||
// @IsInt()
|
||||
@@ -384,6 +449,7 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
slotInterval?: number;
|
||||
|
||||
// @IsString()
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
IntervalLimits_2024_04_15,
|
||||
RecurringEvent_2024_04_15,
|
||||
} from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
@@ -29,23 +29,23 @@ import {
|
||||
|
||||
export class EventTypeOutput {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
@ApiProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE, nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@@ -55,6 +55,7 @@ export class EventTypeOutput {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [EventTypeLocation_2024_04_15], nullable: true })
|
||||
locations!: EventTypeLocation_2024_04_15[] | null;
|
||||
|
||||
@IsInt()
|
||||
|
||||
+94
-3
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
@@ -17,330 +17,421 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class OptionInput {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class BookingField {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
optionsInputs?: { [key: string]: OptionInput };
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
editable?: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Source] })
|
||||
sources?: Source[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
disableOnPrefill?: boolean;
|
||||
}
|
||||
|
||||
class Organization {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
slug?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: Record<string, any>;
|
||||
@ApiPropertyOptional({ type: Object })
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
class Profile {
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
id!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
userId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
uid?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
organizationId!: number | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Organization)
|
||||
@ApiPropertyOptional({ type: Organization, nullable: true })
|
||||
organization?: Organization | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
upId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
image?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
brandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
darkBrandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
bookerLayouts?: any;
|
||||
}
|
||||
|
||||
class Owner {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
weekStart!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
brandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
darkBrandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
theme?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: any;
|
||||
@ApiPropertyOptional()
|
||||
metadata?: any;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
defaultScheduleId?: number | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
nonProfileUsername!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Profile)
|
||||
@ApiProperty({ type: Profile })
|
||||
profile!: Profile;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
weekStart!: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
organizationId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@ApiProperty({ type: Profile })
|
||||
profile!: Profile;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
bookerUrl!: string;
|
||||
}
|
||||
|
||||
class Schedule {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
timeZone!: string | null;
|
||||
}
|
||||
|
||||
class PublicEventTypeOutput {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
description!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
eventName?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
slug!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isInstantEvent!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
aiPhoneCallConfig?: any;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
schedulingType?: any;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
length!: number;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Location)
|
||||
@ApiProperty({ type: [Location] })
|
||||
locations!: Location[];
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Object] })
|
||||
customInputs!: any[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
disableGuests!: boolean;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object, nullable: true })
|
||||
metadata!: object | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
lockTimeZoneToggleOnBookingPage!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
requiresConfirmation!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
requiresBookerEmailVerification!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
recurringEvent?: any;
|
||||
|
||||
@IsNumber()
|
||||
@ApiProperty()
|
||||
price!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
currency!: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
seatsPerTimeSlot?: number | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean, nullable: true })
|
||||
seatsShowAvailabilityCount!: boolean | null;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField)
|
||||
@ApiProperty({ type: [BookingField] })
|
||||
bookingFields!: BookingField[];
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
team?: any;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
successRedirectUrl?: string | null;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
workflows!: any[];
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
hosts!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Owner)
|
||||
@ApiProperty({ type: Owner, nullable: true })
|
||||
owner!: Owner | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Schedule)
|
||||
@ApiProperty({ type: Schedule, nullable: true })
|
||||
schedule!: Schedule | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
hidden!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
assignAllTeamMembers!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
bookerLayouts?: any;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => User)
|
||||
@ApiProperty({ type: [User] })
|
||||
users!: User[];
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
entity!: object;
|
||||
|
||||
@IsBoolean()
|
||||
|
||||
+147
-13
@@ -291,6 +291,7 @@ describe("Event types Endpoints", () => {
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: true,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
scheduleId: firstSchedule.id,
|
||||
@@ -381,10 +382,24 @@ describe("Event types Endpoints", () => {
|
||||
const requestBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
// note(Lauris): location booking field is added if multiple locations are passed
|
||||
{ isDefault: true, required: false, slug: "location", type: "radioInput" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "location",
|
||||
type: "radioInput",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
@@ -787,6 +802,7 @@ describe("Event types Endpoints", () => {
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
bookingLimitsCount: {
|
||||
@@ -853,8 +869,15 @@ describe("Event types Endpoints", () => {
|
||||
const requestBookingFields = body.bookingFields || [];
|
||||
const expectedBookingFields = [
|
||||
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
...requestBookingFields
|
||||
.filter((field) => field.type !== "name" && field.type !== "email")
|
||||
.map((field) => ({ isDefault: false, ...field })),
|
||||
@@ -1055,14 +1078,44 @@ describe("Event types Endpoints", () => {
|
||||
let legacyEventTypeId2: number;
|
||||
|
||||
const expectedReturnSystemFields = [
|
||||
{ isDefault: true, required: true, slug: "name", type: "name" },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email" },
|
||||
{ isDefault: true, type: "radioInput", slug: "location", required: false },
|
||||
{ isDefault: true, required: true, slug: "title", type: "text" },
|
||||
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
|
||||
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false },
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
label: "",
|
||||
placeholder: "",
|
||||
},
|
||||
{ isDefault: true, required: true, slug: "title", type: "text", disableOnPrefill: false, hidden: true },
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "notes",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "guests",
|
||||
type: "multiemail",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
required: false,
|
||||
slug: "rescheduleReason",
|
||||
type: "textarea",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false, hidden: true },
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -1382,6 +1435,8 @@ describe("Event types Endpoints", () => {
|
||||
label: userDefinedBookingField.label,
|
||||
required: userDefinedBookingField.required,
|
||||
placeholder: userDefinedBookingField.placeholder,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -1435,6 +1490,85 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return event type with default bookingFields if they are not defined", async () => {
|
||||
const eventTypeInput = {
|
||||
title: "undefined booking fields",
|
||||
description: "undefined booking fields",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "undefined-booking-fields",
|
||||
locations: [],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
};
|
||||
const eventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${eventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
+18
-1
@@ -34,6 +34,7 @@ import {
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
@@ -113,10 +114,26 @@ export class EventTypesController_2024_06_14 {
|
||||
@Query() queryParams: GetEventTypesQuery_2024_06_14
|
||||
): Promise<GetEventTypesOutput_2024_06_14> {
|
||||
const eventTypes = await this.eventTypesService.getEventTypes(queryParams);
|
||||
const eventTypesFormatted = this.eventTypeResponseTransformPipe.transform(eventTypes);
|
||||
const eventTypesWithoutHiddenFields = eventTypesFormatted.map((eventType) => {
|
||||
return {
|
||||
...eventType,
|
||||
bookingFields: Array.isArray(eventType?.bookingFields)
|
||||
? eventType?.bookingFields
|
||||
.map((field) => {
|
||||
if ("hidden" in field) {
|
||||
return field.hidden !== true ? field : null;
|
||||
}
|
||||
return field;
|
||||
})
|
||||
.filter((f) => f)
|
||||
: [],
|
||||
};
|
||||
}) as EventTypeOutput_2024_06_14[];
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: this.eventTypeResponseTransformPipe.transform(eventTypes),
|
||||
data: eventTypesWithoutHiddenFields,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsString, IsBoolean, IsOptional, IsEnum, Matches } from "class-validator";
|
||||
|
||||
@@ -38,34 +38,34 @@ export class CreatePhoneCallInput {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ description: "Scheduler name" })
|
||||
@ApiPropertyOptional({ description: "Scheduler name" })
|
||||
schedulerName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (value ? value : undefined))
|
||||
@DocsProperty({ description: "Guest name" })
|
||||
@ApiPropertyOptional({ description: "Guest name" })
|
||||
guestName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (value ? value : undefined))
|
||||
@DocsProperty({ description: "Guest email" })
|
||||
@ApiPropertyOptional({ description: "Guest email" })
|
||||
guestEmail?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (value ? value : undefined))
|
||||
@DocsProperty({ description: "Guest company" })
|
||||
@ApiPropertyOptional({ description: "Guest company" })
|
||||
guestCompany?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ description: "Begin message" })
|
||||
@ApiPropertyOptional({ description: "Begin message" })
|
||||
beginMessage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ description: "General prompt" })
|
||||
@ApiPropertyOptional({ description: "General prompt" })
|
||||
generalPrompt?: string;
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ export class EventTypeResponseTransformPipe implements PipeTransform {
|
||||
private transformEventType(eventType: EventTypeResponse): EventTypeOutput_2024_06_14 {
|
||||
return plainToClass(
|
||||
EventTypeOutput_2024_06_14,
|
||||
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType),
|
||||
this.outputEventTypesService.getResponseEventType(eventType.ownerId, eventType, false),
|
||||
{ strategy: "exposeAll" }
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
transformBookingFieldsApiToInternal,
|
||||
transformBookingFieldsApiRequestToInternal,
|
||||
transformLocationsApiToInternal,
|
||||
transformIntervalLimitsApiToInternal,
|
||||
transformFutureBookingLimitsApiToInternal,
|
||||
@@ -218,7 +218,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
hasMultipleLocations: boolean
|
||||
) {
|
||||
const customFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
? transformBookingFieldsApiToInternal(inputBookingFields)
|
||||
? transformBookingFieldsApiRequestToInternal(inputBookingFields)
|
||||
: [];
|
||||
const customFieldsWithoutNameEmail = customFields.filter(
|
||||
(field) => field.type !== "name" && field.type !== "email"
|
||||
|
||||
+19
-2
@@ -21,6 +21,7 @@ import {
|
||||
transformSeatsInternalToApi,
|
||||
InternalLocation,
|
||||
BookingFieldSchema,
|
||||
getBookingFieldsWithSystemFields,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
@@ -88,7 +89,11 @@ type Input = Pick<
|
||||
|
||||
@Injectable()
|
||||
export class OutputEventTypesService_2024_06_14 {
|
||||
getResponseEventType(ownerId: number, databaseEventType: Input): EventTypeOutput_2024_06_14 {
|
||||
getResponseEventType(
|
||||
ownerId: number,
|
||||
databaseEventType: Input,
|
||||
isOrgTeamEvent: boolean
|
||||
): EventTypeOutput_2024_06_14 {
|
||||
const {
|
||||
id,
|
||||
length,
|
||||
@@ -122,7 +127,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
const customName = databaseEventType?.eventName ?? undefined;
|
||||
const bookingFields = databaseEventType.bookingFields
|
||||
? this.transformBookingFields(databaseEventType.bookingFields)
|
||||
: [];
|
||||
: this.getDefaultBookingFields(isOrgTeamEvent);
|
||||
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||
const users = this.transformUsers(databaseEventType.users || []);
|
||||
@@ -240,6 +245,18 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
|
||||
}
|
||||
|
||||
getDefaultBookingFields(isOrgTeamEvent: boolean) {
|
||||
const defaultBookingFields = getBookingFieldsWithSystemFields({
|
||||
disableGuests: false,
|
||||
bookingFields: null,
|
||||
customInputs: [],
|
||||
metadata: null,
|
||||
workflows: [],
|
||||
isOrgTeamEvent,
|
||||
});
|
||||
return this.transformBookingFields(defaultBookingFields);
|
||||
}
|
||||
|
||||
transformRecurringEvent(recurringEvent: any) {
|
||||
if (!recurringEvent) return null;
|
||||
const recurringEventParsed = parseRecurringEvent(recurringEvent);
|
||||
|
||||
@@ -1,39 +1,51 @@
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsInt, IsEmail, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
import { IsString, IsOptional, IsInt, IsEmail, ValidateNested } from "class-validator";
|
||||
|
||||
export class MeOrgOutput {
|
||||
@DocsProperty()
|
||||
isPlatform!: boolean;
|
||||
|
||||
@DocsProperty()
|
||||
id!: number;
|
||||
}
|
||||
|
||||
export class MeOutput {
|
||||
@IsInt()
|
||||
@DocsProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty()
|
||||
username!: string;
|
||||
|
||||
@IsEmail()
|
||||
@DocsProperty()
|
||||
email!: string;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty()
|
||||
timeFormat!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@DocsProperty({ type: Number, nullable: true })
|
||||
defaultScheduleId!: number | null;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty()
|
||||
weekStart!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ type: Number, nullable: true })
|
||||
organizationId!: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => MeOrgOutput)
|
||||
@ApiPropertyOptional({ type: MeOrgOutput })
|
||||
organization?: MeOrgOutput;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { ProviderModule } from "@/ee/provider/provider.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||
import { SlotsModule } from "@/modules/slots/slots.module";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
@@ -19,6 +21,7 @@ import { Module } from "@nestjs/common";
|
||||
ProviderModule,
|
||||
SchedulesModule_2024_04_15,
|
||||
SchedulesModule_2024_06_11,
|
||||
TeamsEventTypesModule,
|
||||
MeModule,
|
||||
EventTypesModule_2024_04_15,
|
||||
EventTypesModule_2024_06_14,
|
||||
@@ -27,6 +30,7 @@ import { Module } from "@nestjs/common";
|
||||
BookingsModule_2024_08_13,
|
||||
TeamsMembershipsModule,
|
||||
SlotsModule,
|
||||
TeamsModule,
|
||||
],
|
||||
})
|
||||
export class PlatformEndpointsModule implements NestModule {
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
export class CreateScheduleInput_2024_04_15 {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsTimeZone()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateAvailabilityInput_2024_04_15)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [CreateAvailabilityInput_2024_04_15] })
|
||||
availabilities?: CreateAvailabilityInput_2024_04_15[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isDefault!: boolean;
|
||||
}
|
||||
|
||||
+25
-3
@@ -1,95 +1,117 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
|
||||
|
||||
class EventTypeModel_2024_04_15 {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
eventName?: string | null;
|
||||
}
|
||||
|
||||
class AvailabilityModel_2024_04_15 {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
userId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
scheduleId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
eventTypeId?: number | null;
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({ type: [Number] })
|
||||
days!: number[];
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
startTime?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
endTime?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
date?: Date | null;
|
||||
}
|
||||
|
||||
class ScheduleModel_2024_04_15 {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
userId!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
timeZone?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeModel_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiPropertyOptional({ type: [EventTypeModel_2024_04_15] })
|
||||
eventType?: EventTypeModel_2024_04_15[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AvailabilityModel_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiPropertyOptional({ type: [AvailabilityModel_2024_04_15] })
|
||||
availability?: AvailabilityModel_2024_04_15[];
|
||||
}
|
||||
|
||||
export class UpdatedScheduleOutput_2024_04_15 {
|
||||
@ValidateNested()
|
||||
@Type(() => ScheduleModel_2024_04_15)
|
||||
@ApiProperty({ type: ScheduleModel_2024_04_15 })
|
||||
schedule!: ScheduleModel_2024_04_15;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isDefault!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
timeZone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
prevDefaultId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
currentDefaultId?: number | null;
|
||||
}
|
||||
|
||||
@@ -1,105 +1,132 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsDate, IsOptional, IsArray, IsBoolean, IsInt, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
class AvailabilityModel {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
userId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
eventTypeId?: number | null;
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({ type: [Number] })
|
||||
days!: number[];
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiProperty({ type: Date })
|
||||
startTime!: Date;
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiProperty({ type: Date })
|
||||
endTime!: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiPropertyOptional({ type: Date, nullable: true })
|
||||
date?: Date | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
scheduleId?: number | null;
|
||||
}
|
||||
|
||||
class WorkingHours {
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({ type: [Number] })
|
||||
days!: number[];
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
startTime!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
endTime!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
userId?: number | null;
|
||||
}
|
||||
|
||||
class TimeRange {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
userId?: number | null;
|
||||
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
start!: Date;
|
||||
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
end!: Date;
|
||||
}
|
||||
|
||||
export class ScheduleOutput {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isManaged!: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkingHours)
|
||||
@ApiProperty({ type: [WorkingHours] })
|
||||
workingHours!: WorkingHours[];
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AvailabilityModel)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [AvailabilityModel] })
|
||||
schedule!: AvailabilityModel[];
|
||||
|
||||
@ApiProperty({ type: [[TimeRange]] })
|
||||
availability!: TimeRange[][];
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
// note(Lauris) it should be
|
||||
// dateOverrides!: { ranges: TimeRange[] }[];
|
||||
// but docs aren't generating correctly it results in array of strings
|
||||
dateOverrides!: unknown[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isDefault!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isLastSchedule!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
readOnly!: boolean;
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export class OutputSchedulesService_2024_06_11 {
|
||||
|
||||
async getResponseSchedule(databaseSchedule: Schedule & { availability: Availability[] }) {
|
||||
if (!databaseSchedule.timeZone) {
|
||||
throw new Error("Failed to create schedule because its timezone is not set.");
|
||||
databaseSchedule.timeZone = "Europe/London";
|
||||
}
|
||||
|
||||
const ownerDefaultScheduleId = await this.usersRepository.getUserScheduleDefaultId(
|
||||
|
||||
@@ -30,12 +30,14 @@ export type Environment = {
|
||||
RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS: number;
|
||||
AXIOM_DATASET: string;
|
||||
AXIOM_TOKEN: string;
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID: string;
|
||||
IS_TEAM_BILLING_ENABLED: boolean;
|
||||
};
|
||||
|
||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||
const value = process.env[key] as Environment[K] | undefined;
|
||||
|
||||
if (!value) {
|
||||
if (value === undefined) {
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
} from "@prisma/client/runtime/library";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||
import {
|
||||
BAD_REQUEST,
|
||||
CONFLICT,
|
||||
ERROR_STATUS,
|
||||
INTERNAL_SERVER_ERROR,
|
||||
NOT_FOUND,
|
||||
} from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
|
||||
type PrismaError =
|
||||
@@ -43,11 +49,37 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
||||
|
||||
let message = "There was an error, please try again later.";
|
||||
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let errorCode = INTERNAL_SERVER_ERROR;
|
||||
if (error instanceof PrismaClientKnownRequestError) {
|
||||
switch (error.code) {
|
||||
case "P2002": // Unique constraint failed
|
||||
errorCode = CONFLICT;
|
||||
statusCode = HttpStatus.CONFLICT;
|
||||
message = "Invalid Input: Trying to create a record that already exists.";
|
||||
break;
|
||||
case "P2025": // Record not found
|
||||
errorCode = NOT_FOUND;
|
||||
statusCode = HttpStatus.NOT_FOUND;
|
||||
message = "Invalid Query: The requested record was not found.";
|
||||
break;
|
||||
case "P2003": // Foreign key constraint failed
|
||||
errorCode = BAD_REQUEST;
|
||||
statusCode = HttpStatus.BAD_REQUEST;
|
||||
message = "Invalid input: The referenced data does not exist.";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.status(statusCode).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
error: { code: INTERNAL_SERVER_ERROR, message: "There was an error, please try again later." },
|
||||
error: { code: errorCode, message: message },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
|
||||
describe("isOriginAllowed", () => {
|
||||
describe("is allowed", () => {
|
||||
it("should return true for exact match without wildcard", () => {
|
||||
const allowedOrigins = ["https://app.cal.com/callback"];
|
||||
const origin = "https://app.cal.com/callback";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for wildcard domain match", () => {
|
||||
const allowedOrigins = ["*.multiscreen.d1test.biz"];
|
||||
const origin = "https://sub.multiscreen.d1test.biz";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for wildcard pattern matching any domain", () => {
|
||||
const allowedOrigins = ["*/callback"];
|
||||
const origin = "https://another.com/callback";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle a wildcard only pattern", () => {
|
||||
const allowedOrigins = ["*"];
|
||||
const origin = "https://any.domain.com/anypath";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle multiple allowed URIs where only one matches", () => {
|
||||
const allowedOrigins = [
|
||||
"https://app.cal.com/other",
|
||||
"*.notthisone.com",
|
||||
"https://app.cal.com/callback",
|
||||
];
|
||||
const origin = "https://app.cal.com/callback";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle patterns with multiple wildcards correctly", () => {
|
||||
const allowedOrigins = ["https://*.mydomain.com/*"];
|
||||
const origin = "https://sub.mydomain.com/path/to/resource";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("is not allowed", () => {
|
||||
it("should return false if no allowed patterns match", () => {
|
||||
const allowedOrigins = ["https://app.cal.com/callback", "*.multiscreen.d1test.biz", "*/callback"];
|
||||
const origin = "https://unknown.com";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if wildcard pattern doesn't match the given origin", () => {
|
||||
const allowedOrigins = ["*.example.com"];
|
||||
const origin = "https://notexample.org";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle empty allowedUris array correctly", () => {
|
||||
const allowedOrigins: string[] = [];
|
||||
const origin = "https://app.cal.com/callback";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
export function isOriginAllowed(origin: string, allowedOrigins: string[]): boolean {
|
||||
return allowedOrigins.some((allowedOrigin) => {
|
||||
if (allowedOrigin.includes("*")) {
|
||||
return wildcardToRegex(allowedOrigin).test(origin);
|
||||
}
|
||||
return origin === allowedOrigin;
|
||||
});
|
||||
}
|
||||
|
||||
function wildcardToRegex(pattern: string): RegExp {
|
||||
const escaped = escapeRegex(pattern);
|
||||
const regexPattern = "^" + escaped.replace(/\\\*/g, ".*") + "$";
|
||||
return new RegExp(regexPattern);
|
||||
}
|
||||
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export const defaultBookingResponses = {
|
||||
name: "unknown",
|
||||
email: "unknown",
|
||||
guests: [],
|
||||
rescheduleReason: "unknown",
|
||||
};
|
||||
|
||||
export const defaultBookingMetadata = { videoCallUrl: "unknown" };
|
||||
|
||||
export const defaultSeatedBookingData = { responses: { name: "unknown", email: "unknown" } };
|
||||
|
||||
export const defaultSeatedBookingMetadata = {};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ZodSchema } from "zod";
|
||||
|
||||
const logger = new Logger("safeParse");
|
||||
|
||||
export function safeParse<T>(schema: ZodSchema<T>, value: unknown, defaultValue: T): T {
|
||||
const result = schema.safeParse(value);
|
||||
if (result.success) {
|
||||
return result.data;
|
||||
} else {
|
||||
const errorStack = new Error().stack;
|
||||
|
||||
logger.error(
|
||||
`Zod parsing failed.\n` +
|
||||
`1. Schema: ${schema.description || "UnnamedSchema"}\n` +
|
||||
`2. Input: ${JSON.stringify(value, null, 2)}\n` +
|
||||
`3. Zod Error: ${result.error}\n` +
|
||||
`4. Call Stack: ${errorStack}`
|
||||
);
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,13 @@ import { CredentialsRepository } from "@/modules/credentials/credentials.reposit
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule],
|
||||
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule, TeamsEventTypesModule],
|
||||
providers: [
|
||||
EventTypesAtomService,
|
||||
ConferencingAtomsService,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/platform-libraries";
|
||||
import { paymentDataSelect } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
@@ -14,4 +15,53 @@ export class AtomsRepository {
|
||||
select: paymentDataSelect,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserTeams(userId: number) {
|
||||
const userTeams = await this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return userTeams;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { ArrayNotEmpty, IsArray, IsInt, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
export class EventTypesAppInput {
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
@@ -12,5 +14,6 @@ export class BulkUpdateEventTypeToDefaultLocationDto {
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({ type: [Number] })
|
||||
eventTypeIds!: number[];
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_
|
||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
@@ -46,12 +45,11 @@ export class EventTypesAtomService {
|
||||
private readonly membershipsRepository: MembershipsRepository,
|
||||
private readonly credentialsRepository: CredentialsRepository,
|
||||
private readonly atomsRepository: AtomsRepository,
|
||||
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly eventTypeService: EventTypesService_2024_06_14,
|
||||
private readonly teamEventTypeService: OrganizationsEventTypesService
|
||||
private readonly teamEventTypeService: TeamsEventTypesService
|
||||
) {}
|
||||
|
||||
async getUserEventType(user: UserWithProfile, eventTypeId: number) {
|
||||
@@ -183,7 +181,7 @@ export class EventTypesAtomService {
|
||||
let credentials = await this.credentialsRepository.getAllUserCredentialsById(userId);
|
||||
let userTeams: TeamQuery[] = [];
|
||||
if (teamId) {
|
||||
const teamsQuery = await this.organizationsTeamsRepository.getUserTeamsById(userId);
|
||||
const teamsQuery = await this.atomsRepository.getUserTeams(userId);
|
||||
// If a team is a part of an org then include those apps
|
||||
// Don't want to iterate over these parent teams
|
||||
const filteredTeams: TeamQuery[] = [];
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { ExecutionContext } from "@nestjs/common";
|
||||
import { createParamDecorator } from "@nestjs/common";
|
||||
|
||||
export type GetUserReturnType = UserWithProfile & { isSystemAdmin: boolean };
|
||||
|
||||
export const GetUser = createParamDecorator<
|
||||
keyof GetUserReturnType | (keyof GetUserReturnType)[],
|
||||
keyof ApiAuthGuardUser | (keyof ApiAuthGuardUser)[],
|
||||
ExecutionContext
|
||||
>((data, ctx) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user as GetUserReturnType;
|
||||
const user = request.user as ApiAuthGuardUser;
|
||||
|
||||
if (!user) {
|
||||
throw new Error("GetUser decorator : User not found");
|
||||
}
|
||||
|
||||
user.isSystemAdmin = user.role === "ADMIN";
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return data.reduce((prev, curr) => {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { PlatformPlanType } from "@/modules/billing/types";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
@@ -19,7 +19,7 @@ export class PlatformPlanGuard implements CanActivate {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const teamId = request.params.teamId as string;
|
||||
const orgId = request.params.orgId as string;
|
||||
const user = request.user as GetUserReturnType;
|
||||
const user = request.user as ApiAuthGuardUser;
|
||||
const minimumPlan = this.reflector.get(PlatformPlan, context.getHandler()) as PlatformPlanType;
|
||||
|
||||
const REDIS_CACHE_KEY = `apiv2:user:${user?.id ?? "none"}:org:${orgId ?? "none"}:team:${
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ORG_ROLES, TEAM_ROLES, SYSTEM_ADMIN_ROLE } from "@/lib/roles/constants";
|
||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException, Logger } from "@nestjs/common";
|
||||
@@ -22,7 +22,7 @@ export class RolesGuard implements CanActivate {
|
||||
const request = context.switchToHttp().getRequest<Request & { team: Team }>();
|
||||
const teamId = request.params.teamId as string;
|
||||
const orgId = request.params.orgId as string;
|
||||
const user = request.user as GetUserReturnType;
|
||||
const user = request.user as ApiAuthGuardUser;
|
||||
const allowedRole = this.reflector.get(Roles, context.getHandler());
|
||||
const REDIS_CACHE_KEY = `apiv2:user:${user.id ?? "none"}:org:${orgId ?? "none"}:team:${
|
||||
teamId ?? "none"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
@@ -16,6 +17,8 @@ import { getToken } from "next-auth/jwt";
|
||||
|
||||
import { INVALID_ACCESS_TOKEN, X_CAL_CLIENT_ID, X_CAL_SECRET_KEY } from "@calcom/platform-constants";
|
||||
|
||||
export type ApiAuthGuardUser = UserWithProfile & { isSystemAdmin: boolean };
|
||||
|
||||
@Injectable()
|
||||
export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth") {
|
||||
constructor(
|
||||
@@ -74,12 +77,19 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
|
||||
async authenticateNextAuth(token: { email?: string | null }) {
|
||||
const user = await this.nextAuthStrategy(token);
|
||||
return this.success(user);
|
||||
return this.success(this.getSuccessUser(user));
|
||||
}
|
||||
|
||||
getSuccessUser(user: UserWithProfile): ApiAuthGuardUser {
|
||||
return {
|
||||
...user,
|
||||
isSystemAdmin: user.role === "ADMIN",
|
||||
};
|
||||
}
|
||||
|
||||
async authenticateOAuthClient(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
const user = await this.oAuthClientStrategy(oAuthClientId, oAuthClientSecret);
|
||||
return this.success(user);
|
||||
return this.success(this.getSuccessUser(user));
|
||||
}
|
||||
|
||||
async oAuthClientStrategy(oAuthClientId: string, oAuthClientSecret: string) {
|
||||
@@ -118,7 +128,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
return this.error(new UnauthorizedException("No user associated with the provided token"));
|
||||
}
|
||||
|
||||
return this.success(user);
|
||||
return this.success(this.getSuccessUser(user));
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
return this.error(err);
|
||||
@@ -167,7 +177,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
throw new UnauthorizedException("OAuth client not found given the access token");
|
||||
}
|
||||
|
||||
if (origin && !client.redirectUris.some((uri) => uri.startsWith(origin))) {
|
||||
if (origin && !isOriginAllowed(origin, client.redirectUris)) {
|
||||
throw new UnauthorizedException(
|
||||
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client.`
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ConferencingController } from "@/modules/conferencing/controllers/confe
|
||||
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.respository";
|
||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
|
||||
import { Office365VideoService } from "@/modules/conferencing/services/office365-video.service";
|
||||
import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -21,6 +22,7 @@ import { ConfigModule } from "@nestjs/config";
|
||||
UsersRepository,
|
||||
TokensRepository,
|
||||
ZoomVideoService,
|
||||
Office365VideoService,
|
||||
AppsRepository,
|
||||
],
|
||||
exports: [],
|
||||
|
||||
@@ -15,6 +15,7 @@ import { GetDefaultConferencingAppOutputResponseDto } from "@/modules/conferenci
|
||||
import { SetDefaultConferencingAppOutputResponseDto } from "@/modules/conferencing/outputs/set-default-conferencing-app.output";
|
||||
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
|
||||
import { GoogleMeetService } from "@/modules/conferencing/services/google-meet.service";
|
||||
import { Office365VideoService } from "@/modules/conferencing/services/office365-video.service";
|
||||
import { ZoomVideoService } from "@/modules/conferencing/services/zoom-video.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
@@ -39,7 +40,7 @@ import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { Request } from "express";
|
||||
|
||||
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO } from "@calcom/platform-constants";
|
||||
|
||||
export type OAuthCallbackState = {
|
||||
accessToken: string;
|
||||
@@ -61,7 +62,8 @@ export class ConferencingController {
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly conferencingService: ConferencingService,
|
||||
private readonly googleMeetService: GoogleMeetService,
|
||||
private readonly zoomVideoService: ZoomVideoService
|
||||
private readonly zoomVideoService: ZoomVideoService,
|
||||
private readonly office365VideoService: Office365VideoService
|
||||
) {}
|
||||
|
||||
@Post("/:app/connect")
|
||||
@@ -116,8 +118,18 @@ export class ConferencingController {
|
||||
data: plainToInstance(ConferencingAppsOauthUrlOutputDto, credential),
|
||||
};
|
||||
|
||||
case OFFICE_365_VIDEO:
|
||||
credential = await this.office365VideoService.generateOffice365AuthUrl(JSON.stringify(state));
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(ConferencingAppsOauthUrlOutputDto, credential),
|
||||
};
|
||||
|
||||
default:
|
||||
throw new BadRequestException("Invalid conferencing app, available apps are: ", [ZOOM].join(", "));
|
||||
throw new BadRequestException(
|
||||
"Invalid conferencing app, available apps are: ",
|
||||
[ZOOM, OFFICE_365_VIDEO].join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,8 +159,14 @@ export class ConferencingController {
|
||||
case ZOOM:
|
||||
return await this.zoomVideoService.connectZoomApp(decodedCallbackState, code, userId);
|
||||
|
||||
case OFFICE_365_VIDEO:
|
||||
return await this.office365VideoService.connectOffice365App(decodedCallbackState, code, userId);
|
||||
|
||||
default:
|
||||
throw new BadRequestException("Invalid conferencing app, available apps are: ", [ZOOM].join(", "));
|
||||
throw new BadRequestException(
|
||||
"Invalid conferencing app, available apps are: ",
|
||||
[ZOOM, OFFICE_365_VIDEO].join(", ")
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, ValidateNested, IsEnum, IsNumber, IsOptional, IsBoolean } from "class-validator";
|
||||
|
||||
@@ -20,7 +20,11 @@ export class ConferencingAppsOutputDto {
|
||||
@IsNumber()
|
||||
userId!: number;
|
||||
|
||||
@ApiProperty({ example: true, description: "Whether if the connection is working or not." })
|
||||
@ApiPropertyOptional({
|
||||
example: true,
|
||||
description: "Whether if the connection is working or not.",
|
||||
nullable: true,
|
||||
})
|
||||
@Expose()
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@@ -28,24 +32,26 @@ export class ConferencingAppsOutputDto {
|
||||
}
|
||||
|
||||
export class ConferencingAppsOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@ApiProperty({ type: String, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ConferencingAppsOutputDto)
|
||||
@ApiProperty({ type: [ConferencingAppsOutputDto] })
|
||||
data!: ConferencingAppsOutputDto[];
|
||||
}
|
||||
|
||||
export class ConferencingAppOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@ApiProperty({ type: String, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => ConferencingAppsOutputDto)
|
||||
@ApiProperty({ type: ConferencingAppsOutputDto })
|
||||
data!: ConferencingAppsOutputDto;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { OAuthCallbackState } from "@/modules/conferencing/controllers/conferencing.controller";
|
||||
import { BadRequestException, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OFFICE_365_VIDEO, OFFICE_365_VIDEO_TYPE } from "@calcom/platform-constants";
|
||||
|
||||
import stringify = require("qs-stringify");
|
||||
|
||||
const zoomAppKeysSchema = z.object({
|
||||
client_id: z.string(),
|
||||
client_secret: z.string(),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class Office365VideoService {
|
||||
private logger = new Logger("Office365VideoService");
|
||||
private redirectUri = `${this.config.get("api.url")}/conferencing/${OFFICE_365_VIDEO}/oauth/callback`;
|
||||
private scopes = ["OnlineMeetings.ReadWrite", "offline_access"];
|
||||
|
||||
constructor(private readonly config: ConfigService, private readonly appsRepository: AppsRepository) {}
|
||||
|
||||
async getOffice365AppKeys() {
|
||||
const app = await this.appsRepository.getAppBySlug(OFFICE_365_VIDEO);
|
||||
|
||||
const { client_id, client_secret } = zoomAppKeysSchema.parse(app?.keys);
|
||||
|
||||
if (!client_id) {
|
||||
throw new NotFoundException("Office365 app not found");
|
||||
}
|
||||
|
||||
if (!client_secret) {
|
||||
throw new NotFoundException("Office365 app not found");
|
||||
}
|
||||
|
||||
return { client_id, client_secret };
|
||||
}
|
||||
|
||||
async generateOffice365AuthUrl(state: string) {
|
||||
const { client_id } = await this.getOffice365AppKeys();
|
||||
|
||||
const params = {
|
||||
response_type: "code",
|
||||
client_id,
|
||||
scope: this.scopes.join(" "),
|
||||
redirect_uri: this.redirectUri,
|
||||
state: state,
|
||||
};
|
||||
|
||||
const query = stringify(params);
|
||||
|
||||
const url = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${query}`;
|
||||
return { url };
|
||||
}
|
||||
|
||||
async connectOffice365App(state: OAuthCallbackState, code: string, userId: number) {
|
||||
const { client_id, client_secret } = await this.getOffice365AppKeys();
|
||||
|
||||
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||
Object.keys(payload)
|
||||
.map((key) => `${key}=${encodeURIComponent(payload[key])}`)
|
||||
.join("&");
|
||||
|
||||
const body = toUrlEncoded({
|
||||
client_id,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
scope: this.scopes.join(" "),
|
||||
redirect_uri: this.redirectUri,
|
||||
client_secret,
|
||||
});
|
||||
|
||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
const responseBody = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new BadRequestException(responseBody.error);
|
||||
}
|
||||
|
||||
const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
|
||||
headers: { Authorization: `Bearer ${responseBody.access_token}` },
|
||||
});
|
||||
|
||||
const graphUser = await whoami.json();
|
||||
|
||||
// In some cases, graphUser.mail is null. Then graphUser.userPrincipalName most likely contains the email address.
|
||||
responseBody.email = graphUser.mail ?? graphUser.userPrincipalName;
|
||||
responseBody.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in); // set expiry date in seconds
|
||||
delete responseBody.expires_in;
|
||||
|
||||
const existingCredentialOffice365Video = await this.appsRepository.findAppCredential({
|
||||
type: OFFICE_365_VIDEO_TYPE,
|
||||
userId,
|
||||
appId: OFFICE_365_VIDEO,
|
||||
});
|
||||
|
||||
const credentialIdsToDelete = existingCredentialOffice365Video.map((item) => item.id);
|
||||
if (credentialIdsToDelete.length > 0) {
|
||||
await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);
|
||||
}
|
||||
|
||||
await this.appsRepository.createAppCredential(
|
||||
OFFICE_365_VIDEO_TYPE,
|
||||
responseBody as unknown as Prisma.InputJsonObject,
|
||||
userId,
|
||||
OFFICE_365_VIDEO
|
||||
);
|
||||
|
||||
return { url: state.returnTo ?? "" };
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const zoomAppKeysSchema = z.object({
|
||||
@Injectable()
|
||||
export class ZoomVideoService {
|
||||
private logger = new Logger("ZoomVideoService");
|
||||
private redirectUri = `${this.config.get("api.url")}/conferencing/zoom/oauth/callback`;
|
||||
private redirectUri = `${this.config.get("api.url")}/conferencing/${ZOOM}/oauth/callback`;
|
||||
|
||||
constructor(private readonly config: ConfigService, private readonly appsRepository: AppsRepository) {}
|
||||
|
||||
|
||||
@@ -33,6 +33,16 @@ export class MembershipsRepository {
|
||||
return membership;
|
||||
}
|
||||
|
||||
async findUserMemberships(userId: number) {
|
||||
const memberships = await this.dbRead.prisma.membership.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
return memberships;
|
||||
}
|
||||
|
||||
async findMembershipByOrgId(orgId: number, userId: number) {
|
||||
return this.findMembershipByTeamId(orgId, userId);
|
||||
}
|
||||
|
||||
+7
-7
@@ -10,10 +10,10 @@ export class ManagedUserOutput {
|
||||
@ApiProperty({ example: "alice+cluo37fwd0001khkzqqynkpj3@example.com" })
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({ example: "alice" })
|
||||
@ApiProperty({ example: "alice", nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@ApiProperty({ example: "alice" })
|
||||
@ApiProperty({ example: "alice", nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@ApiProperty({ example: "America/New_York" })
|
||||
@@ -22,19 +22,19 @@ export class ManagedUserOutput {
|
||||
@ApiProperty({ example: "Sunday" })
|
||||
weekStart!: string;
|
||||
|
||||
@ApiProperty({ example: "2024-04-01T00:00:00.000Z", type: "string" })
|
||||
@ApiProperty({ type: String, example: "2024-04-01T00:00:00.000Z" })
|
||||
@Transform(({ value }) => value.toISOString())
|
||||
createdDate!: Date;
|
||||
|
||||
@ApiProperty({ example: 12, nullable: true })
|
||||
@ApiProperty({ type: Number, example: 12, nullable: true })
|
||||
timeFormat!: number | null;
|
||||
|
||||
@ApiProperty({ example: null })
|
||||
@ApiProperty({ type: Number, example: null, nullable: true })
|
||||
defaultScheduleId!: number | null;
|
||||
|
||||
@IsEnum(Locales)
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: Locales.EN, enum: Locales })
|
||||
@ApiPropertyOptional({ type: String, example: Locales.EN, enum: Locales })
|
||||
locale?: Locales;
|
||||
|
||||
@IsUrl()
|
||||
@@ -43,7 +43,7 @@ export class ManagedUserOutput {
|
||||
type: String,
|
||||
example: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
description: `URL of the user's avatar image`,
|
||||
required: false,
|
||||
nullable: true,
|
||||
})
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
@@ -30,7 +30,7 @@ export class PlatformOAuthClientDto {
|
||||
@IsNumber()
|
||||
permissions!: number;
|
||||
|
||||
@ApiProperty({ example: "https://example.com/logo.png", required: false })
|
||||
@ApiPropertyOptional({ example: "https://example.com/logo.png" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
logo!: string | null;
|
||||
@@ -54,9 +54,7 @@ export class GetOAuthClientResponseDto {
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: PlatformOAuthClientDto,
|
||||
})
|
||||
@ApiProperty({ type: PlatformOAuthClientDto })
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => PlatformOAuthClientDto)
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { getEnv } from "@/env";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
@@ -62,7 +63,7 @@ export class OAuthFlowController {
|
||||
throw new BadRequestException(`OAuth client with ID '${clientId}' not found`);
|
||||
}
|
||||
|
||||
if (!oauthClient?.redirectUris.includes(body.redirectUri)) {
|
||||
if (!isOriginAllowed(body.redirectUri, oauthClient.redirectUris)) {
|
||||
throw new BadRequestException("Invalid 'redirect_uri' value.");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import {
|
||||
@@ -15,8 +15,8 @@ export class OAuthClientGuard implements CanActivate {
|
||||
constructor(private oAuthClientRepository: OAuthClientRepository, private usersService: UsersService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request & { user: GetUserReturnType }>();
|
||||
const user: GetUserReturnType = request.user;
|
||||
const request = context.switchToHttp().getRequest<Request & { user: ApiAuthGuardUser }>();
|
||||
const user: ApiAuthGuardUser = request.user;
|
||||
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||
const oAuthClientId = request.params.clientId;
|
||||
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsArray, IsBoolean, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class UpdateOAuthClientInput {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
logo?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
redirectUris?: string[] = [];
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
redirectUris?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
bookingRedirectUri?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
bookingCancelRedirectUri?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
bookingRescheduleRedirectUri?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
areEmailsEnabled?: boolean;
|
||||
}
|
||||
|
||||
+10
-8
@@ -1,6 +1,5 @@
|
||||
import { CreatePhoneCallInput } from "@/ee/event-types/event-types_2024_06_14/inputs/create-phone-call.input";
|
||||
import { CreatePhoneCallOutput } from "@/ee/event-types/event-types_2024_06_14/outputs/create-phone-call.output";
|
||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
@@ -11,15 +10,15 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { CreateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/create-team-event-type.output";
|
||||
import { DeleteTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/delete-team-event-type.output";
|
||||
import { GetTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-type.output";
|
||||
import { GetTeamEventTypesOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/get-team-event-types.output";
|
||||
import { UpdateTeamEventTypeOutput } from "@/modules/organizations/controllers/event-types/outputs/teams/update-team-event-type.output";
|
||||
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/controllers/pipes/event-types/team-event-types-response.transformer";
|
||||
import { InputOrganizationsEventTypesService } from "@/modules/organizations/services/event-types/input.service";
|
||||
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
|
||||
import { DatabaseTeamEventType } from "@/modules/organizations/services/event-types/output.service";
|
||||
import { CreateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/create-team-event-type.output";
|
||||
import { DeleteTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/delete-team-event-type.output";
|
||||
import { GetTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/get-team-event-type.output";
|
||||
import { GetTeamEventTypesOutput } from "@/modules/teams/event-types/outputs/get-team-event-types.output";
|
||||
import { UpdateTeamEventTypeOutput } from "@/modules/teams/event-types/outputs/update-team-event-type.output";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
@@ -62,7 +61,6 @@ export class OrganizationsEventTypesController {
|
||||
constructor(
|
||||
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
|
||||
private readonly inputService: InputOrganizationsEventTypesService,
|
||||
private readonly inputUserEventTypesService: InputEventTypesService_2024_06_14,
|
||||
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
|
||||
) {}
|
||||
|
||||
@@ -180,7 +178,11 @@ export class OrganizationsEventTypesController {
|
||||
@Query() queryParams: SkipTakePagination
|
||||
): Promise<GetTeamEventTypesOutput> {
|
||||
const { skip, take } = queryParams;
|
||||
const eventTypes = await this.organizationsEventTypesService.getTeamsEventTypes(orgId, skip, take);
|
||||
const eventTypes = await this.organizationsEventTypesService.getOrganizationsTeamsEventTypes(
|
||||
orgId,
|
||||
skip,
|
||||
take
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user