Skip to content

Commit

Permalink
Merge branch 'main' into fix/global-booking-limits
Browse files Browse the repository at this point in the history
  • Loading branch information
CarinaWolli committed Sep 20, 2024
2 parents f7de41a + c1c4b12 commit 910fff7
Show file tree
Hide file tree
Showing 300 changed files with 10,774 additions and 9,607 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -343,12 +343,14 @@ 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_WORKFLOWS_ENABLED=0
APP_ROUTER_SETTINGS_TEAMS_ENABLED=0
APP_ROUTER_GETTING_STARTED_STEP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_VIDEO_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
APP_ROUTER_TEAMS_ENABLED=0
APP_ROUTER_AVAILABILITY_ENABLED=0
APP_ROUTER_AUTH_FORGOT_PASSWORD_ENABLED=0
Expand Down Expand Up @@ -392,3 +394,7 @@ VAPID_PRIVATE_KEY=
# Custom privacy policy / terms URLs (for self-hosters: change to your privacy policy / terms URLs)
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=
NEXT_PUBLIC_WEBSITE_TERMS_URL=

# NEXT_PUBLIC_LOGGER_LEVEL=3 sets to log info, warn, error and fatal logs.
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
NEXT_PUBLIC_LOGGER_LEVEL=
6 changes: 3 additions & 3 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ jobs:
test:
name: Unit
timeout-minutes: 20
runs-on: buildjet-2vcpu-ubuntu-2204
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- run: yarn test
- run: yarn test -- --no-isolate
# We could add different timezones here that we need to run our tests in
- run: TZ=America/Los_Angeles yarn test -- --timeZoneDependentTestsOnly
- run: TZ=America/Los_Angeles yarn test -- --timeZoneDependentTestsOnly --no-isolate
- name: Run API v2 tests
working-directory: apps/api/v2
run: |
Expand Down
26 changes: 26 additions & 0 deletions .yarn/patches/@prisma-client-npm-5.4.2-fca489b2dc.patch

Large diffs are not rendered by default.

22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,29 @@ yarn dx

#### Development tip

> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **tRPC**.
Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
Where {level} can be one of the following:

`0` for silly \
`1` for trace \
`2` for debug \
`3` for info \
`4` for warn \
`5` for error \
`6` for fatal

When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:

The logger will include all logs that are at the specified level or higher. For example: \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \



```sh
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
echo 'NEXT_PUBLIC_LOGGER_LEVEL=3' >> .env
```
for Logger level to be set at info, for example.

#### Gitpod Setup

Expand Down
26 changes: 25 additions & 1 deletion apps/api/v1/lib/validations/booking.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { z } from "zod";

import { _AttendeeModel, _BookingModel as Booking, _PaymentModel, _UserModel } from "@calcom/prisma/zod";
import {
_AttendeeModel,
_BookingModel as Booking,
_EventTypeModel,
_PaymentModel,
_TeamModel,
_UserModel,
} from "@calcom/prisma/zod";
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";

import { schemaQueryUserId } from "./shared/queryUserId";
Expand Down Expand Up @@ -46,7 +53,23 @@ export const schemaBookingEditBodyParams = schemaBookingBaseBodyParams
.merge(schemaBookingEditParams)
.omit({ uid: true });

const teamSchema = _TeamModel.pick({
name: true,
slug: true,
});

export const schemaBookingReadPublic = Booking.extend({
eventType: _EventTypeModel
.pick({
title: true,
slug: true,
})
.merge(
z.object({
team: teamSchema.nullish(),
})
)
.nullish(),
attendees: z
.array(
_AttendeeModel.pick({
Expand Down Expand Up @@ -87,6 +110,7 @@ export const schemaBookingReadPublic = Booking.extend({
timeZone: true,
attendees: true,
user: true,
eventType: true,
payment: true,
metadata: true,
status: true,
Expand Down
12 changes: 12 additions & 0 deletions apps/api/v1/lib/validations/shared/queryExpandRelations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from "zod";

const expandEnum = z.enum(["team"]);

export const schemaQuerySingleOrMultipleExpand = z
.union([
expandEnum, // Allow a single value from the enum
z.array(expandEnum).refine((arr) => new Set(arr).size === arr.length, {
message: "Array values must be unique",
}), // Allow an array of enum values, with uniqueness constraint
])
.optional();
15 changes: 14 additions & 1 deletion apps/api/v1/pages/api/bookings/[id]/_get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";

import { schemaBookingReadPublic } from "~/lib/validations/booking";
import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations";
import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt";

/**
Expand Down Expand Up @@ -89,9 +90,21 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
export async function getHandler(req: NextApiRequest) {
const { query } = req;
const { id } = schemaQueryIdParseInt.parse(query);

const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand);
const expand = Array.isArray(queryFilterForExpand)
? queryFilterForExpand
: queryFilterForExpand
? [queryFilterForExpand]
: [];
const booking = await prisma.booking.findUnique({
where: { id },
include: { attendees: true, user: true, payment: true },
include: {
attendees: true,
user: true,
payment: true,
eventType: expand.includes("team") ? { include: { team: true } } : false,
},
});
return { booking: schemaBookingReadPublic.parse(booking) };
}
Expand Down
9 changes: 9 additions & 0 deletions apps/api/v1/pages/api/bookings/_get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "~/lib/utils/retrieveScopedAccessibleUsers";
import { schemaBookingGetParams, schemaBookingReadPublic } from "~/lib/validations/booking";
import { schemaQuerySingleOrMultipleAttendeeEmails } from "~/lib/validations/shared/queryAttendeeEmail";
import { schemaQuerySingleOrMultipleExpand } from "~/lib/validations/shared/queryExpandRelations";
import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/queryUserId";

/**
Expand Down Expand Up @@ -216,10 +217,18 @@ export async function handler(req: NextApiRequest) {
args.take = take;
args.skip = skip;
}
const queryFilterForExpand = schemaQuerySingleOrMultipleExpand.parse(req.query.expand);
const expand = Array.isArray(queryFilterForExpand)
? queryFilterForExpand
: queryFilterForExpand
? [queryFilterForExpand]
: [];

args.include = {
attendees: true,
user: true,
payment: true,
eventType: expand.includes("team") ? { include: { team: true } } : false,
};

const queryFilterForAttendeeEmails = schemaQuerySingleOrMultipleAttendeeEmails.parse(req.query);
Expand Down
24 changes: 23 additions & 1 deletion apps/api/v1/test/lib/bookings/_get.integration-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ describe("GET /api/bookings", async () => {

const responseData = await handler(req);
responseData.bookings.forEach((booking) => {
console.log(booking);
expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime());
});
});
Expand All @@ -188,4 +187,27 @@ describe("GET /api/bookings", async () => {
});
});
});

describe("Expand feature to add relational data in return payload", () => {
it("Returns only team data when expand=team is set", async () => {
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "[email protected]" } });
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
method: "GET",
query: {
expand: "team",
},
pagination: DefaultPagination,
});

req.userId = adminUser.id;
req.isOrganizationOwnerOrAdmin = true;

const responseData = await handler(req);
console.log("bookings=>", responseData.bookings);
responseData.bookings.forEach((booking) => {
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
if (booking.id === 19) expect(booking.eventType?.team).toBe(null);
});
});
});
});
2 changes: 1 addition & 1 deletion apps/api/v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/[email protected].34",
"@calcom/platform-libraries": "npm:@calcom/[email protected].36",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/[email protected]",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
Expand Down
6 changes: 4 additions & 2 deletions apps/api/v2/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import appConfig from "@/config/app";
import { CustomThrottlerGuard } from "@/lib/throttler-guard";
import { AppLoggerMiddleware } from "@/middleware/app.logger.middleware";
import { RewriterMiddleware } from "@/middleware/app.rewrites.middleware";
import { JsonBodyMiddleware } from "@/middleware/body/json.body.middleware";
Expand All @@ -15,7 +16,7 @@ import { BullModule } from "@nestjs/bull";
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
import { seconds, ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { seconds, ThrottlerModule } from "@nestjs/throttler";
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";

import { AppController } from "./app.controller";
Expand All @@ -32,6 +33,7 @@ import { AppController } from "./app.controller";
BullModule.forRoot({
redis: `${process.env.REDIS_URL}${process.env.NODE_ENV === "production" ? "?tls=true" : ""}`,
}),
// Rate limiting here is handled by the CustomThrottlerGuard
ThrottlerModule.forRootAsync({
imports: [RedisModule],
inject: [RedisService],
Expand Down Expand Up @@ -59,7 +61,7 @@ import { AppController } from "./app.controller";
},
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
useClass: CustomThrottlerGuard,
},
],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
InternalServerErrorException,
ParseIntPipe,
} from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";

import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import { getPublicEvent, getEventTypesByViewer } from "@calcom/platform-libraries-0.0.2";
Expand All @@ -47,7 +47,7 @@ import { PrismaClient } from "@calcom/prisma";
version: [VERSION_2024_04_15, VERSION_2024_06_11],
})
@UseGuards(PermissionsGuard)
@DocsTags("Event types")
@DocsExcludeController(true)
export class EventTypesController_2024_04_15 {
constructor(
private readonly eventTypesService: EventTypesService_2024_04_15,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
updateEventType,
EventTypesPublic,
getEventTypesPublic,
systemBeforeFieldEmail,
} from "@calcom/platform-libraries";
import { EventType } from "@calcom/prisma/client";

Expand Down Expand Up @@ -127,8 +128,17 @@ export class EventTypesService_2024_04_15 {
async updateEventType(eventTypeId: number, body: UpdateEventTypeInput_2024_04_15, user: UserWithProfile) {
this.checkCanUpdateEventType(user.id, eventTypeId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];

if (
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
bookingFields.push(systemBeforeFieldEmail);
}

await updateEventType({
input: { id: eventTypeId, ...body },
input: { id: eventTypeId, ...body, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,6 @@ describe("Event types Endpoints", () => {
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.locations).toEqual(body.locations);
expect(createdEventType.bookingFields).toEqual(body.bookingFields);
expect(createdEventType.ownerId).toEqual(user.id);
expect(createdEventType.scheduleId).toEqual(firstSchedule.id);
expect(createdEventType.bookingLimitsCount).toEqual(body.bookingLimitsCount);
Expand All @@ -259,6 +258,16 @@ describe("Event types Endpoints", () => {
expect(createdEventType.offsetStart).toEqual(body.offsetStart);
expect(createdEventType.bookingWindow).toEqual(body.bookingWindow);
expect(createdEventType.recurrence).toEqual(body.recurrence);

const responseBookingFields = body.bookingFields || [];
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", type: "name" },
{ isDefault: true, required: true, slug: "email", type: "email" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
...responseBookingFields.map((field) => ({ isDefault: false, ...field })),
];

expect(createdEventType.bookingFields).toEqual(expectedBookingFields);
eventType = responseBody.data;
});
});
Expand Down Expand Up @@ -476,6 +485,16 @@ describe("Event types Endpoints", () => {
let legacyEventTypeId1: number;
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" },
];

beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Expand Down Expand Up @@ -545,7 +564,7 @@ describe("Event types Endpoints", () => {
.expect(400);
});

it("should return empty bookingFields if system fields are the only one in database", async () => {
it("should return system bookingFields stored in database", async () => {
const legacyEventTypeInput = {
title: "legacy event type",
description: "legacy event type description",
Expand Down Expand Up @@ -638,11 +657,11 @@ describe("Event types Endpoints", () => {
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const fetchedEventType = responseBody.data;
expect(fetchedEventType.bookingFields).toEqual([]);
expect(fetchedEventType.bookingFields).toEqual(expectedReturnSystemFields);
});
});

it("should return user created bookingFields among system fields in the database", async () => {
it("should return user created bookingFields with system fields", async () => {
const userDefinedBookingField = {
name: "team",
type: "textarea",
Expand Down Expand Up @@ -755,7 +774,9 @@ describe("Event types Endpoints", () => {
const fetchedEventType = responseBody.data;

expect(fetchedEventType.bookingFields).toEqual([
...expectedReturnSystemFields,
{
isDefault: false,
type: userDefinedBookingField.type,
slug: userDefinedBookingField.name,
label: userDefinedBookingField.label,
Expand Down
Loading

0 comments on commit 910fff7

Please sign in to comment.