Compare commits

...
Author SHA1 Message Date
github-actions[bot] aea6e105e6 chore: version packages (#23248)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-22 14:17:18 +00:00
a082cf73a0 fix: Manual RR reassign location change - Cal Video/Zoom/GoogleMeet to Zoom/Google Meet/Cal Video (#22840)
* wip

* fix tests

* fix webex OAUTH_URL for local testing as done in other apps

* fix type errors

* Improve check

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-08-22 17:52:22 +05:30
Lauris Skraucis 61268f36b9 chore: add debug logs (#23278) 2025-08-22 12:20:39 +00:00
99cd0c5c41 fix: Use Memberships with OWNER role for platform owner lookup (#22475)
* fix platform owner identification to use Memberships with OWNER role

* replaced the hardcoded OWNER string with the enum value

* replaced enum with hard coded OWNER value for safty

* added enum usage

* added admin lookout if owner validation fails

* chore

* chore

* fixed tests

* chore

* chore

* chore

* chore

---------

Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-08-22 14:14:28 +05:30
Eunjae LeeandDevin AI 2266301469 feat: implement system filter segments for data tables (#22939)
* feat: implement default filter segments for data tables

- Add defaultSegmentId column to UserFilterSegmentPreference table
- Support mixed segment ID types (number for user segments, string for default segments)
- Add DefaultFilterSegment and CombinedFilterSegment types
- Update useSegments hook to handle default segments with date range recalculation
- Modify FilterSegmentSelect to group and display default segments separately
- Add default segments to bookings view (My Bookings, Upcoming Bookings)
- Prevent editing/deleting of default segments in SaveFilterSegmentButton
- Update DataTableProvider to support defaultSegments prop
- Update parsers and repository to handle mixed segment ID types

Implements frontend-only default segments as specified in the requirements.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* chore: update ESLint dependencies to resolve configuration issues

- Update eslint-config-next and @typescript-eslint packages to latest versions
- Fix dependency compatibility issues that were blocking pre-commit hooks

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* revert eslint change

* some changes

* refactor: implement discriminated union for segment types

- Update SegmentIdentifier to use discriminated union with 'custom' vs 'default' types
- Refactor setSegmentId to accept object parameters: { id: string; type: 'default' } | { id: number; type: 'custom' }
- Update type definitions with DefaultFilterSegment, CustomFilterSegment, and CombinedFilterSegment
- Modify useSegments hook to handle new segment type structure
- Update FilterSegmentSelect component to work with discriminated unions
- Refactor database preference handling to store segment type alongside ID
- Add proper type safety throughout the data flow
- Remove description property from DefaultFilterSegment type

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: update unit test to expect discriminated union format for preferredSegmentId

The test was expecting preferredSegmentId to be a number, but after the discriminated union refactor it now returns { id: number, type: 'custom' } for custom segments. Updated the assertion to use toEqual() for deep object comparison.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: correct SegmentIdentifier type to exclude undefined

- Remove null from SegmentIdentifier type definition since null/undefined indicate absence of identifier
- Update DataTableProvider and useSegments to handle SegmentIdentifier | null properly
- Fix type safety while maintaining discriminated union functionality
- Ensure setAndPersistSegmentId accepts null values for clearing segments

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* remove unused segment

* a little update

* feat: add default- prefix to default segment IDs for clearer type identification

- Update 'my_bookings' to 'default-my_bookings'
- Update 'upcoming-bookings' to 'default-upcoming-bookings'
- Makes it easier to verify segment type by looking at the ID string
- Maintains all existing discriminated union functionality

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: rename segment types from default/custom to system/user for better semantics

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: update unit test to expect 'user' type instead of 'custom' in discriminated union

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: complete type renaming from default/custom to system/user across all component files

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: update segment ID prefix from default- to system- to match type naming

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* remove unused segment

* some fixes

* revert schema change

* rename defaultSegmentId to systemSegmentId

* renaming

* fix save button

* remove as any

* clean up prisma migrations

* type fixes

* many fixes

* remove icon property

* fix infinite rendering

* fix race condition

* re-visiting useSegments implementation WIP

* extract useElementByClassName

* add e2e tests

* fix a bug that the created segment was not selected automatically

* add useSegments to /insights and fix e2e test

* fix type error

* fix type error

* apply feedback

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-22 10:18:18 +03:00
Bandhan MajumderandKartik Saini 30b0012e6a fix: truncated droplist in reroute booking (#23263)
Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-08-22 04:32:21 +00:00
emrysal 77d1958c42 chore: release v5.6.4
Post release / release (push) Canceled after 0s
2025-08-22 01:49:37 +00:00
88f347f18c feat: improve unkey error handling with heavy logging for unknown errors (#23223)
* feat: improve unkey error handling with heavy logging for unknown errors

- Add onError handler to all Ratelimit instances to catch unknown service errors
- Log comprehensive error details including stack traces and identifiers
- Return fallback response allowing requests through instead of rejecting promises
- Preserve existing rate limit functionality for legitimate rate limit cases
- Maintain consistency with existing timeout fallback behavior

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Update rateLimit.ts

* chore: Bump @unkey/ratelimit version

* Add required props to rateLimiter onError response

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-08-21 23:37:01 +01:00
Somay ChauhanandDevin AI 09cf8885cf feat: add send & verify code flow to booker atom when email verification is turned on (#23074)
* feat: extract tRPC verification logic to platform libraries and create v2 atoms endpoints

- Extract verifyCodeUnAuthenticated, verifyCodeAuthenticated, and sendVerifyEmailCode logic from tRPC handlers into reusable platform library functions
- Create VerificationAtomsService and AtomsVerificationController following atoms pattern
- Add v2 endpoints: /atoms/verification/email/send-code, /atoms/verification/email/verify-code, /atoms/verification/email/verify-code-authenticated
- Update useVerifyEmail and useVerifyCode hooks to use new v2 endpoints instead of verified-resources endpoints
- Export verification functions from @calcom/platform-libraries following getPublicEvent pattern
- Integrate platform-specific verification hooks into BookerPlatformWrapper

Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com>

* feat: update platform hooks to use v2 verification endpoints

- Update useVerifyEmail hook to use correct response type for v2 endpoint
- Update useVerifyCode hook to use v2 verification endpoint
- All verification functions now properly exported from platform libraries
- Type checking passes with no errors

Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com>

* fix: update v2 API to use workspace version of platform-libraries

- Change package.json dependency from pinned version to workspace version
- Add proper error handling to verification service with NestJS HTTP exceptions
- Convert generic Error objects to BadRequestException and UnauthorizedException
- Ensure verification endpoints return proper HTTP status codes instead of 500 errors

Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com>

* chore: update auto-generated files after package.json changes

- Update OpenAPI documentation for v2 verification endpoints
- Update yarn.lock after changing platform-libraries dependency

Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com>

* refactor: extract verification logic from tRPC handlers into exportable functions

- Refactor sendVerifyEmailCode.handler.ts to extract core logic into sendVerifyEmailCode function
- Refactor verifyCodeUnAuthenticated.handler.ts to extract core logic into verifyCodeUnAuthenticated function
- Refactor organizations/verifyCode.handler.ts to extract core logic into verifyCodeAuthenticated function
- Update platform/libraries/index.ts to import from refactored handler files instead of standalone verification.ts
- Remove standalone verification.ts file as requested by user
- Keep original tRPC handlers as wrappers that call the extracted functions
- Maintain backward compatibility for existing tRPC endpoints

Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com>

* extract logic into seperate functions

* Update index.ts

* refactor: remove unused type imports from verification-atom service

* feat: create v2 atoms input/output types for verification endpoints

- Add SendVerificationEmailInput and VerifyEmailCodeInput with validation decorators
- Add SendVerificationEmailOutput and VerifyEmailCodeOutput following v2 atoms patterns
- Update atoms verification controller to use custom types instead of platform types
- Auto-update openapi.json with new type definitions

Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com>

* fix: add missing type imports to verification service

- Import ZVerifyCodeInputSchema from @calcom/prisma/zod-utils
- Import VerifyCodeAuthenticatedInput from organizations handler
- Import TSendVerifyEmailCodeSchema from sendVerifyEmailCode schema
- Resolves CI build failure in API v2 tests

Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com>

* feat: add email verification check endpoint and refactor verification flow

* added Throttle

* added chagelog

* chore: update platform libraries to 0.0.317

* refactor: remove unused context and update email verification query key dependencies

* chore: bump @calcom/platform-libraries from 0.0.318 to 0.0.319

* Update verifyCode.handler.ts

* chore: bump @calcom/platform-libraries from 0.0.319 to 0.0.320

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-21 15:06:42 +00:00
Romit f3d4eab951 fix: POST v2/bookings - recurrenceCount (with eventType reoccuring enabled) causes lengthInMinutes to be ignored (#23023)
* fix

* added tests

* chore
2025-08-21 16:54:33 +02:00
Anik Dhabal Babu 866b6cc233 fix: attendess are not showing (#23255) 2025-08-21 11:32:42 -03:00
沙什瓦特andDevanshu Sharma cafecad8ae feat: multi Select for "Event Type" in Insights (#23173)
* Multi select event type in insights

* Fixing multi select query generation

* Added null check for eventType filters

* Updated test to support multiselect

* Type fix

---------

Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
2025-08-21 13:30:21 +00:00
Benny Joo 0ea673e507 feat: update global feature flag to team feature flag for team booking page cache (#23154)
* add team id fetcher based on slugs

* export getOrgContext

* update RSC

* refactor

* further refactor
2025-08-21 21:34:45 +09:00
Eunjae Leeandcoderabbitai[bot] cf367c9340 feat: add attendee filters to /insights/routing (+ use RoutingFormResponseDenormalized) (#21297)
* fix: denormalize RoutingFormResponse with InsightsRoutingService

* fix tests

* fix type error

* update service usage

* fix attendee filters

* display attendee phone number

* use useInsightsRoutingParameters()

* remove unnecessary type check

* clean up

* fix type error

* fix wrong import

* fix integration tests

* Update packages/lib/server/service/__tests__/InsightsRoutingService.integration-test.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-21 14:09:22 +02:00
Alex van Andel d72e15649f chore: Remove maintenance mode middleware (#23251) 2025-08-21 11:12:54 +00:00
Somay Chauhan 89a3b77a1d feat: add hideEventMetadata prop (#23211)
* feat: add hideEventMetadata prop

* added changelog

* udpate

* undo extra changes
2025-08-21 09:00:51 +00:00
Somay Chauhan 38f6f71ae0 feat: add organizer.id to the BOOKING_CREATED webhook for requires confirmation event bookings (#23245) 2025-08-21 08:58:51 +00:00
Rajiv Sahal 40caa82e55 fix: onBookerStateChange prop for Booker atom (#23213)
* fix onBookerStateChange not working

* add changesets

* update changesets message
2025-08-21 08:51:48 +00:00
Joe Au-Yeung b9aee3b18c fix: Add guests via URL param even if field is hidden (#23229)
* Add guests via URL param even if field is hidden

* Clean up
2025-08-21 02:27:52 +00:00
Volnei Munhoz 3f8d2f2b38 chore: add coderabbit config file (#23200)
Post release / release (push) Canceled after 0s
* chore: add coderabbit config file

* remove wrong config

* add coderabbit instructions

* remove unnecessary paths

* add path instructions

* Fix duplicated instructions on yaml file

* Fix coderabbit.yaml syntax
2025-08-20 22:55:27 +00:00
emrysal c3731d9f17 chore: release v5.6.3 2025-08-20 22:49:28 +00:00
Vitor Monteiro 6e4b2d8886 fix: addOnLeading text field caret cropping (#23225) 2025-08-20 22:32:19 +00:00
cf13c06c57 chore: Upgrade boxyhq/saml-jackson to 1.52.2 (#23231)
* Upgrade jackson to 1.52.2

* Update apps/web/package.json

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Remove unused option prop

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-08-20 23:13:36 +01:00
Anik Dhabal Babu e309e72be3 fix: duplicate event created on attendee calendar (#23216) 2025-08-20 15:53:15 +00:00
b4d804de7c chore: hide cal branding for platform bookings (#22979)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2025-08-20 12:27:09 -03:00
Volnei Munhoz f501ef3269 refactor: remove highperf endpoints (#23171)
* refactor: remove highperf endpoints

* fix trpc function name

* chore: remove isTeamEvent logic

* chore: remove isTeamEvent at all

* revert wrong change

* Revert "chore: remove isTeamEvent at all"

This reverts commit 6dbc8c31f16c40a7654a05240661d7892aa48bb8.

* revert wrong change
2025-08-20 15:12:31 +00:00
Volnei Munhoz 79f1074f93 chore: Remove Vercel functions memory config (#23125)
* chore:Remove Vercel memory config

* chore: remove empty vercel.json configs
2025-08-20 14:51:45 +00:00
Vansh GilhotraandKartik Saini 48aa6bf489 fix: Add missing CSS variable for switch component accent color in getting-started flow (#23129)
- Fix switch component thumb appearing all white when ON
- Add --cal-brand-accent CSS variable definitions for light and dark modes
- Ensures proper contrast: white accent on dark bg, black accent on light bg
- Resolves UI issue on /getting-started/connected-calendar and /getting-started/setup-availability pages

Fixes switch components using bg-brand-accent class that maps to var(--cal-brand-accent,white)
When CSS variable was undefined, it defaulted to white causing invisible thumbs.

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-08-20 18:29:03 +05:30
Hariom Balhara 8ed2aacd9e Add organization rewrite logs (#23210) 2025-08-20 11:51:55 +00:00
Hariom BalharaandDevin AI dfc5d914fc chore: Routing Forms - remove unused report.handler.ts and jsonLogicToPrisma functionality (#23032)
* feat: remove unused report.handler.ts and jsonLogicToPrisma functionality

- Remove unused packages/app-store/routing-forms/trpc/report.handler.ts
- Remove packages/app-store/routing-forms/jsonLogicToPrisma.ts implementation
- Remove packages/app-store/routing-forms/__tests__/jsonLogicToPrisma.test.ts test file

These files were no longer being used in the routing-forms package.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: clean up remaining references to removed report functionality

- Remove unused report.schema.ts file
- Remove buildResponsesForReporting test file
- Remove report endpoint from router
- Update comment referencing jsonLogicToPrisma

All references to the removed jsonLogicToPrisma and report handler functionality have been cleaned up.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Update packages/app-store/routing-forms/lib/getQueryBuilderConfig.ts

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-20 15:17:35 +05:30
Tim NeutkensandKeith Williams 0a11c45065 perf: Only run Sentry in production (#23167)
* Only run Sentry in production

* Only run Sentry in production for instrumentation-client

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-08-20 07:27:51 +00:00
Tim Neutkens 4ae5de5a3a Only apply autoprefixer in production (#23168) 2025-08-20 07:26:32 +00:00
github-actions[bot] e6959f3c7c chore: version packages (#23188)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-20 04:57:40 +00:00
emrysal 67af8b4973 chore: release v5.6.2
Post release / release (push) Canceled after 0s
2025-08-20 01:36:25 +00:00
Volnei Munhoz 7794f2596e chore: deprecated prisma default export (#23199) 2025-08-19 19:42:10 +00:00
167cf6e352 feat: add team schedules (#22954)
* feat: add team schedules

* fixup: implement feedback for teams controller

* cleanup unused functions

* fixup: implement PR feedback

* fixup: move over teams schedule controller inside of teams module

* remove unused param and update summary

* fixup: move over teams schedules controller inside of teams module

* update e2e tests

* add team schedule module

* update teams schedules module

* fixup: update guards

* init tests for teams schedules controller

* implement code rabbit feedback

* fixup: implement PR feedback

* fixup: remove getTeamSchedulesByUserIds

---------

Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
2025-08-19 17:03:55 +03:00
Lauris SkraucisandSomay Chauhan 3d778ba481 fix: get organization bookings bookingUid parameter (#23186)
* fix: get organization bookings bookingUid parameter

* chore: bump @calcom/platform-libraries from 0.0.315 to 0.0.318

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2025-08-19 15:46:40 +02:00
b8f0000b59 feat: hide member filter for users with only MEMBER role (#23097)
- Conditionally disable userId column filter based on isTeamAdminOrOwner flag
- Prevents MEMBER-only users from encountering backend permission errors
- Users with ADMIN/OWNER role in any team can still access the filter

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-08-19 22:28:02 +09:00
Somay Chauhan 618ef63017 fix: Availability atom handleFormSubmit callbacks not triggering (#23174)
* fix: form submit callbacks trigger

* added changelog

* fix: type-error
2025-08-19 17:46:26 +05:30
Morgan ae8e633716 fix: correct impossible logical condition in Office365 calendar error handling #23151 (#23185) 2025-08-19 12:10:28 +00:00
Somay Chauhan 4a23832388 refactor: update microsoft platform slug to office365 for consistency (#23179)
* refactor: update Microsoft platform slug to office365 for consistency

* Update server.test.ts
2025-08-19 15:04:47 +03:00
Benny Joo 02e486d757 perf: og image caching v1 (#23180)
* perf: og image caching v1

* fix

* fix
2025-08-19 08:54:39 -03:00
Anik Dhabal Babu af6e24f433 fix: error oening date override input dialog (#23182) 2025-08-19 11:00:33 +00:00
Joe Au-Yeung f9e24f51a7 refactor: Extract <CalVideoSettings /> from `<Locations /> (#23113)
* Create `CalVideoSettings` type

* Create `LocationOptionContainer`

* Extract cal video settings to `CalVideoSettings`

* Refactor `<Locations />` component to use `<CalVideoSettings />`

* Unify naming

* Move location folder back under components

* Type fixes

* Delete console.log
2025-08-19 10:24:28 +00:00
Morgan 9e9f815809 refactor: replace static methods of HostRepository and PrismaAttributeRepository (#23175) 2025-08-19 07:59:16 +00:00
Lauris Skraucis a91561f3c2 fix: allow org admin to accept/reject booking (#23164)
* fix: allow org admin to accept/reject booking

* chore: bump platform libraries
2025-08-19 12:10:24 +05:30
Joe Au-YeungandHariom Balhara ff79064ca7 fix: Managed event types - children inherit team's appearance settings (#23116)
* Get parent event type team's appearance settings

* Use style profile to get brand colours

* Update packages/features/eventtypes/lib/getPublicEvent.ts

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-08-19 05:52:27 +00:00
Radhey MugdalandKartik Saini 1a573e1619 feat: v2 api hidden event types (#22715)
* feat: v2 api hidden event types

* feat: Add 'hidden' property to Input type in output.service.ts

---------

Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com>
2025-08-19 11:07:12 +05:30
沙什瓦特 511be7386c Fixed scroll issue while configuring workflow (#23142) 2025-08-19 11:03:54 +05:30
emrysal 2a0991c259 chore: release v5.6.1
Post release / release (push) Canceled after 0s
2025-08-18 23:55:48 +00:00
Eunjae LeeandDevin AI 2e454c9723 refactor: rename insights routing service files (#22917)
- Rename insightsRoutingDI.ts -> InsightsRoutingDIService.ts
- Rename insightsRoutingBase.ts -> InsightsRoutingBaseService.ts
- Update all import statements to use new file names
- No functional changes, only file renames and import updates

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-18 15:15:21 +00:00
Anik Dhabal Babu 7e6d94c8e0 fix: team admin/owner can't change others member role (#23161)
* fix: team admin/owner can't change member role

* update

* Update role-management.factory.ts

* address review
2025-08-18 14:57:22 +00:00
Volnei Munhoz e18b790ccd chore: Upgrade Turborepo from 1 to 2.5 (#23101)
* upgrade turborepo from 1 to 2.5

* Fix tsconfig.json paths path

* chore: fix root path on tsconfig.paths

* chore: add baseUrl to tsconfig.json

* chore: tsconfig.json issue

* try: CI without tsconfig paths

* chore: add only local paths on tsconfig.json

* chore: Fix docs

* Fix missing env var on turbo.json
2025-08-18 11:17:27 -03:00
Morgan 0274417ad1 fix: api v2 workflow controller trigger validation (#23159) 2025-08-18 16:15:40 +03:00
a8a3a93ace fix: redirect to existing payment session on paid booking retry (#23096)
* fix: redirect to existing payment page on booking retry

- Extract existing payment UID from unpaid bookings
- Return paymentUid and paymentId when payment form should be shown
- Prevents payment form bypass by redirecting to original payment session
- Maintains payment continuity without creating duplicate bookings

Fixes issue where canceled payments showed success page on rebooking

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* test: add unit test for payment retry scenario

- Test verifies existing payment UID is returned on booking retry
- Ensures no duplicate bookings are created in database
- Validates paymentRequired flag is set correctly for unpaid bookings
- Covers the payment redirect fix in handleNewBooking.ts

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* update

* fix: restore calendar event deletion for round robin reassignment

- Add deleteEventsAndMeetings call back to EventManager.reschedule() when changedOrganizer is true
- Add unit test to verify calendar deletion during round robin reassignment
- Fixes regression where calendar events weren't deleted from original host's calendar

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* Update roundRobinReassignment.test.ts

* Update EventManager.ts

* test: update payment retry test title to single descriptive line

- Changed from numbered list format to concise single line description
- Test functionality remains unchanged and passes successfully

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-18 11:44:40 +00:00
88bbab216e fix: Consistently remove a member from organization/sub-team/team - through all the APIs and webapp actions (#22806)
* Fix membership deletion handling and add tests

* fixes

* revert api-v2 specific changes

* Add more tests

* refactor: Move removeMember function into TeamService as private static methods

- Moved removeMember and all related helper functions from separate file into TeamService class
- Made all functions private static methods instead of exporting them
- Deleted the original removeMember.ts file since it's no longer needed
- Updated imports to use the new location
- All integration tests pass successfully

* refactor: Rename memberId to userId in TeamService methods

- Renamed memberId parameter to userId in removeMember private method
- Renamed memberIds parameter to userIds in removeMembers public method
- Updated all calls to removeMembers to use userIds instead of memberIds
- Parameter names now accurately reflect that they are user IDs, not membership IDs

* test: add service unit tests and simplify e2e tests for membership deletion

- Add unit tests for membership deletion services
- Remove redundant deletion behavior tests from controllers
- Keep only happy path tests in e2e controller tests
- Fix unused imports and variables

* package.json version

* fix unit teswtes

* No specs file

* fix unit tests

* fix: Add platform-libraries build step to E2E API v2 workflow

The E2E API v2 tests were failing with TypeScript compilation errors because
TeamService imports from @calcom/platform-libraries require the package to be
built first to generate the dist/ folder with compiled exports.

This adds the same build step that was added to unit-tests.yml to resolve
the module resolution errors.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Making tests clearer and easier to undestand

* chore: bump platform libs

* chore: bump platform libs

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: cal.com <morgan@cal.com>
2025-08-18 08:38:32 -03:00
Chinmay Patil f74434a282 fix: show temporarily out-of-office message on all screen sizes (#23130) 2025-08-18 16:56:49 +05:30
Joe Au-Yeung ed6f9d8252 fix: Add cal.eu to IS_CALCOM (#23111)
* Add cal.eu to `IS_CALCOM`

* Create `getCalcomUrl` function

* Use `getCalcomURl` in generate metadata

* Use `getCalcomURl` in `buildCanonical`

* Linting fix
2025-08-18 16:04:48 +05:30
Benny Joo a19ccab16a perf: use promise.all in /apps/installed (#23146) 2025-08-18 10:11:22 +05:30
Benny Joo 77369a13f1 chore: remove unused orgMigrations files (#23056) 2025-08-17 22:18:14 -03:00
Anik Dhabal Babu 8231356d2a test: E2E test to verify event multiple duration works correctly (#23144)
* fix: multiple booking duration isn't working

* followup test
2025-08-17 20:26:14 +01:00
Anik Dhabal Babu 469a942b5a fix: multiple booking duration isn't working (#23140) 2025-08-17 17:08:27 +00:00
221 changed files with 8461 additions and 4529 deletions

No files matched your search

+187
View File
@@ -0,0 +1,187 @@
language: en-US
tone_instructions: ''
early_access: false
enable_free_tier: true
reviews:
profile: chill
request_changes_workflow: false
high_level_summary: false
high_level_summary_placeholder: '@coderabbitai summary'
high_level_summary_in_walkthrough: false
auto_title_placeholder: '@coderabbitai'
auto_title_instructions: ''
review_status: true
commit_status: true
fail_commit_status: false
collapse_walkthrough: false
changed_files_summary: false
sequence_diagrams: false
estimate_code_review_effort: false
assess_linked_issues: true
related_issues: false
related_prs: true
suggested_labels: false
auto_apply_labels: false
suggested_reviewers: false
auto_assign_reviewers: false
poem: false
labeling_instructions: []
path_filters:
- '!**/node_modules/**'
- '!**/dist/**'
- '!**/build/**'
- '!**/.next/**'
path_instructions:
- path: "**/*.{ts,tsx,js,jsx}"
instructions: "Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module."
abort_on_close: true
disable_cache: false
auto_review:
enabled: true
auto_incremental_review: true
ignore_title_keywords: []
labels: []
drafts: false
base_branches: []
finishing_touches:
docstrings:
enabled: true
unit_tests:
enabled: true
pre_merge_checks:
docstrings:
mode: warning
threshold: 80
title:
mode: warning
requirements: ''
description:
mode: warning
issue_assessment:
mode: warning
tools:
ast-grep:
rule_dirs: []
util_dirs: []
essential_rules: true
packages: []
shellcheck:
enabled: true
ruff:
enabled: true
markdownlint:
enabled: true
github-checks:
enabled: true
timeout_ms: 180000
languagetool:
enabled: true
enabled_rules: []
disabled_rules: []
enabled_categories: []
disabled_categories: []
enabled_only: false
level: default
biome:
enabled: true
hadolint:
enabled: true
swiftlint:
enabled: true
phpstan:
enabled: true
level: default
phpmd:
enabled: true
phpcs:
enabled: true
golangci-lint:
enabled: true
yamllint:
enabled: true
gitleaks:
enabled: true
checkov:
enabled: true
detekt:
enabled: true
eslint:
enabled: true
flake8:
enabled: true
rubocop:
enabled: true
buf:
enabled: true
regal:
enabled: true
actionlint:
enabled: true
pmd:
enabled: true
cppcheck:
enabled: true
semgrep:
enabled: true
circleci:
enabled: true
clippy:
enabled: true
sqlfluff:
enabled: true
prismaLint:
enabled: true
pylint:
enabled: true
oxc:
enabled: true
shopifyThemeCheck:
enabled: true
luacheck:
enabled: true
brakeman:
enabled: true
dotenvLint:
enabled: true
htmlhint:
enabled: true
checkmake:
enabled: true
osvScanner:
enabled: true
chat:
art: true
auto_reply: true
integrations:
jira:
usage: auto
linear:
usage: auto
knowledge_base:
opt_out: false
web_search:
enabled: true
code_guidelines:
enabled: true
filePatterns: []
learnings:
scope: auto
issues:
scope: auto
jira:
usage: auto
project_keys: []
linear:
usage: auto
team_keys: []
pull_requests:
scope: auto
mcp:
usage: auto
disabled_servers: []
code_generation:
docstrings:
language: en-US
path_instructions: []
unit_tests:
path_instructions: []
+2 -2
View File
@@ -724,7 +724,7 @@ ${we.map(pe=>`Not found: ${lo(pe)}
To understand the dependency tree requiring vulnerable packages, check the raw report with the \`--json\` flag or use \`yarn why <package>\` to get more information as to who depends on them.
`,examples:[["Checks for known security issues with the installed packages. The output is a list of known issues.","yarn npm audit"],["Audit dependencies in all workspaces","yarn npm audit --all"],["Limit auditing to `dependencies` (excludes `devDependencies`)","yarn npm audit --environment production"],["Show audit report as valid JSON","yarn npm audit --json"],["Audit all direct and transitive dependencies","yarn npm audit --recursive"],["Output moderate (or more severe) vulnerabilities","yarn npm audit --severity moderate"],["Exclude certain packages","yarn npm audit --exclude package1 --exclude package2"],["Ignore specific advisories","yarn npm audit --ignore 1234567 --ignore 7654321"]]});var TM=Pe(Xr()),OM=J("util"),Yu=class extends De{constructor(){super(...arguments);this.fields=z.String("-f,--fields",{description:"A comma-separated list of manifest fields that should be displayed"});this.json=z.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.packages=z.Rest()}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),{project:i}=await je.find(t,this.context.cwd),n=typeof this.fields<"u"?new Set(["name",...this.fields.split(/\s*,\s*/)]):null,s=[],o=!1,a=await Ge.start({configuration:t,includeFooter:!1,json:this.json,stdout:this.context.stdout},async l=>{for(let c of this.packages){let u;if(c==="."){let L=i.topLevelWorkspace;if(!L.manifest.name)throw new be(`Missing ${ee.pretty(t,"name",ee.Type.CODE)} field in ${K.fromPortablePath(x.join(L.cwd,xt.manifest))}`);u=P.makeDescriptor(L.manifest.name,"unknown")}else u=P.parseDescriptor(c);let g=Mt.getIdentUrl(u),f=MM(await Mt.get(g,{configuration:t,ident:u,jsonResponse:!0,customErrorMessage:Mt.customPackageError})),h=Object.keys(f.versions).sort(TM.default.compareLoose),C=f["dist-tags"].latest||h[h.length-1],y=vt.validRange(u.range);if(y){let L=TM.default.maxSatisfying(h,y);L!==null?C=L:(l.reportWarning(0,`Unmet range ${P.prettyRange(t,u.range)}; falling back to the latest version`),o=!0)}else Object.prototype.hasOwnProperty.call(f["dist-tags"],u.range)?C=f["dist-tags"][u.range]:u.range!=="unknown"&&(l.reportWarning(0,`Unknown tag ${P.prettyRange(t,u.range)}; falling back to the latest version`),o=!0);let B=f.versions[C],v={...f,...B,version:C,versions:h},D;if(n!==null){D={};for(let L of n){let H=v[L];if(typeof H<"u")D[L]=H;else{l.reportWarning(1,`The ${ee.pretty(t,L,ee.Type.CODE)} field doesn't exist inside ${P.prettyIdent(t,u)}'s information`),o=!0;continue}}}else this.json||(delete v.dist,delete v.readme,delete v.users),D=v;l.reportJson(D),this.json||s.push(D)}});OM.inspect.styles.name="cyan";for(let l of s)(l!==s[0]||o)&&this.context.stdout.write(`
`),this.context.stdout.write(`${(0,OM.inspect)(l,{depth:1/0,colors:!0,compact:!1})}
`);return a.exitCode()}};Yu.paths=[["npm","info"]],Yu.usage=ve.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@<range>` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]});function MM(r){if(Array.isArray(r)){let e=[];for(let t of r)t=MM(t),t&&e.push(t);return e}else if(typeof r=="object"&&r!==null){let e={};for(let t of Object.keys(r)){if(t.startsWith("_"))continue;let i=MM(r[t]);i&&(e[t]=i)}return e}else return r||null}var sue=Pe(Pm()),ju=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--scope",{description:"Login to the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Login to the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i=await Qb({configuration:t,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ge.start({configuration:t,stdout:this.context.stdout,includeFooter:!1},async s=>{let o=await zVe({configuration:t,registry:i,report:s,stdin:this.context.stdin,stdout:this.context.stdout}),a=`/-/user/org.couchdb.user:${encodeURIComponent(o.name)}`,l=await Mt.put(a,o,{attemptedAs:o.name,configuration:t,registry:i,jsonResponse:!0,authType:Mt.AuthType.NO_AUTH});return await WVe(i,l.token,{configuration:t,scope:this.scope}),s.reportInfo(0,"Successfully logged in")})).exitCode()}};ju.paths=[["npm","login"]],ju.usage=ve.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--scope` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --scope my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]});async function Qb({scope:r,publish:e,configuration:t,cwd:i}){return r&&e?or.getScopeRegistry(r,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):r?or.getScopeRegistry(r,{configuration:t}):e?or.getPublishRegistry((await Ph(t,i)).manifest,{configuration:t}):or.getDefaultRegistry({configuration:t})}async function WVe(r,e,{configuration:t,scope:i}){let n=o=>a=>{let l=Ie.isIndexableObject(a)?a:{},c=l[o],u=Ie.isIndexableObject(c)?c:{};return{...l,[o]:{...u,npmAuthToken:e}}},s=i?{npmScopes:n(i)}:{npmRegistries:n(r)};return await ye.updateHomeConfiguration(s)}async function zVe({configuration:r,registry:e,report:t,stdin:i,stdout:n}){t.reportInfo(0,`Logging in to ${ee.pretty(r,e,ee.Type.URL)}`);let s=!1;if(e.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(t.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),s=!0),t.reporLine truncated
`);return a.exitCode()}};Yu.paths=[["npm","info"]],Yu.usage=ve.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@<range>` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]});function MM(r){if(Array.isArray(r)){let e=[];for(let t of r)t=MM(t),t&&e.push(t);return e}else if(typeof r=="object"&&r!==null){let e={};for(let t of Object.keys(r)){if(t.startsWith("_"))continue;let i=MM(r[t]);i&&(e[t]=i)}return e}else return r||null}var sue=Pe(Pm()),ju=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--filter",{description:"Login to the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Login to the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i=await Qb({configuration:t,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Ge.start({configuration:t,stdout:this.context.stdout,includeFooter:!1},async s=>{let o=await zVe({configuration:t,registry:i,report:s,stdin:this.context.stdin,stdout:this.context.stdout}),a=`/-/user/org.couchdb.user:${encodeURIComponent(o.name)}`,l=await Mt.put(a,o,{attemptedAs:o.name,configuration:t,registry:i,jsonResponse:!0,authType:Mt.AuthType.NO_AUTH});return await WVe(i,l.token,{configuration:t,scope:this.scope}),s.reportInfo(0,"Successfully logged in")})).exitCode()}};ju.paths=[["npm","login"]],ju.usage=ve.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--filter` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --filter my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]});async function Qb({scope:r,publish:e,configuration:t,cwd:i}){return r&&e?or.getScopeRegistry(r,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):r?or.getScopeRegistry(r,{configuration:t}):e?or.getPublishRegistry((await Ph(t,i)).manifest,{configuration:t}):or.getDefaultRegistry({configuration:t})}async function WVe(r,e,{configuration:t,scope:i}){let n=o=>a=>{let l=Ie.isIndexableObject(a)?a:{},c=l[o],u=Ie.isIndexableObject(c)?c:{};return{...l,[o]:{...u,npmAuthToken:e}}},s=i?{npmScopes:n(i)}:{npmRegistries:n(r)};return await ye.updateHomeConfiguration(s)}async function zVe({configuration:r,registry:e,report:t,stdin:i,stdout:n}){t.reportInfo(0,`Logging in to ${ee.pretty(r,e,ee.Type.URL)}`);let s=!1;if(e.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(t.reportInfo(0,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),s=!0),t.reLine truncated
This command will list all tags of a package from the npm registry.
If the package is not specified, Yarn will default to the current workspace.
@@ -732,7 +732,7 @@ ${we.map(pe=>`Not found: ${lo(pe)}
This command will add a tag to the npm registry for a specific version of a package. If the tag already exists, it will be overwritten.
`,examples:[["Add a `beta` tag for version `2.3.4-beta.4` of package `my-pkg`","yarn npm tag add my-pkg@2.3.4-beta.4 beta"]]});var Vu=class extends De{constructor(){super(...arguments);this.package=z.String();this.tag=z.String()}async execute(){if(this.tag==="latest")throw new be("The 'latest' tag cannot be removed.");let t=await ye.find(this.context.cwd,this.context.plugins),{project:i,workspace:n}=await je.find(t,this.context.cwd);if(!n)throw new ct(i.cwd,this.context.cwd);let s=P.parseIdent(this.package),o=or.getPublishRegistry(n.manifest,{configuration:t}),a=ee.pretty(t,this.tag,ee.Type.CODE),l=ee.pretty(t,s,ee.Type.IDENT),c=await IE(s,t);if(!Object.prototype.hasOwnProperty.call(c,this.tag))throw new be(`${a} is not a tag of package ${l}`);return(await Ge.start({configuration:t,stdout:this.context.stdout},async g=>{let f=`/-/package${Mt.getIdentUrl(s)}/dist-tags/${encodeURIComponent(this.tag)}`;await Mt.del(f,{configuration:t,registry:o,ident:s,jsonResponse:!0}),g.reportInfo(0,`Tag ${a} removed from package ${l}`)})).exitCode()}};Vu.paths=[["npm","tag","remove"]],Vu.usage=ve.Usage({category:"Npm-related commands",description:"remove a tag from a package",details:`
This command will remove a tag from a package from the npm registry.
`,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]});var Xu=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--scope",{description:"Print username for the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Print username for the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i;return this.scope&&this.publish?i=or.getScopeRegistry(this.scope,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):this.scope?i=or.getScopeRegistry(this.scope,{configuration:t}):this.publish?i=or.getPublishRegistry((await Ph(t,this.context.cwd)).manifest,{configuration:t}):i=or.getDefaultRegistry({configuration:t}),(await Ge.start({configuration:t,stdout:this.context.stdout},async s=>{var a,l;let o;try{o=await Mt.get("/-/whoami",{configuration:t,registry:i,authType:Mt.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?P.makeIdent(this.scope,""):void 0})}catch(c){if(((a=c.response)==null?void 0:a.statusCode)===401||((l=c.response)==null?void 0:l.statusCode)===403){s.reportError(41,"Authentication failed - your credentials may have expired");return}else throw c}s.reportInfo(0,o.username)})).exitCode()}};Xu.paths=[["npm","whoami"]],Xu.usage=ve.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--scope`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --scope company"]]});var _Ve={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[Gu,Yu,ju,qu,Ju,zu,Wu,Vu,Xu]},ZVe=_Ve;var zM={};ut(zM,{default:()=>h6e,patchUtils:()=>_u});var _u={};ut(_u,{applyPatchFile:()=>Sb,diffFolders:()=>JM,ensureUnpatchedDescriptor:()=>HM,extractPackageToDisk:()=>qM,extractPatchFlags:()=>hue,isParentRequired:()=>jM,loadPatchFiles:()=>QE,makeDescriptor:()=>GM,makeLocator:()=>YM,makePatchHash:()=>WM,parseDescriptor:()=>wE,parseLocator:()=>BE,parsePatchFile:()=>yE});var $Ve=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function Ap(r){return x.relative(Oe.root,x.resolve(Oe.root,K.toPortablePath(r)))}function e6e(r){let e=r.trim().match($Ve);if(!e)throw new Error(`Bad header line: '${r}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var t6e=420,r6e=493;var Aue=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),i6e=r=>({header:e6e(r),parts:[]}),n6e={["@"]:"header",["-"]:"deletion",["+"]:"insertion",[" "]:"context",["\\"]:"pragma",undefined:"context"};function s6e(r){let e=[],t=Aue(),i="parsing header",n=null,s=null;function o(){n&&(s&&(n.parts.push(s),s=null),t.hunks.push(n),n=null)}function a(){o(),e.push(t),t=Aue()}for(let l=0;l<r.length;l++){let c=r[l];if(i==="parsing header")if(c.startsWith("@@"))i="parsing hunks",t.hunks=[],l-=1;else if(c.startsWith("diff --git ")){t&&t.diffLineFromPath&&a();let u=c.match(/^diff --git a\/(.*?) b\/(.*?)\s*$/);if(!u)throw new Error(`Bad diff line: ${c}`);t.diffLineFromPath=u[1],t.diffLineToPath=u[2]}else if(c.startsWith("old mode "))t.oldMode=c.slice(9).trim();else if(c.startsWith("new mode "))t.newMode=c.slice(9).trim();else if(c.startsWith("deleted file mode "))t.deletedFileMode=c.slice(18).trim();else if(c.startsWith("new file mode "))t.newFileMode=c.slice(14).trim();else if(c.startsWith("rename from "))t.renameFrom=c.slice(12).trim();else if(c.startsWith("rename to "))t.renameTo=c.slice(10).trim();else if(c.startsWith("index ")){let u=c.match(/(\w+)\.\.(\w+)/);if(!u)continue;t.beforeHash=u[1],t.afterHash=u[2]}else c.startsWith("semver exclusivity ")?t.semverExclusivity=c.slice(19).trim():c.startsWith("--- ")?t.fromPath=c.slice(6).trim():c.startsWith("+++ ")&&(t.toP Line truncated
`,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]});var Xu=class extends De{constructor(){super(...arguments);this.scope=z.String("-s,--filter",{description:"Print username for the registry configured for a given scope"});this.publish=z.Boolean("--publish",!1,{description:"Print username for the publish registry"})}async execute(){let t=await ye.find(this.context.cwd,this.context.plugins),i;return this.scope&&this.publish?i=or.getScopeRegistry(this.scope,{configuration:t,type:or.RegistryType.PUBLISH_REGISTRY}):this.scope?i=or.getScopeRegistry(this.scope,{configuration:t}):this.publish?i=or.getPublishRegistry((await Ph(t,this.context.cwd)).manifest,{configuration:t}):i=or.getDefaultRegistry({configuration:t}),(await Ge.start({configuration:t,stdout:this.context.stdout},async s=>{var a,l;let o;try{o=await Mt.get("/-/whoami",{configuration:t,registry:i,authType:Mt.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?P.makeIdent(this.scope,""):void 0})}catch(c){if(((a=c.response)==null?void 0:a.statusCode)===401||((l=c.response)==null?void 0:l.statusCode)===403){s.reportError(41,"Authentication failed - your credentials may have expired");return}else throw c}s.reportInfo(0,o.username)})).exitCode()}};Xu.paths=[["npm","whoami"]],Xu.usage=ve.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--filter`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --filter company"]]});var _Ve={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:"STRING",default:null},npmAuditExcludePackages:{description:"Array of glob patterns of packages to exclude from npm audit",type:"STRING",default:[],isArray:!0},npmAuditIgnoreAdvisories:{description:"Array of glob patterns of advisory IDs to exclude from npm audit",type:"STRING",default:[],isArray:!0}},commands:[Gu,Yu,ju,qu,Ju,zu,Wu,Vu,Xu]},ZVe=_Ve;var zM={};ut(zM,{default:()=>h6e,patchUtils:()=>_u});var _u={};ut(_u,{applyPatchFile:()=>Sb,diffFolders:()=>JM,ensureUnpatchedDescriptor:()=>HM,extractPackageToDisk:()=>qM,extractPatchFlags:()=>hue,isParentRequired:()=>jM,loadPatchFiles:()=>QE,makeDescriptor:()=>GM,makeLocator:()=>YM,makePatchHash:()=>WM,parseDescriptor:()=>wE,parseLocator:()=>BE,parsePatchFile:()=>yE});var $Ve=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function Ap(r){return x.relative(Oe.root,x.resolve(Oe.root,K.toPortablePath(r)))}function e6e(r){let e=r.trim().match($Ve);if(!e)throw new Error(`Bad header line: '${r}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var t6e=420,r6e=493;var Aue=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),i6e=r=>({header:e6e(r),parts:[]}),n6e={["@"]:"header",["-"]:"deletion",["+"]:"insertion",[" "]:"context",["\\"]:"pragma",undefined:"context"};function s6e(r){let e=[],t=Aue(),i="parsing header",n=null,s=null;function o(){n&&(s&&(n.parts.push(s),s=null),t.hunks.push(n),n=null)}function a(){o(),e.push(t),t=Aue()}for(let l=0;l<r.length;l++){let c=r[l];if(i==="parsing header")if(c.startsWith("@@"))i="parsing hunks",t.hunks=[],l-=1;else if(c.startsWith("diff --git ")){t&&t.diffLineFromPath&&a();let u=c.match(/^diff --git a\/(.*?) b\/(.*?)\s*$/);if(!u)throw new Error(`Bad diff line: ${c}`);t.diffLineFromPath=u[1],t.diffLineToPath=u[2]}else if(c.startsWith("old mode "))t.oldMode=c.slice(9).trim();else if(c.startsWith("new mode "))t.newMode=c.slice(9).trim();else if(c.startsWith("deleted file mode "))t.deletedFileMode=c.slice(18).trim();else if(c.startsWith("new file mode "))t.newFileMode=c.slice(14).trim();else if(c.startsWith("rename from "))t.renameFrom=c.slice(12).trim();else if(c.startsWith("rename to "))t.renameTo=c.slice(10).trim();else if(c.startsWith("index ")){let u=c.match(/(\w+)\.\.(\w+)/);if(!u)continue;t.beforeHash=u[1],t.afterHash=u[2]}else c.startsWith("semver exclusivity ")?t.semverExclusivity=c.slice(19).trim():c.startsWith("--- ")?t.fromPath=c.slice(6).trim():c.startsWith("+++ ")&&(t. Line truncated
`)+(n.hunk.parts[0].noNewlineAtEndOfFile?"":`
`):"";await e.mkdirpPromise(x.dirname(n.path),{chmod:493,utimes:[xr.SAFE_TIME,xr.SAFE_TIME]}),await e.writeFilePromise(n.path,s,{mode:n.mode}),await e.utimesPromise(n.path,xr.SAFE_TIME,xr.SAFE_TIME)}break;case"patch":await cp(e,n.path,async()=>{await c6e(n,{baseFs:e,dryRun:t})});break;case"mode change":{let o=(await e.statPromise(n.path)).mode;if(lue(n.newMode)!==lue(o))continue;await cp(e,n.path,async()=>{await e.chmodPromise(n.path,n.newMode)})}break;default:Ie.assertNever(n);break}}function lue(r){return(r&64)>0}function cue(r){return r.replace(/\s+$/,"")}function l6e(r,e){return cue(r)===cue(e)}async function c6e({hunks:r,path:e},{baseFs:t,dryRun:i=!1}){let n=await t.statSync(e).mode,o=(await t.readFileSync(e,"utf8")).split(/\n/),a=[],l=0,c=0;for(let g of r){let f=Math.max(c,g.header.patched.start+l),h=Math.max(0,f-c),p=Math.max(0,o.length-f-g.header.original.length),C=Math.max(h,p),y=0,B=0,v=null;for(;y<=C;){if(y<=h&&(B=f-y,v=uue(g,o,B),v!==null)){y=-y;break}if(y<=p&&(B=f+y,v=uue(g,o,B),v!==null))break;y+=1}if(v===null)throw new lp(r.indexOf(g),g);a.push(v),l+=y,c=B+g.header.original.length}if(i)return;let u=0;for(let g of a)for(let f of g)switch(f.type){case"splice":{let h=f.index+u;o.splice(h,f.numToDelete,...f.linesToInsert),u+=f.linesToInsert.length-f.numToDelete}break;case"pop":o.pop();break;case"push":o.push(f.line);break;default:Ie.assertNever(f);break}await t.writeFilePromise(e,o.join(`
`),{mode:n})}function uue(r,e,t){let i=[];for(let n of r.parts)switch(n.type){case"context":case"deletion":{for(let s of n.lines){let o=e[t];if(o==null||!l6e(o,s))return null;t+=1}n.type==="deletion"&&(i.push({type:"splice",index:t-n.lines.length,numToDelete:n.lines.length,linesToInsert:[]}),n.noNewlineAtEndOfFile&&i.push({type:"push",line:""}))}break;case"insertion":i.push({type:"splice",index:t,numToDelete:0,linesToInsert:n.lines}),n.noNewlineAtEndOfFile&&i.push({type:"pop"});break;default:Ie.assertNever(n.type);break}return i}var g6e=/^builtin<([^>]+)>$/;function UM(r,e){let{source:t,selector:i,params:n}=P.parseRange(r);if(t===null)throw new Error("Patch locators must explicitly define their source");let s=i?i.split(/&/).map(c=>K.toPortablePath(c)):[],o=n&&typeof n.locator=="string"?P.parseLocator(n.locator):null,a=n&&typeof n.version=="string"?n.version:null,l=e(t);return{parentLocator:o,sourceItem:l,patchPaths:s,sourceVersion:a}}function wE(r){let{sourceItem:e,...t}=UM(r.range,P.parseDescriptor);return{...t,sourceDescriptor:e}}function BE(r){let{sourceItem:e,...t}=UM(r.reference,P.parseLocator);return{...t,sourceLocator:e}}function HM(r){if(!r.range.startsWith("patch:"))return r;let{sourceItem:e}=UM(r.range,P.parseDescriptor);return e}function gue({parentLocator:r,sourceItem:e,patchPaths:t,sourceVersion:i,patchHash:n},s){let o=r!==null?{locator:P.stringifyLocator(r)}:{},a=typeof i<"u"?{version:i}:{},l=typeof n<"u"?{hash:n}:{};return P.makeRange({protocol:"patch:",source:s(e),selector:t.join("&"),params:{...a,...l,...o}})}function GM(r,{parentLocator:e,sourceDescriptor:t,patchPaths:i}){return P.makeDescriptor(r,gue({parentLocator:e,sourceItem:t,patchPaths:i},P.stringifyDescriptor))}function YM(r,{parentLocator:e,sourcePackage:t,patchPaths:i,patchHash:n}){return P.makeLocator(r,gue({parentLocator:e,sourceItem:t,sourceVersion:t.version,patchPaths:i,patchHash:n},P.stringifyLocator))}function fue({onAbsolute:r,onRelative:e,onBuiltin:t},i){i.startsWith("~")&&(i=i.slice(1));let s=i.match(g6e);return s!==null?t(s[1]):x.isAbsolute(i)?r(i):e(i)}function hue(r){let e=r.startsWith("~");return e&&(r=r.slice(1)),{optional:e}}function jM(r){return fue({onAbsolute:()=>!1,onRelative:()=>!0,onBuiltin:()=>!1},r)}async function QE(r,e,t){let i=r!==null?await t.fetcher.fetch(r,t):null,n=i&&i.localPath?{packageFs:new jt(Oe.root),prefixPath:x.relative(Oe.root,i.localPath)}:i;i&&i!==n&&i.releaseFs&&i.releaseFs();let s=await Ie.releaseAfterUseAsync(async()=>await Promise.all(e.map(async o=>{let a=hue(o),l=await fue({onAbsolute:async()=>await M.readFilePromise(o,"utf8"),onRelative:async()=>{if(n===null)throw new Error("Assertion failed: The parent locator should have been fetched");return await n.packageFs.readFilePromise(x.join(n.prefixPath,o),"utf8")},onBuiltin:async c=>await t.project.configuration.firstHook(u=>u.getBuiltinPatch,t.project,c)},o);return{...a,source:l}})));for(let o of s)typeof o.source=="string"&&(o.source=o.source.replace(/\r\n?/g,`
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+1 -1
View File
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
Under Vercel > Your API Project > Settings
In General > Build & Development Settings
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps`
BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps`
OUTPUT DIRECTORY: `apps/api/.next`
In Git > Ignored Build Step
-7
View File
@@ -1,7 +0,0 @@
{
"functions": {
"pages/api/slots/*.ts": {
"memory": 512
}
}
}
+1 -1
View File
@@ -38,7 +38,7 @@
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.312",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.320",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
"@calcom/prisma": "*",
+10
View File
@@ -9,6 +9,7 @@ import { TestingModule } from "@nestjs/testing";
import { Test } from "@nestjs/testing";
import * as request from "supertest";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
@@ -33,6 +34,7 @@ describe("AppController", () => {
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipRepositoryFixture: MembershipRepositoryFixture;
let apiKeyString: string;
@@ -108,6 +110,14 @@ describe("AppController", () => {
organization: { connect: { id: organization.id } },
});
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
await membershipRepositoryFixture.create({
user: { connect: { id: user.id } },
team: { connect: { id: organization.id } },
role: "OWNER",
accepted: true,
});
app = moduleRef.createNestApplication();
await app.init();
});
@@ -15,11 +15,11 @@ import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
@@ -36,6 +36,7 @@ import {
CreateBookingInput_2024_08_13,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
} from "@calcom/platform-types";
@@ -50,9 +51,9 @@ describe("Managed user bookings 2024-08-13", () => {
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
let platformAdmin: User;
@@ -67,6 +68,10 @@ describe("Managed user bookings 2024-08-13", () => {
let thirdManagedUser: CreateManagedUserData;
let firstManagedUserEventTypeId: number;
let eventTypeRequiresConfirmationId: number;
const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`;
let orgAdminManagedUser: CreateManagedUserData;
let firstManagedUserBookingsCount = 0;
let secondManagedUserBookingsCount = 0;
@@ -85,9 +90,9 @@ describe("Managed user bookings 2024-08-13", () => {
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
@@ -95,6 +100,13 @@ describe("Managed user bookings 2024-08-13", () => {
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
platformBilling: {
create: {
customerId: "cus_999",
plan: "SCALE",
subscriptionId: "sub_999",
},
},
});
oAuthClient = await createOAuthClient(organization.id);
@@ -243,6 +255,59 @@ describe("Managed user bookings 2024-08-13", () => {
thirdManagedUser = responseBody.data;
});
it(`should create org admin managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: orgAdminManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Org Admin Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
orgAdminManagedUser = responseBody.data;
await request(app.getHttpServer())
.post(`/v2/organizations/${organization.id}/memberships`)
.set("x-cal-client-id", oAuthClient.id)
.set("x-cal-secret-key", oAuthClient.secret)
.send({
userId: orgAdminManagedUser.user.id,
role: "ADMIN",
accepted: true,
})
.expect(201);
});
it("should create an event type requiring confirmation for first managed user", async () => {
const eventTypeRequiresConfirmation = await eventTypesRepositoryFixture.create(
{
title: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
slug: `managed-user-bookings-event-type-requires-confirmation-${randomString()}`,
length: 60,
requiresConfirmation: true,
},
firstManagedUser.user.id
);
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
});
describe("bookings using original emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
@@ -543,10 +608,175 @@ describe("Managed user bookings 2024-08-13", () => {
});
});
describe("booking confirmation by org admin", () => {
it("should allow org admin managed user to confirm booking using access token", async () => {
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
user: {
connect: {
id: firstManagedUser.user.id,
},
},
startTime: new Date(Date.UTC(2030, 0, 9, 13, 0, 0)),
endTime: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)),
title: "Booking requiring confirmation",
uid: `booking-requiring-confirmation-${randomString()}`,
eventType: {
connect: {
id: eventTypeRequiresConfirmationId,
},
},
location: "https://meet.google.com/abc-def-ghi",
customInputs: {},
metadata: {},
status: "PENDING",
responses: {
name: secondManagedUser.user.name,
email: secondManagedUserEmail,
},
attendees: {
create: {
email: secondManagedUserEmail,
name: secondManagedUser.user.name!,
locale: secondManagedUser.user.locale,
timeZone: secondManagedUser.user.timeZone,
},
},
});
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
.expect(200);
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const bookingData = responseBody.data as BookingOutput_2024_08_13;
expect(bookingData.status).toEqual("accepted");
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
const confirmedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
expect(confirmedBooking?.status).toEqual("ACCEPTED");
});
it("should allow org admin managed user to reject booking using access token", async () => {
const bookingRequiringConfirmation = await bookingsRepositoryFixture.create({
user: {
connect: {
id: firstManagedUser.user.id,
},
},
startTime: new Date(Date.UTC(2030, 0, 9, 15, 0, 0)),
endTime: new Date(Date.UTC(2030, 0, 9, 16, 0, 0)),
title: "Booking requiring rejection",
uid: `booking-requiring-rejection-${randomString()}`,
eventType: {
connect: {
id: eventTypeRequiresConfirmationId,
},
},
location: "https://meet.google.com/abc-def-ghi",
customInputs: {},
metadata: {},
status: "PENDING",
responses: {
name: secondManagedUser.user.name,
email: secondManagedUserEmail,
},
attendees: {
create: {
email: secondManagedUserEmail,
name: secondManagedUser.user.name!,
locale: secondManagedUser.user.locale,
timeZone: secondManagedUser.user.timeZone,
},
},
});
const response = await request(app.getHttpServer())
.post(`/v2/bookings/${bookingRequiringConfirmation.uid}/decline`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
.expect(200);
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const bookingData = responseBody.data as BookingOutput_2024_08_13;
expect(bookingData.status).toEqual("rejected");
expect(bookingData.uid).toEqual(bookingRequiringConfirmation.uid);
const rejectedBooking = await bookingsRepositoryFixture.getByUid(bookingRequiringConfirmation.uid);
expect(rejectedBooking?.status).toEqual("REJECTED");
});
it("should return unauthorized when org admin tries to confirm regular user's booking", async () => {
const regularUser = await userRepositoryFixture.create({
email: `managed-user-bookings-2024-08-13-regular-user-${randomString()}@api.com`,
name: "Regular User",
});
const regularUserEventType = await eventTypesRepositoryFixture.create(
{
title: `regular-user-event-type-requires-confirmation-${randomString()}`,
slug: `regular-user-event-type-requires-confirmation-${randomString()}`,
length: 60,
requiresConfirmation: true,
},
regularUser.id
);
const regularUserBooking = await bookingsRepositoryFixture.create({
user: {
connect: {
id: regularUser.id,
},
},
startTime: new Date(Date.UTC(2030, 0, 9, 17, 0, 0)),
endTime: new Date(Date.UTC(2030, 0, 9, 18, 0, 0)),
title: "Regular user booking requiring confirmation",
uid: `regular-user-booking-${randomString()}`,
eventType: {
connect: {
id: regularUserEventType.id,
},
},
location: "https://meet.google.com/abc-def-ghi",
customInputs: {},
metadata: {},
status: "PENDING",
responses: {
name: "External Attendee",
email: "external@example.com",
},
attendees: {
create: {
email: "external@example.com",
name: "External Attendee",
locale: "en",
timeZone: "Europe/Rome",
},
},
});
await request(app.getHttpServer())
.post(`/v2/bookings/${regularUserBooking.uid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
.expect(401);
await userRepositoryFixture.delete(regularUser.id);
});
});
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
await userRepositoryFixture.delete(thirdManagedUser.user.id);
await userRepositoryFixture.delete(orgAdminManagedUser.user.id);
await userRepositoryFixture.delete(platformAdmin.id);
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -18,7 +18,6 @@ import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repo
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
@@ -27,7 +26,6 @@ import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@cal
import {
CancelBookingInput_2024_08_13,
CancelSeatedBookingInput_2024_08_13,
CreateRecurringSeatedBookingOutput_2024_08_13,
CreateSeatedBookingOutput_2024_08_13,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
@@ -35,11 +33,9 @@ import {
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
import { Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
describe("Seated bookings", () => {
let app: INestApplication;
let organization: Team;
@@ -48,7 +44,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
@@ -58,7 +53,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
let apiKeyString: string;
let seatedEventTypeId: number;
const maxRecurrenceCount = 3;
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
@@ -83,7 +77,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
@@ -190,6 +183,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
absent: false,
seatUid,
bookingFieldsResponses: {
guests: [],
name: body.attendee.name,
...body.bookingFieldsResponses,
},
@@ -264,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
absent: false,
seatUid: createdSeatedBooking.seatUid,
bookingFieldsResponses: {
guests: [],
name: createdSeatedBooking.attendees[0].name,
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
},
@@ -278,6 +273,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
absent: false,
seatUid,
bookingFieldsResponses: {
guests: [],
name: body.attendee.name,
...body.bookingFieldsResponses,
},
@@ -517,6 +513,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
absent: false,
seatUid,
bookingFieldsResponses: {
guests: [],
name: body.attendee.name,
...body.bookingFieldsResponses,
},
@@ -21,7 +21,12 @@ import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
CreateRecurringBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
} from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
@@ -44,6 +49,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const VARIABLE_LENGTH_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
let normalEventType: EventType;
const NORMAL_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`;
let variableLengthRecurringEventType: EventType;
const VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG = `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -105,6 +112,17 @@ describe("Bookings Endpoints 2024-08-13", () => {
user.id
);
variableLengthRecurringEventType = await eventTypesRepositoryFixture.create(
{
title: `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`,
slug: VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG,
length: 15,
metadata: { multipleDuration: [15, 30, 60] },
recurringEvent: { freq: 2, count: 3, interval: 1 }, // Weekly, 3 times, every 1 week
},
user.id
);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
@@ -246,6 +264,63 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("create recurring bookings", () => {
it("should create recurring bookings with custom lengthInMinutes", async () => {
const lengthInMinutes = 30;
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 1, 1, 14, 0, 0)).toISOString(),
eventTypeId: variableLengthRecurringEventType.id,
lengthInMinutes,
recurrenceCount: 2,
attendee: {
name: "Mr Recurring",
email: "mr_recurring@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: { status: string; data: RecurringBookingOutput_2024_08_13[] } = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(Array.isArray(responseBody.data)).toBe(true);
expect(responseBody.data).toHaveLength(2);
const firstBooking = responseBody.data[0];
const secondBooking = responseBody.data[1];
expect(firstBooking.duration).toEqual(lengthInMinutes);
expect(secondBooking.duration).toEqual(lengthInMinutes);
});
});
it("should reject recurring booking with invalid lengthInMinutes", async () => {
const body: CreateRecurringBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 3, 1, 10, 0, 0)).toISOString(),
eventTypeId: variableLengthRecurringEventType.id,
lengthInMinutes: 45, // Not in allowed [15, 30, 60]
recurrenceCount: 2,
attendee: {
name: "Mr Invalid",
email: "mr_invalid@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -420,6 +420,9 @@ export class InputBookingsService_2024_08_13 {
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`);
}
this.validateBookingLengthInMinutes(inputBooking, eventType);
const lengthInMinutes = inputBooking.lengthInMinutes ?? eventType.length;
const occurrence = recurringEventSchema.parse(eventType.recurringEvent);
const repeatsEvery = occurrence.interval;
@@ -456,7 +459,7 @@ export class InputBookingsService_2024_08_13 {
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
for (let i = 0; i < repeatsTimes; i++) {
const endTime = startTime.plus({ minutes: eventType.length });
const endTime = startTime.plus({ minutes: lengthInMinutes });
events.push({
start: startTime.toISO(),
@@ -702,6 +705,7 @@ export class InputBookingsService_2024_08_13 {
beforeUpdatedDate: queryParams.beforeUpdatedAt,
afterCreatedDate: queryParams.afterCreatedAt,
beforeCreatedDate: queryParams.beforeCreatedAt,
bookingUid: queryParams.bookingUid,
};
}
@@ -1,6 +1,5 @@
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { Module } from "@nestjs/common";
@@ -2147,6 +2147,51 @@ describe("Event types Endpoints", () => {
});
});
describe("EventType Hidden Property", () => {
let createdEventTypeId: number;
it("should create an event type with hidden=true", async () => {
const createPayload = {
title: "Hidden Event",
slug: "hidden-event",
lengthInMinutes: 30,
hidden: true,
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(createPayload)
.expect(201);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(createPayload.title);
expect(createdEventType.slug).toEqual(createPayload.slug);
expect(createdEventType.lengthInMinutes).toEqual(createPayload.lengthInMinutes);
expect(createdEventType.hidden).toBe(true);
createdEventTypeId = createdEventType.id;
});
it("should update the hidden property to false", async () => {
const updatePayload = {
hidden: false,
};
const response = await request(app.getHttpServer())
.patch(`/api/v2/event-types/${createdEventTypeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(updatePayload)
.expect(200);
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
+expect(updatedEventType.hidden).toBe(false);
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -89,6 +89,7 @@ type Input = Pick<
| "hideCalendarEventDetails"
| "hideOrganizerEmail"
| "calVideoSettings"
| "hidden"
>;
@Injectable()
@@ -127,6 +128,7 @@ export class OutputEventTypesService_2024_06_14 {
hideCalendarEventDetails,
hideOrganizerEmail,
calVideoSettings,
hidden,
} = databaseEventType;
const locations = this.transformLocations(databaseEventType.locations);
@@ -203,6 +205,7 @@ export class OutputEventTypesService_2024_06_14 {
hideCalendarEventDetails,
hideOrganizerEmail,
calVideoSettings,
hidden,
};
}
@@ -229,4 +229,19 @@ export class SchedulesRepository_2024_06_11 {
},
});
}
async getSchedulesByUserIds(userIds: number[], skip: number, take: number) {
return this.dbRead.prisma.schedule.findMany({
where: {
userId: {
in: userIds,
},
},
include: {
availability: true,
},
skip,
take,
});
}
}
@@ -4,10 +4,12 @@ import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/ato
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
import { AtomsVerificationController } from "@/modules/atoms/controllers/atoms.verification.controller";
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
@@ -32,6 +34,7 @@ import { Module } from "@nestjs/common";
AtomsRepository,
UsersService,
SchedulesAtomsService,
VerificationAtomsService,
RedisService,
],
exports: [EventTypesAtomService],
@@ -40,6 +43,7 @@ import { Module } from "@nestjs/common";
AtomsEventTypesController,
AtomsConferencingAppsController,
AtomsSchedulesController,
AtomsVerificationController,
],
})
export class AtomsModule {}
@@ -0,0 +1,108 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { Throttle } from "@/lib/endpoint-throttler-decorator";
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
Post,
Body,
UseGuards,
Version,
VERSION_NEUTRAL,
HttpCode,
HttpStatus,
Get,
Query,
} from "@nestjs/common";
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiResponse } from "@calcom/platform-types";
@Controller({
path: "/v2/atoms",
version: API_VERSIONS_VALUES,
})
@DocsTags("Atoms - verification endpoints for atoms")
@DocsExcludeController(true)
export class AtomsVerificationController {
constructor(private readonly verificationService: VerificationAtomsService) {}
@Post("/verification/email/send-code")
@Version(VERSION_NEUTRAL)
@HttpCode(HttpStatus.OK)
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "atoms_verification_email_send_code" })
async sendEmailVerificationCode(
@Body() body: SendVerificationEmailInput
): Promise<SendVerificationEmailOutput> {
const result = await this.verificationService.sendEmailVerificationCode({
email: body.email,
username: body.username,
language: body.language,
isVerifyingEmail: body.isVerifyingEmail,
});
return {
data: { sent: result.ok && !result.skipped },
status: SUCCESS_STATUS,
};
}
@Get("/verification/email/check")
@Version(VERSION_NEUTRAL)
@HttpCode(HttpStatus.OK)
async checkEmailVerificationRequired(
@Query() query: CheckEmailVerificationRequiredParams
): Promise<ApiResponse<boolean>> {
const required = await this.verificationService.checkEmailVerificationRequired({
email: query.email,
userSessionEmail: query.userSessionEmail,
});
return {
data: required,
status: SUCCESS_STATUS,
};
}
@Post("/verification/email/verify-code")
@Version(VERSION_NEUTRAL)
@HttpCode(HttpStatus.OK)
async verifyEmailCode(@Body() body: VerifyEmailCodeInput): Promise<VerifyEmailCodeOutput> {
const verified = await this.verificationService.verifyEmailCodeUnAuthenticated({
email: body.email,
code: body.code,
});
return {
data: { verified },
status: SUCCESS_STATUS,
};
}
@Post("/verification/email/verify-code-authenticated")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
@HttpCode(HttpStatus.OK)
async verifyEmailCodeAuthenticated(
@Body() body: VerifyEmailCodeInput,
@GetUser() user: UserWithProfile
): Promise<VerifyEmailCodeOutput> {
const verified = await this.verificationService.verifyEmailCodeAuthenticated(user, {
email: body.email,
code: body.code,
});
return {
data: { verified },
status: SUCCESS_STATUS,
};
}
}
@@ -0,0 +1,13 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEmail, IsOptional, IsString } from "class-validator";
export class CheckEmailVerificationRequiredParams {
@ApiProperty({ example: "user@example.com" })
@IsEmail()
email!: string;
@ApiPropertyOptional({ example: "user@example.com" })
@IsOptional()
@IsString()
userSessionEmail?: string;
}
@@ -0,0 +1,23 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEmail, IsOptional, IsString, IsBoolean } from "class-validator";
export class SendVerificationEmailInput {
@ApiProperty({ example: "user@example.com" })
@IsEmail()
email!: string;
@ApiPropertyOptional({ example: "johndoe" })
@IsOptional()
@IsString()
username?: string;
@ApiPropertyOptional({ example: "en" })
@IsOptional()
@IsString()
language?: string;
@ApiPropertyOptional({ example: true })
@IsOptional()
@IsBoolean()
isVerifyingEmail?: boolean;
}
@@ -0,0 +1,12 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsString } from "class-validator";
export class VerifyEmailCodeInput {
@ApiProperty({ example: "user@example.com" })
@IsEmail()
email!: string;
@ApiProperty({ example: "123456" })
@IsString()
code!: string;
}
@@ -0,0 +1,25 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsString, ValidateNested, IsBoolean } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class SendVerificationEmailData {
@ApiProperty({ example: true })
@IsBoolean()
@Expose()
readonly sent!: boolean;
}
export class SendVerificationEmailOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsString()
@Expose()
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ValidateNested()
@Type(() => SendVerificationEmailData)
@Expose()
@ApiProperty({ type: SendVerificationEmailData })
readonly data!: SendVerificationEmailData;
}
@@ -0,0 +1,25 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsString, ValidateNested, IsBoolean } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class VerifyEmailCodeData {
@ApiProperty({ example: true })
@IsBoolean()
@Expose()
readonly verified!: boolean;
}
export class VerifyEmailCodeOutput {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsString()
@Expose()
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ValidateNested()
@Type(() => VerifyEmailCodeData)
@Expose()
@ApiProperty({ type: VerifyEmailCodeData })
readonly data!: VerifyEmailCodeData;
}
@@ -0,0 +1,64 @@
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
import {
verifyCodeUnAuthenticated,
verifyCodeAuthenticated,
sendEmailVerificationByCode,
checkEmailVerificationRequired,
} from "@calcom/platform-libraries";
@Injectable()
export class VerificationAtomsService {
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
return await checkEmailVerificationRequired(input);
}
async verifyEmailCodeUnAuthenticated(input: VerifyEmailCodeInput) {
try {
return await verifyCodeUnAuthenticated(input);
} catch (error) {
if (error instanceof Error) {
if (error.message === "invalid_code") {
throw new BadRequestException("Invalid verification code");
}
if (error.message === "BAD_REQUEST") {
throw new BadRequestException("Email and code are required");
}
}
throw new BadRequestException("Verification failed");
}
}
async verifyEmailCodeAuthenticated(user: UserWithProfile, input: VerifyEmailCodeInput) {
try {
return await verifyCodeAuthenticated({
user,
email: input.email,
code: input.code,
});
} catch (error) {
if (error instanceof Error) {
if (error.message === "invalid_code") {
throw new BadRequestException("Invalid verification code");
}
if (error.message === "BAD_REQUEST") {
throw new BadRequestException("Email, code, and user ID are required");
}
}
throw new UnauthorizedException("Verification failed");
}
}
async sendEmailVerificationCode(input: SendVerificationEmailInput) {
return await sendEmailVerificationByCode({
email: input.email,
username: input.username,
language: input.language,
isVerifyingEmail: input.isVerifyingEmail,
});
}
}
@@ -6,7 +6,6 @@ import { NextAuthStrategy } from "@/modules/auth/strategies/next-auth/next-auth.
import { DeploymentsModule } from "@/modules/deployments/deployments.module";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { ProfilesModule } from "@/modules/profiles/profiles.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
@@ -22,7 +21,6 @@ import { PassportModule } from "@nestjs/passport";
MembershipsModule,
TokensModule,
DeploymentsModule,
ProfilesModule,
],
providers: [NextAuthGuard, NextAuthStrategy, ApiAuthGuard, ApiAuthStrategy, OAuthFlowService],
exports: [NextAuthGuard, ApiAuthGuard],
@@ -4,6 +4,7 @@ import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { DeploymentsRepository } from "@/modules/deployments/deployments.repository";
import { DeploymentsService } from "@/modules/deployments/deployments.service";
import { JwtService } from "@/modules/jwt/jwt.service";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
@@ -20,6 +21,7 @@ import { Test, TestingModule } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User } from "@prisma/client";
import { createRequest } from "node-mocks-http";
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
@@ -45,6 +47,7 @@ describe("ApiAuthStrategy", () => {
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
let oAuthClientRepositoryFixture: OAuthClientRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipRepositoryFixture: MembershipRepositoryFixture;
const validApiKeyEmail = `api-auth-api-key-user-${randomString()}@api.com`;
const validAccessTokenEmail = `api-auth-access-token-user-${randomString()}@api.com`;
@@ -65,6 +68,7 @@ describe("ApiAuthStrategy", () => {
}),
ProfilesModule,
TokensModule,
MembershipsModule,
],
providers: [
MockedRedisService,
@@ -91,6 +95,7 @@ describe("ApiAuthStrategy", () => {
teamRepositoryFixture = new TeamRepositoryFixture(module);
oAuthClientRepositoryFixture = new OAuthClientRepositoryFixture(module);
profilesRepositoryFixture = new ProfileRepositoryFixture(module);
membershipRepositoryFixture = new MembershipRepositoryFixture(module);
organization = await teamRepositoryFixture.create({ name: `api-auth-organization-1-${randomString()}` });
organizationTwo = await teamRepositoryFixture.create({
name: `api-auth-organization-2-${randomString()}`,
@@ -121,6 +126,20 @@ describe("ApiAuthStrategy", () => {
organization: { connect: { id: organizationTwo.id } },
});
await membershipRepositoryFixture.create({
user: { connect: { id: validOAuthUser.id } },
team: { connect: { id: organization.id } },
role: "OWNER",
accepted: true,
});
await membershipRepositoryFixture.create({
user: { connect: { id: validOAuthUser.id } },
team: { connect: { id: organizationTwo.id } },
role: "OWNER",
accepted: true,
});
const data = {
logo: "logo-url",
name: "name",
@@ -4,9 +4,9 @@ import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
import { BaseStrategy } from "@/lib/passport/strategies/types";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { DeploymentsService } from "@/modules/deployments/deployments.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { TokensService } from "@/modules/tokens/tokens.service";
import { UsersService } from "@/modules/users/services/users.service";
@@ -45,8 +45,8 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
private readonly userRepository: UsersRepository,
private readonly apiKeyRepository: ApiKeysRepository,
private readonly oauthRepository: OAuthClientRepository,
private readonly profilesRepository: ProfilesRepository,
private readonly usersService: UsersService
private readonly usersService: UsersService,
private readonly membershipsRepository: MembershipsRepository
) {
super();
}
@@ -172,7 +172,9 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
throw new UnauthorizedException("ApiAuthStrategy - oAuth client - Invalid client secret");
}
const platformCreatorId = await this.profilesRepository.getPlatformOwnerUserId(client.organizationId);
const platformCreatorId =
(await this.membershipsRepository.findPlatformOwnerUserId(client.organizationId)) ||
(await this.membershipsRepository.findPlatformAdminUserId(client.organizationId));
if (!platformCreatorId) {
throw new UnauthorizedException(
@@ -11,6 +11,7 @@ import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
import { RouterModule } from "@/modules/router/router.module";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TeamsSchedulesModule } from "@/modules/teams/schedules/teams-schedules.module";
import { TimezoneModule } from "@/modules/timezones/timezones.module";
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
@@ -38,6 +39,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
OrganizationsRoutingFormsModule,
VerifiedResourcesModule,
RouterModule,
TeamsSchedulesModule,
],
})
export class EndpointsModule implements NestModule {
@@ -20,6 +20,36 @@ export class MembershipsRepository {
return membership;
}
async findPlatformOwnerUserId(organizationId: number): Promise<number | undefined> {
const owner = await this.dbRead.prisma.membership.findFirst({
where: {
teamId: organizationId,
role: "OWNER",
accepted: true,
},
select: {
userId: true,
},
});
return owner?.userId ?? undefined;
}
async findPlatformAdminUserId(organizationId: number): Promise<number | undefined> {
const admin = await this.dbRead.prisma.membership.findFirst({
where: {
teamId: organizationId,
role: "ADMIN",
accepted: true,
},
select: {
userId: true,
},
});
return admin?.userId ?? undefined;
}
async findMembershipByTeamId(teamId: number, userId: number) {
const membership = await this.dbRead.prisma.membership.findUnique({
where: {
@@ -15,6 +15,7 @@ import { NestExpressApplication } from "@nestjs/platform-express";
import { Test, TestingModule } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
@@ -63,6 +64,7 @@ describe("OAuthFlow Endpoints", () => {
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
let oAuthClientsRepositoryFixture: OAuthClientRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipRepositoryFixture: MembershipRepositoryFixture;
let user: User;
let organization: Team;
@@ -89,6 +91,7 @@ describe("OAuthFlow Endpoints", () => {
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
usersRepositoryFixtures = new UserRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
user = await usersRepositoryFixtures.create({
email: userEmail,
@@ -104,6 +107,13 @@ describe("OAuthFlow Endpoints", () => {
movedFromUser: { connect: { id: user.id } },
organization: { connect: { id: organization.id } },
});
await membershipRepositoryFixture.create({
user: { connect: { id: user.id } },
team: { connect: { id: organization.id } },
role: "OWNER",
accepted: true,
});
oAuthClient = await createOAuthClient(organization.id);
});
@@ -66,6 +66,8 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
let orgEventTypeId2: number;
let nonOrgEventTypeId: number;
let collectiveOrgBookingUid: string;
beforeAll(async () => {
const moduleRef = await withApiAuth(
orgUserEmail2,
@@ -91,12 +93,17 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await organizationsRepositoryFixture.create({ name: "organization bookings" });
oAuthClient = await createOAuthClient(organization.id);
team1 = await teamRepositoryFixture.create({
name: "team orgs booking 1",
isOrganization: false,
parent: { connect: { id: organization.id } },
createdByOAuthClient: {
connect: {
id: oAuthClient.id,
},
},
});
oAuthClient = await createOAuthClient(organization.id);
nonOrgUser1 = await userRepositoryFixture.create({
email: nonOrgUserEmail1,
@@ -315,6 +322,7 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
collectiveOrgBookingUid = data.uid;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts.length).toEqual(1);
@@ -669,6 +677,125 @@ describe("Organizations Bookings Endpoints 2024-08-13", () => {
);
});
});
describe("get by bookingUid param", () => {
it("should get a specific booking by bookingUid query param", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${collectiveOrgBookingUid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(1);
expect(responseDataIsBooking(data[0])).toBe(true);
if (responseDataIsBooking(data[0])) {
const booking: BookingOutput_2024_08_13 = data[0];
expect(booking.uid).toEqual(collectiveOrgBookingUid);
expect(booking.eventTypeId).toEqual(orgEventTypeId);
expect(booking.status).toEqual("accepted");
expect(booking.attendees.length).toEqual(2);
expect(booking.attendees[0].name).toEqual("alice");
expect(booking.attendees[0].email).toEqual("alice@gmail.com");
} else {
throw new Error("Expected single booking but received different response type");
}
});
});
it("should return empty array for non-existent booking UID query param", async () => {
const nonExistentUid = "non-existent-booking-uid";
return request(app.getHttpServer())
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${nonExistentUid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.length).toEqual(0);
});
});
it("should return empty array for booking UID not belonging to the organization", async () => {
const regularUser = await userRepositoryFixture.create({
email: `org-bookings-regular-user-${Math.floor(Math.random() * 10000)}@api.com`,
name: "Regular User",
});
const regularUserEventType = await eventTypesRepositoryFixture.create(
{
title: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
slug: `regular-user-event-type-${Math.floor(Math.random() * 10000)}`,
length: 60,
bookingFields: [],
locations: [],
},
regularUser.id
);
const regularUserBooking = await bookingsRepositoryFixture.create({
user: {
connect: {
id: regularUser.id,
},
},
startTime: new Date(Date.UTC(2030, 0, 15, 13, 0, 0)),
endTime: new Date(Date.UTC(2030, 0, 15, 14, 0, 0)),
title: "Regular user booking",
uid: `regular-user-booking-${Math.floor(Math.random() * 10000)}`,
eventType: {
connect: {
id: regularUserEventType.id,
},
},
location: "https://meet.google.com/regular-user",
customInputs: {},
metadata: {},
status: "ACCEPTED",
responses: {
name: "Regular Attendee",
email: "regular@example.com",
},
attendees: {
create: {
email: "regular@example.com",
name: "Regular Attendee",
locale: "en",
timeZone: "Europe/Rome",
},
},
});
return request(app.getHttpServer())
.get(`/v2/organizations/${organization.id}/bookings?bookingUid=${regularUserBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set(X_CAL_CLIENT_ID, oAuthClient.id)
.set(X_CAL_SECRET_KEY, oAuthClient.secret)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.length).toEqual(0);
await userRepositoryFixture.delete(regularUser.id);
});
});
});
});
async function createOAuthClient(organizationId: number) {
@@ -39,6 +39,7 @@ import {
HttpStatus,
NotFoundException,
Query,
Logger,
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
@@ -66,6 +67,8 @@ export type EventTypeHandlerResponse = {
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsEventTypesController {
private readonly logger = new Logger("OrganizationsEventTypesController");
constructor(
private readonly organizationsEventTypesService: OrganizationsEventTypesService,
private readonly inputService: InputOrganizationsEventTypesService,
@@ -88,6 +91,10 @@ export class OrganizationsEventTypesController {
teamId,
bodyEventType
);
this.logger.debug(
"nl debug - create org team event type - transformedBody",
JSON.stringify(transformedBody, null, 2)
);
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
user,
@@ -47,6 +47,10 @@ export class OrganizationsEventTypesService {
prisma: this.dbWrite.prisma,
},
});
this.logger.debug(
"nl debug - create org team event type - eventTypeCreated",
JSON.stringify(eventTypeCreated, null, 2)
);
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
}
@@ -74,6 +74,7 @@ type Input = Pick<
| "hideOrganizerEmail"
| "team"
| "calVideoSettings"
| "hidden"
>;
@Injectable()
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { TeamService } from "@calcom/platform-libraries";
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
}
async deleteOrgMembership(organizationId: number, membershipId: number) {
const membership = await this.organizationsMembershipRepository.deleteOrgMembership(
// Get the membership first to get the userId
const membership = await this.organizationsMembershipRepository.findOrgMembership(
organizationId,
membershipId
);
if (!membership) {
throw new NotFoundException(
`Membership with id ${membershipId} within organization id ${organizationId} not found`
);
}
await TeamService.removeMembers({
teamIds: [organizationId],
userIds: [membership.userId],
isOrg: true,
});
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
}
@@ -1,5 +1,9 @@
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
import { InputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/input-schedules.service";
import { SchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/schedules.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { ConferencingRepository } from "@/modules/conferencing/repositories/conferencing.repository";
import { ConferencingService } from "@/modules/conferencing/services/conferencing.service";
@@ -18,13 +22,10 @@ import { OrganizationAttributesService } from "@/modules/organizations/attribute
import { OrganizationAttributeOptionRepository } from "@/modules/organizations/attributes/options/organization-attribute-options.repository";
import { OrganizationsAttributesOptionsController } from "@/modules/organizations/attributes/options/organizations-attributes-options.controller";
import { OrganizationAttributeOptionService } from "@/modules/organizations/attributes/options/services/organization-attributes-option.service";
import { OrganizationsConferencingController } from "@/modules/organizations/conferencing/organizations-conferencing.controller";
import { OrganizationsConferencingModule } from "@/modules/organizations/conferencing/organizations-conferencing.module";
import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service";
import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module";
import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller";
import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module";
import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller";
import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository";
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service";
@@ -38,7 +39,6 @@ import { OrganizationsMembershipOutputService } from "@/modules/organizations/me
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
import { OrganizationsOrganizationsModule } from "@/modules/organizations/organizations/organizations-organizations.module";
import { OrganizationsSchedulesController } from "@/modules/organizations/schedules/organizations-schedules.controller";
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
import { OrganizationsSchedulesService } from "@/modules/organizations/schedules/services/organizations-schedules.service";
import { OrganizationsStripeModule } from "@/modules/organizations/stripe/organizations-stripe.module";
import { OrganizationsStripeService } from "@/modules/organizations/stripe/services/organizations-stripe.service";
@@ -65,6 +65,7 @@ import { RedisModule } from "@/modules/redis/redis.module";
import { RedisService } from "@/modules/redis/redis.service";
import { StripeModule } from "@/modules/stripe/stripe.module";
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
import { TeamsModule } from "@/modules/teams/teams/teams.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
@@ -102,7 +103,6 @@ import { Module } from "@nestjs/common";
OrganizationsTeamsService,
MembershipsRepository,
OrganizationsSchedulesService,
OrganizationSchedulesRepository,
OrganizationsUsersRepository,
OrganizationsUsersService,
EmailService,
@@ -144,6 +144,9 @@ import { Module } from "@nestjs/common";
TeamWorkflowsService,
WorkflowsInputService,
WorkflowsOutputService,
TeamsSchedulesService,
SchedulesService_2024_06_11,
InputSchedulesService_2024_06_11,
],
exports: [
OrganizationsService,
@@ -1,5 +1,5 @@
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
@@ -8,8 +8,8 @@ import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
@Injectable()
export class OrganizationsSchedulesService {
constructor(
private readonly organizationSchedulesService: OrganizationSchedulesRepository,
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11,
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
private readonly usersRepository: UsersRepository
) {}
@@ -17,7 +17,7 @@ export class OrganizationsSchedulesService {
const users = await this.usersRepository.getOrganizationUsers(organizationId);
const usersIds = users.map((user) => user.id);
const schedules = await this.organizationSchedulesService.getSchedulesByUserIds(usersIds, skip, take);
const schedules = await this.schedulesRepository.getSchedulesByUserIds(usersIds, skip, take);
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
@@ -20,7 +20,6 @@ import {
OrgTeamMembershipOutputResponseDto,
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
import {
Controller,
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
constructor(
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
private teamsEventTypesService: TeamsEventTypesService,
private readonly organizationsRepository: OrganizationsRepository
) {}
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
membershipId
);
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
return {
status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { TeamService } from "@calcom/platform-libraries";
@Injectable()
export class OrganizationsTeamsMembershipsService {
constructor(
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
}
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById(
// First get the membership to get the userId
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
organizationId,
teamId,
membershipId
);
if (!teamMembership) {
throw new NotFoundException(
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
);
}
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
return teamMembership;
}
}
@@ -14,11 +14,12 @@ 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 { IsUserInOrgTeam } from "@/modules/auth/guards/users/is-user-in-org-team.guard";
import { Controller, UseGuards, Get, Param, ParseIntPipe } from "@nestjs/common";
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
@Controller({
path: "/v2/organizations/:orgId/teams/:teamId",
@@ -29,7 +30,31 @@ import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
@ApiHeader(OPTIONAL_API_KEY_HEADER)
export class OrganizationsTeamsSchedulesController {
constructor(private schedulesService: SchedulesService_2024_06_11) {}
constructor(
private schedulesService: SchedulesService_2024_06_11,
private teamsSchedulesService: TeamsSchedulesService
) {}
@UseGuards(IsTeamInOrg)
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@Get("/schedules")
@DocsTags("Orgs / Teams / Schedules")
@ApiOperation({ summary: "Get all team member schedules" })
async getTeamSchedules(
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: SkipTakePagination
): Promise<GetSchedulesOutput_2024_06_11> {
const { skip, take } = queryParams;
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
return {
status: SUCCESS_STATUS,
data: schedules,
};
}
@Roles("TEAM_ADMIN")
@PlatformPlan("ESSENTIALS")
@@ -18,6 +18,7 @@ import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
import { ApiSuccessResponse, ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
import { Team, Schedule } from "@calcom/prisma/client";
@@ -36,6 +37,8 @@ describe("Organizations Teams Schedules Endpoints", () => {
let org: Team;
let orgTeam: Team;
let nonOrgTeam: Team;
let userSchedule: Schedule;
let user2Schedule: Schedule;
const userEmail = `organizations-teams-schedules-admin-${randomString()}@api.com`;
@@ -73,6 +76,16 @@ describe("Organizations Teams Schedules Endpoints", () => {
username: userEmail2,
});
userSchedule = await scheduleRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
name: `organizations-teams-schedules-user-schedule-${randomString()}`,
timeZone: "America/New_York",
});
user2Schedule = await scheduleRepositoryFixture.create({
user: {
connect: {
@@ -214,6 +227,32 @@ describe("Organizations Teams Schedules Endpoints", () => {
});
});
it("should get all the schedules of members in a team", async () => {
return request(app.getHttpServer())
.get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/schedules`)
.expect(200)
.then((response) => {
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(Array.isArray(responseBody.data)).toBe(true);
expect(responseBody.data.length).toEqual(2);
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
expect(userOneSchedule).toBeDefined();
expect(userTwoSchedule).toBeDefined();
expect(userOneSchedule?.id).toEqual(userSchedule.id);
expect(userOneSchedule?.name).toEqual(userSchedule.name);
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await userRepositoryFixture.deleteByEmail(userToInviteViaApi.email);
@@ -12,7 +12,12 @@ import {
EMAIL,
WorkflowEmailAttendeeStepDto,
} from "@/modules/workflows/inputs/workflow-step.input";
import { BEFORE_EVENT, DAY } from "@/modules/workflows/inputs/workflow-trigger.input";
import {
BEFORE_EVENT,
DAY,
OnAfterEventTriggerDto,
OnBeforeEventTriggerDto,
} from "@/modules/workflows/inputs/workflow-trigger.input";
// Adjust path if needed
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
import { INestApplication } from "@nestjs/common";
@@ -255,6 +260,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
"+37255555555"
);
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
const trigger = sampleCreateWorkflowDto.trigger as OnBeforeEventTriggerDto;
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
createdWorkflowId = responseBody.data.id;
createdWorkflow = responseBody.data;
});
@@ -330,6 +339,13 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
expect(step1).toBeDefined();
const partialUpdateDto: Partial<CreateWorkflowDto> = {
name: updatedName,
trigger: {
type: "afterEvent",
offset: {
unit: "minute",
value: 10,
},
},
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
};
expect(createdWorkflowId).toBeDefined();
@@ -348,6 +364,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
expect(responseBody.data.steps[1]?.id).toBeUndefined();
const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto;
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
});
});
@@ -133,6 +133,7 @@ export class TeamsEventTypesService {
});
const eventType = await this.teamsEventTypesRepository.getEventTypeById(eventTypeId);
this.logger.debug("nl debug - update team event type - eventType", JSON.stringify(eventType, null, 2));
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
@@ -156,17 +157,4 @@ export class TeamsEventTypesService {
return this.eventTypesRepository.deleteEventType(eventTypeId);
}
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
try {
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
} catch (err) {
this.logger.error("Could not remove user from all team event-types.", {
error: err,
userId,
teamId,
});
}
}
}
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
export class TeamsMembershipsController {
private logger = new Logger("TeamsMembershipsController");
constructor(
private teamsMembershipsService: TeamsMembershipsService,
private teamsEventTypesService: TeamsEventTypesService
) {}
constructor(private teamsMembershipsService: TeamsMembershipsService) {}
@Roles("TEAM_ADMIN")
@Post("/")
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
membershipId,
body
);
if (!currentMembership.accepted && updatedMembership.accepted) {
try {
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
): Promise<DeleteTeamMembershipOutput> {
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
return {
status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import { TeamService } from "@calcom/platform-libraries";
@Injectable()
export class TeamsMembershipsService {
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
}
async deleteTeamMembership(teamId: number, membershipId: number) {
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById(
teamId,
membershipId
);
// First get the membership to get the userId
const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
if (!teamMembership) {
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
}
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
return teamMembership;
}
}
@@ -0,0 +1,41 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetSchedulesOutput_2024_06_11, SkipTakePagination } from "@calcom/platform-types";
@Controller({
path: "/v2/teams/:teamId/schedules",
version: API_VERSIONS_VALUES,
})
@UseGuards(ApiAuthGuard, RolesGuard)
@DocsTags("Teams / Schedules")
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
export class TeamsSchedulesController {
constructor(private teamsSchedulesService: TeamsSchedulesService) {}
@Roles("TEAM_ADMIN")
@Get("/")
@ApiOperation({
summary: "Get all team member schedules",
})
async getTeamSchedules(
@Param("teamId", ParseIntPipe) teamId: number,
@Query() queryParams: SkipTakePagination
): Promise<GetSchedulesOutput_2024_06_11> {
const { skip, take } = queryParams;
const schedules = await this.teamsSchedulesService.getTeamSchedules(teamId, skip, take);
return {
status: SUCCESS_STATUS,
data: schedules,
};
}
}
@@ -0,0 +1,143 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
import { Team, Schedule } from "@calcom/prisma/client";
describe("Teams Schedules Endpoints", () => {
describe("User Authentication - User is Team Admin", () => {
let app: INestApplication;
let userRepositoryFixture: UserRepositoryFixture;
let teamsRepositoryFixture: TeamRepositoryFixture;
let scheduleRepositoryFixture: SchedulesRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
let nonOrgTeam: Team;
let userSchedule: Schedule;
let user2Schedule: Schedule;
const userEmail = `teams-schedules-admin-${randomString()}@api.com`;
const userEmail2 = `teams-schedules-member-${randomString()}@api.com`;
let user: User;
let user2: User;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
})
).compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
username: userEmail,
});
user2 = await userRepositoryFixture.create({
email: userEmail2,
username: userEmail2,
});
userSchedule = await scheduleRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
name: `teams-schedules-user-schedule-${randomString()}`,
timeZone: "America/New_York",
});
user2Schedule = await scheduleRepositoryFixture.create({
user: {
connect: {
id: user2.id,
},
},
name: `teams-schedules-user2-schedule-${randomString()}`,
timeZone: "America/New_York",
});
nonOrgTeam = await teamsRepositoryFixture.create({
name: `teams-schedules-non-org-team-${randomString()}`,
isOrganization: false,
});
await membershipsRepositoryFixture.create({
role: "ADMIN",
user: { connect: { id: user.id } },
team: { connect: { id: nonOrgTeam.id } },
});
await membershipsRepositoryFixture.create({
role: "MEMBER",
user: { connect: { id: user2.id } },
team: { connect: { id: nonOrgTeam.id } },
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
it("should get all the schedules of members in a team", async () => {
return request(app.getHttpServer())
.get(`/v2/teams/${nonOrgTeam.id}/schedules`)
.expect(200)
.then((response) => {
const responseBody: GetSchedulesOutput_2024_06_11 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(Array.isArray(responseBody.data)).toBe(true);
expect(responseBody.data.length).toEqual(2);
const userOneSchedule = responseBody.data.find((schedule) => schedule.id === userSchedule.id);
const userTwoSchedule = responseBody.data.find((schedule) => schedule.id === user2Schedule.id);
expect(userOneSchedule).toBeDefined();
expect(userTwoSchedule).toBeDefined();
expect(userOneSchedule?.id).toEqual(userSchedule.id);
expect(userOneSchedule?.name).toEqual(userSchedule.name);
expect(userOneSchedule?.timeZone).toEqual(userSchedule.timeZone);
expect(userTwoSchedule?.id).toEqual(user2Schedule.id);
expect(userTwoSchedule?.name).toEqual(user2Schedule.name);
expect(userOneSchedule?.timeZone).toEqual(user2Schedule.timeZone);
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await userRepositoryFixture.deleteByEmail(user2.email);
await teamsRepositoryFixture.delete(nonOrgTeam.id);
await app.close();
});
});
});
@@ -0,0 +1,28 @@
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { Injectable } from "@nestjs/common";
import { ScheduleOutput_2024_06_11 } from "@calcom/platform-types";
@Injectable()
export class TeamsSchedulesService {
constructor(
private readonly teamsRepository: TeamsRepository,
private readonly schedulesRepository: SchedulesRepository_2024_06_11,
private readonly outputSchedulesService: OutputSchedulesService_2024_06_11
) {}
async getTeamSchedules(teamId: number, skip = 0, take = 250) {
const userIds = await this.teamsRepository.getTeamUsersIds(teamId);
const schedules = await this.schedulesRepository.getSchedulesByUserIds(userIds, skip, take);
const responseSchedules: ScheduleOutput_2024_06_11[] = [];
for (const schedule of schedules) {
responseSchedules.push(await this.outputSchedulesService.getResponseSchedule(schedule));
}
return responseSchedules;
}
}
@@ -0,0 +1,43 @@
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { OutputSchedulesService_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/services/output-schedules.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { MembershipsModule } from "@/modules/memberships/memberships.module";
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
import { OrganizationSchedulesRepository } from "@/modules/organizations/schedules/organizations-schedules.repository";
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { StripeService } from "@/modules/stripe/stripe.service";
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
import { TeamsSchedulesController } from "@/modules/teams/schedules/controllers/teams-schedules.controller";
import { TeamsSchedulesService } from "@/modules/teams/schedules/services/teams-schedules.service";
import { TeamsController } from "@/modules/teams/teams/controllers/teams.controller";
import { TeamsService } from "@/modules/teams/teams/services/teams.service";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, MembershipsModule, RedisModule],
providers: [
TeamsRepository,
TeamsService,
TeamsMembershipsRepository,
TeamsMembershipsService,
OutputSchedulesService_2024_06_11,
OrganizationSchedulesRepository,
StripeService,
UsersRepository,
AppsRepository,
CredentialsRepository,
OrganizationsRepository,
TeamsSchedulesService,
OrganizationsTeamsRepository,
SchedulesRepository_2024_06_11,
],
controllers: [TeamsController, TeamsSchedulesController],
exports: [TeamsRepository],
})
export class TeamsSchedulesModule {}
@@ -1,7 +1,7 @@
import { ApiProperty } from "@nestjs/swagger";
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
import { Type } from "class-transformer";
import { IsNumber, ValidateNested } from "class-validator";
import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
export const BEFORE_EVENT = "beforeEvent";
export const EVENT_CANCELLED = "eventCancelled";
@@ -68,6 +68,8 @@ export class WorkflowTriggerOffsetDto {
value!: number;
@ApiProperty({ description: "Unit for the offset time", example: HOUR })
@IsString()
@IsIn(TIME_UNITS)
unit!: TimeUnitType;
}
@@ -75,6 +77,8 @@ export class BaseWorkflowTriggerDto {
@ApiProperty({
description: "Trigger type for the workflow",
})
@IsString()
@IsIn([WORKFLOW_TRIGGER_TYPES])
type!: WorkflowTriggerType;
}
@@ -82,6 +86,8 @@ export class OnCreationTriggerDto {
@ApiProperty({
description: "Trigger type for the workflow",
})
@IsString()
@IsIn([NEW_EVENT])
type: typeof NEW_EVENT = NEW_EVENT;
}
@@ -89,12 +95,16 @@ export class OnRescheduleTriggerDto {
@ApiProperty({
description: "Trigger type for the workflow",
})
@IsString()
@IsIn([RESCHEDULE_EVENT])
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
}
export class OnCancelTriggerDto {
@ApiProperty({
description: "Trigger type for the workflow",
})
@IsString()
@IsIn([EVENT_CANCELLED])
type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
}
@@ -113,6 +123,8 @@ export class OnBeforeEventTriggerDto extends TriggerOffsetDTO {
description: "Trigger type for the workflow",
example: BEFORE_EVENT,
})
@IsString()
@IsIn([BEFORE_EVENT])
type: typeof BEFORE_EVENT = BEFORE_EVENT;
}
@@ -121,6 +133,8 @@ export class OnAfterEventTriggerDto extends TriggerOffsetDTO {
description: "Trigger type for the workflow",
example: AFTER_EVENT,
})
@IsString()
@IsIn([AFTER_EVENT])
type: typeof AFTER_EVENT = AFTER_EVENT;
}
@@ -129,6 +143,8 @@ export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO {
description: "Trigger type for the workflow",
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
})
@IsString()
@IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW])
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
}
@@ -137,5 +153,7 @@ export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO {
description: "Trigger type for the workflow",
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
})
@IsString()
@IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW])
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
}
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
async delete(eventTypeId: EventType["id"]) {
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
}
async findById(eventTypeId: EventType["id"]) {
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
}
}
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
return membership;
}
async findById(membershipId: Membership["id"]) {
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
}
}
@@ -1,6 +1,6 @@
import { CustomI18nProvider } from "app/CustomI18nProvider";
import { withAppDirSsr } from "app/WithAppDirSsr";
import type { PageProps, SearchParams } from "app/_types";
import type { PageProps, Params, SearchParams } from "app/_types";
import { generateMeetingMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
@@ -15,20 +15,34 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
import LegacyPage from "~/team/type-view";
import type { PageProps as LegacyPageProps } from "~/team/type-view";
import CachedTeamBooker, { generateMetadata as generateCachedMetadata } from "./pageWithCachedData";
import CachedTeamBooker, {
generateMetadata as generateCachedMetadata,
getOrgContext,
} from "./pageWithCachedData";
import { getTeamId } from "./queries";
async function isCachedTeamBookingEnabled(params: Params, searchParams: SearchParams): Promise<boolean> {
if (searchParams.experimentalTeamBookingPageCache !== "true") return false;
const { teamSlug, currentOrgDomain, isValidOrgDomain } = await getOrgContext(params);
const orgSlug = isValidOrgDomain ? currentOrgDomain : null;
const teamId = await getTeamId(teamSlug, orgSlug);
if (!teamId) return false;
async function isCachedTeamBookingEnabled(searchParams: SearchParams): Promise<boolean> {
const featuresRepository = new FeaturesRepository(prisma);
const isGloballyEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
const isTeamFeatureEnabled = await featuresRepository.checkIfTeamHasFeature(
teamId,
"team-booking-page-cache"
);
return isGloballyEnabled && searchParams.experimentalTeamBookingPageCache === "true";
return isTeamFeatureEnabled;
}
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
if (await isCachedTeamBookingEnabled(await searchParams)) {
if (await isCachedTeamBookingEnabled(await params, await searchParams)) {
return await generateCachedMetadata({ params, searchParams });
}
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
const props = await getData(legacyCtx);
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
@@ -68,7 +82,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
const ServerPage = async ({ params, searchParams }: PageProps) => {
if (await isCachedTeamBookingEnabled(await searchParams)) {
if (await isCachedTeamBookingEnabled(await params, await searchParams)) {
return await CachedTeamBooker({ params, searchParams });
}
@@ -41,7 +41,7 @@ const _getTeamMetadataForBooking = (teamData: NonNullable<TeamData>, eventTypeId
};
};
async function _getOrgContext(params: Params) {
export async function getOrgContext(params: Params) {
const result = paramsSchema.safeParse({
slug: params?.slug,
type: params?.type,
@@ -74,7 +74,7 @@ const _getMultipleDurationValue = (
};
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await getOrgContext(await params);
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
if (!teamData) return {}; // should never happen
@@ -123,7 +123,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
};
const CachedTeamBooker = async ({ params, searchParams }: PageProps) => {
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await getOrgContext(await params);
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
// Handle org redirects
@@ -13,6 +13,7 @@ import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventTy
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { TeamRepository } from "@calcom/lib/server/repository/team";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { prisma } from "@calcom/prisma";
import type { SchedulingType } from "@calcom/prisma/enums";
@@ -158,3 +159,14 @@ export async function getCRMData(
crmRecordId,
};
}
export async function getTeamId(teamSlug: string, orgSlug: string | null): Promise<number | null> {
const teamRepo = new TeamRepository(prisma);
const team = await teamRepo.findFirstBySlugAndParentSlug({
slug: teamSlug,
parentSlug: orgSlug,
select: { id: true },
});
return team?.id ?? null;
}
@@ -1,9 +1,14 @@
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
import type { PageProps } from "app/_types";
import { _generateMetadata, getTranslate } from "app/_utils";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validStatuses } from "~/bookings/lib/validStatuses";
import BookingsList from "~/bookings/views/bookings-listing-view";
@@ -26,10 +31,11 @@ const Page = async ({ params }: PageProps) => {
redirect("/bookings/upcoming");
}
const t = await getTranslate();
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
return (
<ShellMainAppDir heading={t("bookings")} subtitle={t("bookings_description")}>
<BookingsList status={parsed.data.status} />
<BookingsList status={parsed.data.status} userId={session?.user?.id} />
</ShellMainAppDir>
);
};
@@ -45,11 +45,13 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
createRouterCaller(appsRouter),
]);
const connectedCalendars = await calendarsCaller.connectedCalendars();
const installedCalendars = await appsCaller.integrations({
variant: "calendar",
onlyInstalled: true,
});
const [connectedCalendars, installedCalendars] = await Promise.all([
calendarsCaller.connectedCalendars(),
appsCaller.integrations({
variant: "calendar",
onlyInstalled: true,
}),
]);
return (
<InstalledApps
@@ -5,6 +5,7 @@ import { unstable_cache } from "next/cache";
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
import prisma from "@calcom/prisma";
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
import { TeamMembersView } from "~/teams/team-members-view";
@@ -36,8 +37,10 @@ const getCachedTeamRoles = unstable_cache(
const getCachedTeamAttributes = unstable_cache(
async (organizationId?: number) => {
if (!organizationId) return [];
const attributeRepo = new PrismaAttributeRepository(prisma);
try {
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId: organizationId });
return await attributeRepo.findAllByOrgIdWithOptions({ orgId: organizationId });
} catch (error) {
return [];
}
@@ -4,6 +4,7 @@ import { unstable_cache } from "next/cache";
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
import { PrismaAttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
import prisma from "@calcom/prisma";
import { viewerOrganizationsRouter } from "@calcom/trpc/server/routers/viewer/organizations/_router";
import { MembersView } from "~/members/members-view";
@@ -19,7 +20,9 @@ export const generateMetadata = async () =>
const getCachedAttributes = unstable_cache(
async (orgId: number) => {
return await PrismaAttributeRepository.findAllByOrgIdWithOptions({ orgId });
const attributeRepo = new PrismaAttributeRepository(prisma);
return await attributeRepo.findAllByOrgIdWithOptions({ orgId });
},
undefined,
{ revalidate: 3600, tags: ["viewer.attributes.list"] } // Cache for 1 hour
@@ -1,4 +1,3 @@
import { _generateMetadata } from "app/_utils";
import { notFound } from "next/navigation";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
+2 -1
View File
@@ -5,6 +5,7 @@ import { getLocale } from "@calcom/features/auth/lib/getLocale";
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
import { buildCanonical } from "@calcom/lib/next-seo.config";
import { getTranslation } from "@calcom/lib/server/i18n";
import { truncateOnWord } from "@calcom/lib/text";
@@ -32,7 +33,7 @@ const _generateMetadataWithoutImage = async (
const description = getDescription(t);
const titleSuffix = `| ${APP_NAME}`;
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
const metadataBase = new URL(IS_CALCOM ? getCalcomUrl() : WEBAPP_URL);
return {
title: title.length === 0 ? APP_NAME : displayedTitle,
+18 -3
View File
@@ -87,7 +87,10 @@ async function handler(req: NextRequest) {
return new Response(img.body, {
status: 200,
headers: { "Content-Type": "image/png", "cache-control": "max-age=0" },
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
},
});
} catch (error) {
if (error instanceof ZodError) {
@@ -116,7 +119,13 @@ async function handler(req: NextRequest) {
});
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
return new Response(img.body, {
status: 200,
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
},
});
} catch (error) {
if (error instanceof ZodError) {
return new Response(
@@ -144,7 +153,13 @@ async function handler(req: NextRequest) {
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig);
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
return new Response(img.body, {
status: 200,
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
},
});
} catch (error) {
if (error instanceof ZodError) {
return new Response(
+2 -1
View File
@@ -9,6 +9,7 @@ import Script from "next/script";
import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
import { buildCanonical } from "@calcom/lib/next-seo.config";
import { IconSprites } from "@calcom/ui/components/icon";
@@ -64,7 +65,7 @@ function PageWrapper(props: AppProps) {
// Set canonical to https://cal.com or self-hosted URL
canonical={
IS_CALCOM
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
? buildCanonical({ path, origin: getCalcomUrl() }) // cal.com & .dev
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
}
{...seoConfig.defaultNextSeo}
@@ -2,9 +2,11 @@ import { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
import type { TLocationOptions } from "@calcom/features/eventtypes/components/Locations";
import type { TEventTypeLocation } from "@calcom/features/eventtypes/components/Locations";
import Locations from "@calcom/features/eventtypes/components/Locations";
import type {
TLocationOptions,
TEventTypeLocation,
} from "@calcom/features/eventtypes/components/locations/Locations";
import Locations from "@calcom/features/eventtypes/components/locations/Locations";
import type { LocationFormValues } from "@calcom/features/eventtypes/lib/types";
import type { SingleValueLocationOption } from "@calcom/features/form/components/LocationSelect";
import { useLocale } from "@calcom/lib/hooks/useLocale";
+1 -1
View File
@@ -910,7 +910,7 @@ export const RerouteDialog = ({ isOpenDialog, setIsOpenDialog, booking }: Rerout
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent enableOverflow preventCloseOnOutsideClick>
<DialogContent preventCloseOnOutsideClick>
<DialogHeader title={t("reroute_booking")} subtitle={t("reroute_booking_description")} />
<RerouteDialogContentAndFooter
booking={teamEventTypeBooking}
+39 -33
View File
@@ -3,42 +3,48 @@
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
if (process.env.NODE_ENV === "production") {
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN_CLIENT,
sampleRate: parseFloat(process.env.SENTRY_SAMPLE_RATE ?? "1.0") || 1.0,
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE ?? "0.0") || 0.0,
sampleRate: parseFloat(process.env.SENTRY_SAMPLE_RATE ?? "1.0") || 1.0,
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE ?? "0.0") || 0.0,
// Define how likely Replay events are sampled.
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
// replaysSessionSampleRate: parseFloat(process.env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE ?? "0.0") || 0.0,
// Define how likely Replay events are sampled.
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
// replaysSessionSampleRate: parseFloat(process.env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE ?? "0.0") || 0.0,
// Define how likely Replay events are sampled when an error occurs.
// replaysOnErrorSampleRate: parseFloat(process.env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE ?? "0.0") || 0.0,
// Define how likely Replay events are sampled when an error occurs.
// replaysOnErrorSampleRate: parseFloat(process.env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE ?? "0.0") || 0.0,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: !!process.env.SENTRY_DEBUG,
beforeSend(event) {
if (
event.exception?.values?.some(
(e) =>
// Ignore fake error "UnhandledRejection: Non-Error promise rejection captured with value: Object Not Found Matching Id:3, MethodName:update, ParamCount:4"
// Raised GH issue: https://github.com/getsentry/sentry-javascript/issues/3440
e.value?.includes("Non-Error promise rejection captured with") ||
e.value?.includes("Object Not Found Matching Id")
)
) {
return null;
}
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: !!process.env.SENTRY_DEBUG,
beforeSend(event) {
if (
event.exception?.values?.some(
(e) =>
// Ignore fake error "UnhandledRejection: Non-Error promise rejection captured with value: Object Not Found Matching Id:3, MethodName:update, ParamCount:4"
// Raised GH issue: https://github.com/getsentry/sentry-javascript/issues/3440
e.value?.includes("Non-Error promise rejection captured with") ||
e.value?.includes("Object Not Found Matching Id")
)
) {
return null;
}
event.tags = {
...event.tags,
errorSource: "client",
};
return event;
},
});
event.tags = {
...event.tags,
errorSource: "client",
};
return event;
},
});
}
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
export function onRouterTransitionStart(url: string, navigationType: "push" | "replace" | "traverse") {
if (process.env.NODE_ENV === "production") {
Sentry.captureRouterTransitionStart(url, navigationType);
}
}
+16 -7
View File
@@ -1,13 +1,22 @@
import { type Instrumentation } from 'next'
import * as Sentry from "@sentry/nextjs";
export async function register() {
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
if (process.env.NODE_ENV === "production") {
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_PUBLIC_SENTRY_DSN && process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}
}
export const onRequestError = Sentry.captureRequestError;
export const onRequestError: Instrumentation.onRequestError = ( err,
request,
context) => {
if (process.env.NODE_ENV === "production") {
Sentry.captureRequestError(err, request, context);
}
};
+2
View File
@@ -17,6 +17,7 @@ export const getEventTypesFromDB = async (id: number) => {
darkBrandColor: true,
email: true,
timeZone: true,
isPlatformManaged: true,
};
const eventType = await prisma.eventType.findUnique({
where: {
@@ -75,6 +76,7 @@ export const getEventTypesFromDB = async (id: number) => {
hideBranding: true,
},
},
createdByOAuthClientId: true,
},
},
workflows: {
@@ -1,34 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react";
import { UserPermissionRole } from "@calcom/prisma/enums";
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getSession(ctx);
if (!session || !session.user) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!isAdmin) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {
error: null,
migrated: null,
userId: session.user.id,
username: session.user.username,
},
};
}
@@ -1,29 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react";
import { UserPermissionRole } from "@calcom/prisma/enums";
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getSession(ctx);
if (!session || !session.user) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!isAdmin) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {},
};
}
@@ -1,29 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react";
import { UserPermissionRole } from "@calcom/prisma/enums";
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getSession(ctx);
if (!session || !session.user) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!isAdmin) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {},
};
}
@@ -1,29 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react";
import { UserPermissionRole } from "@calcom/prisma/enums";
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getSession(ctx);
if (!session || !session.user) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!isAdmin) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {},
};
}
-16
View File
@@ -60,22 +60,6 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
const reqWithEnrichedHeaders = enrichRequestWithHeaders({ req });
const requestHeaders = new Headers(reqWithEnrichedHeaders.headers);
if (!url.pathname.startsWith("/api")) {
//
// NOTE: When tRPC hits an error a 500 is returned, when this is received
// by the application the user is automatically redirected to /auth/login.
//
// - For this reason our matchers are sufficient for an app-wide maintenance page.
//
// Check whether the maintenance page should be shown
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
// If is in maintenance mode, point the url pathname to the maintenance page
if (isInMaintenanceMode) {
reqWithEnrichedHeaders.nextUrl.pathname = `/maintenance`;
return NextResponse.rewrite(reqWithEnrichedHeaders.nextUrl);
}
}
const routingFormRewriteResponse = routingForms.handleRewrite(url);
if (routingFormRewriteResponse) {
return responseWithHeaders({ url, res: routingFormRewriteResponse, req: reqWithEnrichedHeaders });
@@ -156,7 +156,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
))}
</ul>
</div>
<div className="text-default mb-16 mt-4 hidden text-center text-sm md:block">
<div className="text-default mb-16 mt-4 block text-center text-sm">
{t("temporarily_out_of_office")}{" "}
<Link href="settings/my-account/out-of-office" className="underline">
{t("add_a_redirect")}
@@ -1,7 +1,7 @@
"use client";
import { useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from "@tanstack/react-table";
import { useSearchParams } from "next/navigation";
import { useSearchParams, usePathname } from "next/navigation";
import { useMemo, useRef } from "react";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
@@ -17,6 +17,7 @@ import {
ZMultiSelectFilterValue,
ZDateRangeFilterValue,
ZTextFilterValue,
type SystemFilterSegment,
} from "@calcom/features/data-table";
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -55,11 +56,45 @@ const descriptionByStatus: Record<BookingListingStatus, string> = {
type BookingsProps = {
status: (typeof validStatuses)[number];
userId?: number;
};
function useSystemSegments(userId?: number) {
const { t } = useLocale();
const systemSegments: SystemFilterSegment[] = useMemo(() => {
if (!userId) return [];
return [
{
id: "my_bookings",
name: t("my_bookings"),
type: "system",
activeFilters: [
{
f: "userId",
v: {
type: ColumnFilterType.MULTI_SELECT,
data: [userId],
},
},
],
perPage: 10,
},
];
}, [userId, t]);
return systemSegments;
}
export default function Bookings(props: BookingsProps) {
const pathname = usePathname();
const systemSegments = useSystemSegments(props.userId);
return (
<DataTableProvider useSegments={useSegments}>
<DataTableProvider
useSegments={useSegments}
systemSegments={systemSegments}
tableIdentifier={pathname || undefined}>
<BookingsContent {...props} />
</DataTableProvider>
);
@@ -180,7 +215,7 @@ function BookingsContent({ status }: BookingsProps) {
columnHelper.accessor((row) => row.type === "data" && row.booking.user?.id, {
id: "userId",
header: t("member"),
enableColumnFilter: true,
enableColumnFilter: user?.isTeamAdminOrOwner ?? false,
enableSorting: false,
cell: () => null,
meta: {
@@ -228,16 +228,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
? { ...previousBooking, rescheduledBy: bookingInfo.user?.name }
: previousBooking;
const isPlatformBooking = eventType.users[0]?.isPlatformManaged || eventType.team?.createdByOAuthClientId;
return {
props: {
orgSlug: currentOrgDomain,
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
hideBranding: await shouldHideBrandingForEvent({
eventTypeId: eventType.id,
team: eventType.team,
owner: eventType.users[0] ?? null,
organizationId: session?.user?.profile?.organizationId ?? session?.user?.org?.id ?? null,
}),
hideBranding: isPlatformBooking
? true
: await shouldHideBrandingForEvent({
eventTypeId: eventType.id,
team: eventType.team,
owner: eventType.users[0] ?? null,
organizationId: session?.user?.profile?.organizationId ?? session?.user?.org?.id ?? null,
}),
profile,
eventType,
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
@@ -138,7 +138,8 @@ const OnboardingPage = (props: PageProps) => {
"dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1]",
"[--cal-brand-subtle:#9CA3AF]",
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]"
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]",
"[--cal-brand-accent:#FFFFFF] dark:[--cal-brand-accent:#000000]"
)}
data-testid="onboarding"
key={pathname}>
+2 -1
View File
@@ -7,6 +7,7 @@ import {
ColumnFilterType,
type FilterableColumn,
} from "@calcom/features/data-table";
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
import {
AverageEventDurationChart,
BookingKPICards,
@@ -31,7 +32,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
export default function InsightsPage({ timeZone }: { timeZone: string }) {
return (
<DataTableProvider timeZone={timeZone}>
<DataTableProvider useSegments={useSegments} timeZone={timeZone}>
<InsightsOrgTeamsProvider>
<InsightsPageContent />
</InsightsOrgTeamsProvider>
+490 -478
View File
File diff suppressed because it is too large. Load diff
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.6.0",
"version": "5.6.4",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -29,7 +29,7 @@
"yarn": "3.4.1"
},
"dependencies": {
"@boxyhq/saml-jackson": "1.42.0",
"@boxyhq/saml-jackson": "1.52.2",
"@calcom/app-store": "*",
"@calcom/app-store-cli": "*",
"@calcom/dayjs": "*",
@@ -77,7 +77,6 @@
"@tanstack/react-query": "^5.17.15",
"@team-plain/typescript-sdk": "^5.9.0",
"@types/turndown": "^5.0.1",
"@unkey/ratelimit": "^0.1.1",
"@upstash/redis": "^1.35.2",
"@vercel/edge-config": "^0.1.1",
"@vercel/edge-functions-ui": "^0.2.1",
@@ -1,6 +0,0 @@
import { wrapApiHandlerWithSentry } from "@sentry/nextjs";
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
import { highPerfRouter } from "@calcom/trpc/server/routers/viewer/highPerf/_router";
export default wrapApiHandlerWithSentry(createNextApiHandler(highPerfRouter), "/api/trpc/highPerf/[trpc]");
+29 -8
View File
@@ -479,7 +479,6 @@ test.describe("Bookings", () => {
const filterItemsConfig = [
{ key: "eventTypeId", name: "Event Type", testId: "add-filter-item-eventTypeId" },
{ key: "teamId", name: "Team", testId: "add-filter-item-teamId" },
{ key: "userId", name: "Member", testId: "add-filter-item-userId" },
{ key: "attendeeName", name: "Attendees Name", testId: "add-filter-item-attendeeName" },
{ key: "attendeeEmail", name: "Attendee Email", testId: "add-filter-item-attendeeEmail" },
{ key: "dateRange", name: "Date Range", testId: "add-filter-item-dateRange" },
@@ -488,8 +487,16 @@ test.describe("Bookings", () => {
const getFilterItemLocator = (page: Page, testId: string) => page.locator(`[data-testid="${testId}"]`);
test.beforeEach(async ({ page, users }) => {
const user = await users.create();
const setup = async ({
isAdmin,
page,
users,
}: {
isAdmin: boolean;
page: Page;
users: Fixtures["users"];
}) => {
const user = isAdmin ? await users.create(undefined, { hasTeam: true }) : await users.create();
await user.apiLogin();
const bookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
@@ -498,9 +505,10 @@ test.describe("Bookings", () => {
await bookingsGetResponse;
await page.locator('[data-testid="add-filter-button"]').click();
await expect(page.locator(searchInputSelector)).toBeVisible();
});
};
test("should show all filter items initially and after clearing search", async ({ page }) => {
test("should show all filter items initially and after clearing search", async ({ page, users }) => {
await setup({ isAdmin: false, page, users });
const searchInput = page.locator(searchInputSelector);
// Initial check: all defined filter items should be visible
@@ -524,7 +532,18 @@ test.describe("Bookings", () => {
}
});
test("search should be case-insensitive", async ({ page }) => {
test("should show admin-only filter", async ({ page, users }) => {
await setup({ isAdmin: true, page, users });
await expect(
getFilterItemLocator(page, "add-filter-item-userId"),
`Item "Member" should be visible initially`
).toBeVisible();
});
test("search should be case-insensitive", async ({ page, users }) => {
await setup({ isAdmin: true, page, users });
const searchInput = page.locator(searchInputSelector);
// Search for "member" (lowercase)
@@ -534,7 +553,8 @@ test.describe("Bookings", () => {
await expect(getFilterItemLocator(page, "add-filter-item-teamId")).toBeHidden();
});
test("should individually find each filter item by its full name", async ({ page }) => {
test("should individually find each filter item by its full name", async ({ page, users }) => {
await setup({ isAdmin: false, page, users });
const searchInput = page.locator(searchInputSelector);
for (const targetItem of filterItemsConfig) {
@@ -550,7 +570,8 @@ test.describe("Bookings", () => {
}
});
test("should show no items for a non-matching search term", async ({ page }) => {
test("should show no items for a non-matching search term", async ({ page, users }) => {
await setup({ isAdmin: false, page, users });
const searchInput = page.locator(searchInputSelector);
const nonExistentTerm = "NonExistentFilterXYZ123";
await searchInput.fill(nonExistentTerm);
@@ -6,6 +6,7 @@ import { test } from "./lib/fixtures";
import {
bookTimeSlot,
confirmReschedule,
confirmBooking,
doOnOrgDomain,
selectFirstAvailableTimeSlotNextMonth,
selectSecondAvailableTimeSlotNextMonth,
@@ -98,6 +99,58 @@ test("dynamic booking info prefilled by query params", async ({ page, users }) =
activeState = await listItemLocator.getAttribute("data-active");
expect(activeState).toEqual("false");
});
test("multiple duration selection updates event length correctly", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await test.step("update event title to include duration placeholder", async () => {
await page.goto("/event-types");
await page.waitForSelector('[data-testid="event-types"]');
await page.click(`text=Multiple duration`);
await page.waitForSelector('[data-testid="event-title"]');
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
"aria-current",
"page"
);
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
await page.locator('[data-testid="update-eventtype"]').click();
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
});
await page.goto(`/${user.username}/multiple-duration`);
await page.locator('[data-testid="multiple-choice-30mins"]').waitFor({ state: "visible" });
await test.step("verify default 30min duration is selected", async () => {
const duration30 = page.getByTestId("multiple-choice-30mins");
const activeState = await duration30.getAttribute("data-active");
expect(activeState).toEqual("true");
});
await test.step("book with 90min duration and verify title", async () => {
await page.getByTestId("multiple-choice-90mins").click();
const duration90 = page.getByTestId("multiple-choice-90mins");
const activeState = await duration90.getAttribute("data-active");
expect(activeState).toEqual("true");
await selectFirstAvailableTimeSlotNextMonth(page);
await page.fill('[name="name"]', "Test User");
await page.fill('[name="email"]', "test@example.com");
await confirmBooking(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const bookingTitle = page.locator('[data-testid="booking-title"]');
await expect(bookingTitle).toBeVisible();
const titleText = await bookingTitle.textContent();
expect(titleText).toContain("90");
});
});
// eslint-disable-next-line playwright/no-skipped-test
test.skip("it contains the right event details", async ({ page }) => {
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
+69
View File
@@ -31,6 +31,7 @@ export async function applySelectFilter(page: Page, columnId: string, value: str
}
export async function selectOptionValue(page: Page, columnId: string, value: string) {
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).waitFor();
await page.getByTestId(`select-filter-options-${columnId}`).getByRole("option", { name: value }).click();
}
@@ -176,3 +177,71 @@ export async function listSegments(page: Page): Promise<string[]> {
export function locateSelectedSegmentName(page: Page, expectedName: string) {
return page.locator('[data-testid="filter-segment-select"]').filter({ hasText: expectedName });
}
/**
* Check if a system segment is visible in the dropdown
*/
export async function expectSystemSegmentVisible(page: Page, segmentName: string) {
await page.getByTestId("filter-segment-select").click();
await expect(
page.locator('[data-testid="filter-segment-select-content"]').getByText("Default")
).toBeVisible();
await expect(
page
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
.filter({ hasText: segmentName })
).toBeVisible();
await page.keyboard.press("Escape");
}
/**
* Check that no segment is currently selected
*/
export async function expectSegmentCleared(page: Page) {
// Check that no segment is selected (button shows default text)
const segmentSelect = page.getByTestId("filter-segment-select");
const buttonText = await segmentSelect.textContent();
expect(buttonText?.trim()).toEqual("Segment");
}
/**
* Check that a specific segment is currently selected
*/
export async function expectSegmentSelected(page: Page, segmentName: string) {
await expect(locateSelectedSegmentName(page, segmentName)).toBeVisible();
}
/**
* Duplicate an existing segment
*/
export async function duplicateSegment(page: Page, originalName: string, newName: string) {
await openSegmentSubmenu(page, originalName);
await page.getByTestId("filter-segment-select-submenu-content").getByText("Duplicate").click();
await page.getByTestId("duplicate-segment-name").fill(newName);
await page.getByRole("button", { name: "Duplicate" }).click();
await expect(page.getByText("Filter segment duplicated")).toBeVisible();
await page.keyboard.press("Escape");
}
/**
* Rename an existing segment
*/
export async function renameSegment(page: Page, originalName: string, newName: string) {
await openSegmentSubmenu(page, originalName);
await page.getByTestId("filter-segment-select-submenu-content").getByText("Rename").click();
await page.getByTestId("rename-segment-name").fill(newName);
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Filter segment updated")).toBeVisible();
await page.keyboard.press("Escape");
}
/**
* Check if a segment group (like "Default" or "Personal") is visible
*/
export async function expectSegmentGroupVisible(page: Page, groupName: string) {
await page.getByTestId("filter-segment-select").click();
await expect(
page.locator('[data-testid="filter-segment-select-content"]').getByText(groupName)
).toBeVisible();
await page.keyboard.press("Escape");
}
+3 -17
View File
@@ -3,7 +3,7 @@ import { expect } from "@playwright/test";
import { randomString } from "@calcom/lib/random";
import prisma from "@calcom/prisma";
import { addFilter, openFilter, clearFilters } from "./filter-helpers";
import { clearFilters, applySelectFilter } from "./filter-helpers";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
@@ -202,25 +202,11 @@ test.describe("Insights", async () => {
await page.locator('[data-testid="org-teams-filter-item"]').nth(1).click();
await page.keyboard.press("Escape");
// Choose User filter item from dropdown
await addFilter(page, "userId");
// Wait for the URL to include userId
await page.waitForURL((url) => url.toString().includes("userId"));
// Click User filter to see a user list
await openFilter(page, "userId");
await page.locator('[data-testid="select-filter-options-userId"]').getByRole("option").nth(0).click();
await page.locator('[data-testid="select-filter-options-userId"]').getByRole("option").nth(1).click();
// press escape button to close the filter
await page.keyboard.press("Escape");
await applySelectFilter(page, "userId", member.username || "");
await clearFilters(page);
await expect(page.url()).not.toContain("userId");
await expect(page).not.toHaveURL(/[?&]userId=/);
});
test("should test download button", async ({ page, users }) => {
+120
View File
@@ -0,0 +1,120 @@
import { expect } from "@playwright/test";
import type { Page } from "@playwright/test";
import { addFilter, selectSegment, expectSegmentCleared, expectSegmentSelected } from "./filter-helpers";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
/**
* Navigate to bookings page with specific status
*/
async function navigateToBookings(page: Page, status = "upcoming") {
// Wait for the bookings API response like existing tests do
const bookingsGetResponse = page.waitForResponse((response) =>
/\/api\/trpc\/bookings\/get.*/.test(response.url())
);
await page.goto(`/bookings/${status}`, { waitUntil: "domcontentloaded" });
await bookingsGetResponse;
}
test.describe("System Segments", () => {
test.describe("Core Functionality", () => {
test("My Bookings system segment filters to current user's bookings only", async ({ page, users }) => {
const user1 = await users.create({ username: "user1" });
// Create some bookings for user (this would need booking setup)
// For now, we'll test the filter application
await user1.apiLogin();
await navigateToBookings(page);
await selectSegment(page, "My Bookings");
await expectSegmentSelected(page, "My Bookings");
// Verify the userId filter is applied (check URL or filter indicators)
const url = page.url();
expect(url).toContain("activeFilters");
});
test("System segments show only Duplicate option in submenu", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await navigateToBookings(page);
await page.getByTestId("filter-segment-select").click();
await page
.locator('[data-testid="filter-segment-select-content"] [role="menuitem"]')
.filter({ hasText: "My Bookings" })
.locator('[data-testid="filter-segment-select-submenu-button"]')
.click();
const submenu = page.getByTestId("filter-segment-select-submenu-content");
await expect(submenu.getByText("Duplicate")).toBeVisible();
await expect(submenu.getByText("Rename")).toBeHidden();
await expect(submenu.getByText("Delete")).toBeHidden();
});
});
test.describe("State Persistence", () => {
test("System segment selection persists across page visits", async ({ page, users }) => {
const owner = await users.create(undefined, { hasTeam: true });
await owner.apiLogin();
// Visit bookings page and select system segment
await navigateToBookings(page);
await selectSegment(page, "My Bookings");
await expectSegmentSelected(page, "My Bookings");
// Revisit the page - should preserve selection
await navigateToBookings(page);
await expectSegmentSelected(page, "My Bookings");
});
test("Cleared system segment state persists (no default selection after manual clear)", async ({
page,
users,
}) => {
const owner = await users.create(undefined, { hasTeam: true });
await owner.apiLogin();
await navigateToBookings(page);
// Select system segment
await selectSegment(page, "My Bookings");
await expectSegmentSelected(page, "My Bookings");
// Unselect system segment
await selectSegment(page, "My Bookings");
// Revisit the page - should NOT have any segment selected
await navigateToBookings(page);
await expectSegmentCleared(page);
});
});
test.describe("Auto-clear Behavior", () => {
test("Manual filter clears system segment selection", async ({ page, users }) => {
const user = await users.create(undefined, { hasTeam: true });
await user.apiLogin();
await navigateToBookings(page);
// Select system segment
await selectSegment(page, "My Bookings");
await expectSegmentSelected(page, "My Bookings");
// Apply manual filter - this should clear system segment
await addFilter(page, "eventTypeId");
await page.keyboard.press("Escape");
await expectSegmentCleared(page);
// Verify filter is still applied
await expect(page.getByTestId("filter-popover-trigger-eventTypeId")).toBeVisible();
});
});
});
+7 -2
View File
@@ -1,6 +1,11 @@
module.exports = {
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
if (process.env.NODE_ENV === "production") {
config.plugins.autoprefixer = {};
}
module.exports = config;
@@ -3472,5 +3472,7 @@
"webhook_metadata": "Metadata",
"stats": "Stats",
"booking_status": "Booking status",
"my_bookings": "My Bookings",
"phone": "Phone",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
-12
View File
@@ -41,18 +41,6 @@
},
"pages/api/trpc/workflows/[trpc].ts": {
"maxDuration": 800
},
"pages/api/trpc/highPerf/[trpc].ts": {
"memory": 3008
},
"pages/api/trpc/appRoutingForms/[trpc].ts": {
"memory": 2048
},
"pages/router/embed.tsx": {
"memory": 2048
},
"pages/api/book/event.ts": {
"memory": 2048
}
}
}
+1
View File
@@ -129,6 +129,7 @@ Below is a list of props that can be passed to the booker atom.
| roundRobinHideOrgAndTeam | No | Boolean indicating if the organization and team should be hidden in the booker atom sidebar for round robin scheduling type, defaults to false. |
| showNoAvailabilityDialog | No | Boolean indicating if the no availability dialog should be shown, defaults to true. |
| silentlyHandleCalendarFailures | No | Boolean when true the booker still displays slots when the third party calendars credentials are invalid or expired, Booker may show stale availability when enabled |
| hideEventMetadata | No | Boolean that controls the visibility of the event metadata sidebar. When `true`, hides the left sidebar containing event details like title, description, duration, and host information. Defaults to `false`. |
## Styling
+1 -1
View File
@@ -76,7 +76,7 @@ Sometimes, yarn install might fail during deployment on Vercel, in which case, y
<Step title="Override the build command">
Override to:
```
cd ../.. && yarn build --include-dependencies --no-deps
cd ../.. && yarn build --no-deps
```
</Step>
<Step title="Hit Deploy">
+2 -2
View File
@@ -33,8 +33,8 @@ RUN set -eux; \
apt-get clean && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
yarn config set httpTimeout 1200000 && \
npx turbo prune --scope=@calcom/web --docker && \
npx turbo prune --scope=@calcom/api --docker && \
npx turbo prune --filter=@calcom/web --docker && \
npx turbo prune --filter=@calcom/api --docker && \
yarn install && \
yarn turbo run build --filter=@calcom/api
+13 -14
View File
@@ -28,23 +28,23 @@
"edit-app-template": "yarn app-store edit-template",
"delete-app-template": "yarn app-store delete-template",
"build": "turbo run build --filter=@calcom/web...",
"build:ai": "turbo run build --scope=\"@calcom/ai\"",
"build:ai": "turbo run build --filter=\"@calcom/ai\"",
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
"db-deploy": "turbo run db-deploy",
"db-seed": "turbo run db-seed",
"db-studio": "yarn prisma studio",
"deploy": "turbo run deploy",
"dev:all": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\" --scope=\"@calcom/console\"",
"dev:ai": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/ai\"",
"dev:api": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\"",
"dev:api:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/console\"",
"dev:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"",
"dev:swagger": "turbo run dev --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"",
"dev:website": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"",
"dev": "turbo run dev --scope=\"@calcom/web\"",
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"",
"dev:all": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\" --filter=\"@calcom/console\"",
"dev:ai": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/ai\"",
"dev:api": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\"",
"dev:api:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/console\"",
"dev:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/console\"",
"dev:swagger": "turbo run dev --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/swagger\"",
"dev:website": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\"",
"dev": "turbo run dev --filter=\"@calcom/web\"",
"build-storybook": "turbo run build --filter=\"@calcom/storybook\"",
"dx": "turbo run dx",
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"",
"i-dev": "infisical run -- turbo run dev --filter=\"@calcom/web\"",
"i-dx": "infisical run -- turbo run dx",
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
@@ -61,7 +61,7 @@
"pre-commit": "lint-staged",
"predev": "echo 'Checking env files'",
"prisma": "yarn workspace @calcom/prisma prisma",
"start": "turbo run start --scope=\"@calcom/web\"",
"start": "turbo run start --filter=\"@calcom/web\"",
"tdd": "vitest watch",
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
@@ -118,13 +118,12 @@
"@daily-co/daily-js": "^0.76.0",
"@evyweb/ioctopus": "^1.2.0",
"@next/third-parties": "^14.2.5",
"@unkey/ratelimit": "^2.1.2",
"@vercel/functions": "^1.4.0",
"city-timezones": "^1.2.1",
"date-fns-tz": "^3.2.0",
"eslint": "^8.34.0",
"p-limit": "^6.2.0",
"turbo": "^1.10.1"
"turbo": "^2.5.5"
},
"resolutions": {
"types-ramda": "0.29.4",
@@ -10,6 +10,7 @@ import type { FreeBusyArgs } from "@calcom/features/calendar-cache/calendar-cach
import { getTimeMax, getTimeMin } from "@calcom/features/calendar-cache/lib/datesForCache";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import { uniqueBy } from "@calcom/lib/array";
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar";
@@ -85,10 +86,16 @@ export default class GoogleCalendarService implements Calendar {
const selectedHostDestinationCalendar = event.destinationCalendar?.find(
(cal) => cal.credentialId === this.credential.id
);
const isOrganizerExempt = ORGANIZER_EMAIL_EXEMPT_DOMAINS?.split(",")
.filter((domain) => domain.trim() !== "")
.some((domain) => event.organizer.email.toLowerCase().endsWith(domain.toLowerCase()));
const eventAttendees = event.attendees.map(({ id: _id, ...rest }) => ({
...rest,
responseStatus: "accepted",
}));
const attendees: calendar_v3.Schema$EventAttendee[] = [
{
...event.organizer,
@@ -100,7 +107,7 @@ export default class GoogleCalendarService implements Calendar {
// We use || instead of ?? here to handle empty strings
email: hostExternalCalendarId || selectedHostDestinationCalendar?.externalId || event.organizer.email,
},
...eventAttendees,
...(event.hideOrganizerEmail && !isOrganizerExempt ? [] : eventAttendees),
];
if (event.team?.members) {
+5 -2
View File
@@ -1,3 +1,6 @@
/**
* TODO: Consolidate this file with BookingLocationService and add tests
*/
import type { TFunction } from "i18next";
import { z } from "zod";
@@ -58,7 +61,7 @@ export type EventLocationTypeFromApp = Ensure<
export type EventLocationType = DefaultEventLocationType | EventLocationTypeFromApp;
export const DailyLocationType = "integrations:daily";
export const CalVideoLocationType = DailyLocationType;
export const MeetLocationType = "integrations:google:meet";
export const MSTeamsLocationType = "integrations:office365_video";
@@ -380,7 +383,7 @@ export const getLocationValueForDB = (
eventLocations: LocationObject[]
) => {
let bookingLocation = bookingLocationTypeOrValue;
let conferenceCredentialId = undefined;
let conferenceCredentialId: number | undefined = undefined;
eventLocations.forEach((location) => {
if (location.type === bookingLocationTypeOrValue) {
@@ -661,22 +661,30 @@ export default class Office365CalendarService implements Calendar {
);
};
private handleErrorJsonOffice365Calendar = <Type>(response: Response): Promise<Type | string> => {
private async handleErrorJsonOffice365Calendar<Type>(response: Response): Promise<Type | string> {
if (response.headers.get("content-encoding") === "gzip") {
return response.text();
}
if (response.status === 204) {
return new Promise((resolve) => resolve({} as Type));
return {} as Type;
}
if (!response.ok && response.status < 200 && response.status >= 300) {
response.json().then(console.log);
throw Error(response.statusText);
if (!response.ok) {
let errorBody: string | object;
try {
errorBody = await response.json();
} catch (e) {
errorBody = await response.text();
}
this.log.error(
`handleErrorJsonOffice365Calendar: Office365 API request failed with status ${response.status}`,
errorBody
);
}
return response.json();
};
}
async getMainTimeZone(): Promise<string> {
try {
@@ -1,363 +0,0 @@
import { describe, expect, it, afterEach, vi } from "vitest";
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
afterEach(() => {
vi.resetAllMocks();
});
describe("jsonLogicToPrisma(Reporting)", () => {
describe("Text Operand", () => {
it("should support 'Equals' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: { and: [{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "A"] }] },
});
expect(prismaWhere).toEqual({
AND: [
{
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
equals: "A",
},
},
],
});
});
it("should support 'Not Equals' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: { and: [{ "!=": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "abc"] }] },
});
expect(prismaWhere).toEqual({
AND: [
{
NOT: {
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
equals: "abc",
},
},
},
],
});
});
it("should support 'Contains' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: { and: [{ in: ["A", { var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }] }] },
});
expect(prismaWhere).toEqual({
AND: [
{
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
string_contains: "A",
},
},
],
});
});
it("should support 'Not Contains' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: { and: [{ "!": { in: ["a", { var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }] } }] },
});
expect(prismaWhere).toEqual({
AND: [
{
NOT: {
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
string_contains: "a",
},
},
},
],
});
});
describe("Number Type", () => {
it("should support 'greater than' operator", () => {
let prismaWhere = jsonLogicToPrisma({
logic: {
and: [
{
">": [
{
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
},
// Giving a string here to test that it is converted to a number
"100",
],
},
],
},
});
expect(prismaWhere).toEqual({
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], gt: 100 } }],
});
prismaWhere = jsonLogicToPrisma({
logic: {
and: [
{
">": [
{
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
},
// A number would also work
100,
],
},
],
},
});
expect(prismaWhere).toEqual({
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], gt: 100 } }],
});
});
it("should support 'greater than or equal to' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: {
and: [
{
">=": [
{
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
},
// Giving a string here to test that it is converted to a number
"100",
],
},
],
},
});
expect(prismaWhere).toEqual({
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], gte: 100 } }],
});
});
it("should support 'less than' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: {
and: [
{
"<": [
{
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
},
// Giving a string here to test that it is converted to a number
"100",
],
},
],
},
});
expect(prismaWhere).toEqual({
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], lt: 100 } }],
});
});
it("should support 'less than or equal to' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: {
and: [
{
"<=": [
{
var: "a0d113a8-8e40-49b7-87b1-7f4ab57d226f",
},
// Giving a string here to test that it is converted to a number
"100",
],
},
],
},
});
expect(prismaWhere).toEqual({
AND: [{ response: { path: ["a0d113a8-8e40-49b7-87b1-7f4ab57d226f", "value"], lte: 100 } }],
});
});
it("'Equals' operator should query with string as well as number", () => {
const prismaWhere = jsonLogicToPrisma({
logic: { and: [{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "1"] }] },
});
expect(prismaWhere).toEqual({
AND: [
{
OR: [
{
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
equals: "1",
},
},
{
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
equals: 1,
},
},
],
},
],
});
});
});
});
describe("MultiSelect", () => {
it("should support 'Equals' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: {
and: [
{ all: [{ var: "267c7817-81a5-4bef-9d5b-d0faa4cd0d71" }, { in: [{ var: "" }, ["C", "D"]] }] },
],
},
});
expect(prismaWhere).toEqual({
AND: [
{
response: { path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"], array_contains: ["C", "D"] },
},
],
});
});
it("should support 'Any In' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: { and: [{ in: [{ var: "267c7817-81a5-4bef-9d5b-d0faa4cd0d71" }, ["C", "D"]] }] },
});
expect(prismaWhere).toEqual({
AND: [
{
OR: [
{
response: {
path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"],
equals: "C",
},
},
{
response: {
path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"],
equals: "D",
},
},
],
},
],
});
});
it("should support 'Not In' operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: { and: [{ "!": { in: [{ var: "267c7817-81a5-4bef-9d5b-d0faa4cd0d71" }, ["C", "D"]] } }] },
});
expect(prismaWhere).toEqual({
AND: [
{
NOT: {
OR: [
{
response: {
path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"],
equals: "C",
},
},
{
response: {
path: ["267c7817-81a5-4bef-9d5b-d0faa4cd0d71", "value"],
equals: "D",
},
},
],
},
},
],
});
});
it("should support where All Match ['Equals', 'Equals'] operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: {
and: [
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "a"] },
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "b"] },
],
},
});
expect(prismaWhere).toEqual({
AND: [
{
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
equals: "a",
},
},
{
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
equals: "b",
},
},
],
});
});
it("should support where Any Match ['Equals', 'Equals'] operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: {
or: [
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "a"] },
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "b"] },
],
},
});
expect(prismaWhere).toEqual({
OR: [
{
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
equals: "a",
},
},
{
response: {
path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"],
equals: "b",
},
},
],
});
});
it("should support where None Match ['Equals', 'Equals'] operator", () => {
const prismaWhere = jsonLogicToPrisma({
logic: {
"!": {
or: [
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "abc"] },
{ "==": [{ var: "505d3c3c-aa71-4220-93a9-6fd1e1087939" }, "abcd"] },
],
},
},
});
expect(prismaWhere).toEqual({
NOT: {
OR: [
{ response: { path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], equals: "abc" } },
{ response: { path: ["505d3c3c-aa71-4220-93a9-6fd1e1087939", "value"], equals: "abcd" } },
],
},
});
});
});
});
@@ -1,276 +0,0 @@
// It can have many shapes, so just use any and we rely on unit tests to test all those scenarios.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type LogicData = Partial<Record<keyof typeof OPERATOR_MAP, any>>;
type NegatedLogicData = {
"!": LogicData;
};
export type JsonLogicQuery = {
logic: {
and?: LogicData[];
or?: LogicData[];
"!"?: {
and?: LogicData[];
or?: LogicData[];
};
} | null;
};
type PrismaWhere = {
AND?: ReturnType<typeof convertQueriesToPrismaWhereClause>[];
OR?: ReturnType<typeof convertQueriesToPrismaWhereClause>[];
NOT?: PrismaWhere;
};
const OPERATOR_MAP = {
"==": {
operator: "equals",
secondaryOperand: null,
},
in: {
operator: "string_contains",
secondaryOperand: null,
},
"!=": {
operator: "NOT.equals",
secondaryOperand: null,
},
"!": {
operator: "equals",
secondaryOperand: "",
},
"!!": {
operator: "NOT.equals",
secondaryOperand: "",
},
">": {
operator: "gt",
secondaryOperand: null,
},
">=": {
operator: "gte",
secondaryOperand: null,
},
"<": {
operator: "lt",
secondaryOperand: null,
},
"<=": {
operator: "lte",
secondaryOperand: null,
},
all: {
operator: "array_contains",
secondaryOperand: null,
},
};
/**
* Operators supported on array of basic queries
*/
const GROUP_OPERATOR_MAP = {
and: "AND",
or: "OR",
"!": "NOT",
} as const;
const NumberOperators = [">", ">=", "<", "<="];
const negatePrismaWhereClauseIfNeeded = (data: any, isNegation: boolean) => {
if (!isNegation) {
return data;
}
return {
NOT: {
...data,
},
};
};
const convertSingleQueryToPrismaWhereClause = (
operatorName: keyof typeof OPERATOR_MAP,
logicData: LogicData,
isNegation: boolean
) => {
const mappedOperator = OPERATOR_MAP[operatorName].operator;
const staticSecondaryOperand = OPERATOR_MAP[operatorName].secondaryOperand;
isNegation = isNegation || mappedOperator.startsWith("NOT.");
const prismaOperator = mappedOperator.replace("NOT.", "");
const operands =
logicData[operatorName] instanceof Array ? logicData[operatorName] : [logicData[operatorName]];
if (operatorName === "in" && operands[0]?.var && Array.isArray(operands[1])) {
// Case A: Item "in" array
// operands[0]: { var: ... }
// operands[1]: items to test against
// Convert 'in' operator to union of OR clauses
return negatePrismaWhereClauseIfNeeded(
{
OR: (operands[1] ?? []).map((value) => ({
response: {
path: [operands[0].var, "value"],
[`${OPERATOR_MAP["=="].operator}`]: value,
},
})),
},
isNegation
);
}
if (operatorName === "in" && typeof operands[0] === "string" && operands[1]?.var) {
// Case B: String "in" string
// operands[0]: string to test against
// operands[1]: { var: ... }
return negatePrismaWhereClauseIfNeeded(
{
response: {
path: [operands[1].var, "value"],
[`${prismaOperator}`]: operands[0],
},
},
isNegation
);
}
const mainOperand = operands[0].var;
let secondaryOperand;
if (operatorName === "all") {
secondaryOperand = operands[1].in[1];
} else {
secondaryOperand = staticSecondaryOperand || operands[1] || "";
}
const isNumberOperator = NumberOperators.includes(operatorName);
const secondaryOperandAsNumber = typeof secondaryOperand === "string" ? Number(secondaryOperand) : null;
if (secondaryOperandAsNumber && isNumberOperator) {
// We know that it's number operator so Prisma should query number
// Note that if we get string values in DB(e.g. '100'), those values can't be filtered with number operators.
return negatePrismaWhereClauseIfNeeded(
{
response: {
path: [mainOperand, "value"],
[`${prismaOperator}`]: secondaryOperandAsNumber,
},
},
isNegation
);
}
if (secondaryOperandAsNumber && !isNumberOperator) {
// We know that it's not number operator but the input field might have been a number and thus stored value in DB as number.
// Also, even for input type=number we might accidentally get string value(e.g. '100'). So, let reporting do it's best job with both number and string.
return negatePrismaWhereClauseIfNeeded(
{
OR: [
{
response: {
path: [mainOperand, "value"],
// Query as string e.g. equals '100'
[`${prismaOperator}`]: secondaryOperand,
},
},
{
response: {
path: [mainOperand, "value"],
// Query as number e.g. equals 100
[`${prismaOperator}`]: secondaryOperandAsNumber,
},
},
],
},
isNegation
);
}
return negatePrismaWhereClauseIfNeeded(
{
response: {
path: [mainOperand, "value"],
[`${prismaOperator}`]: secondaryOperand,
},
},
isNegation
);
};
const isNegation = (logicData: LogicData | NegatedLogicData) => {
if ("!" in logicData) {
const negatedLogicData = logicData["!"];
for (const [operatorName] of Object.entries(OPERATOR_MAP)) {
if (negatedLogicData[operatorName]) {
return true;
}
}
}
return false;
};
const convertQueriesToPrismaWhereClause = (logicData: LogicData) => {
const _isNegation = isNegation(logicData);
if (_isNegation) {
logicData = logicData["!"];
}
for (const [key] of Object.entries(OPERATOR_MAP)) {
const operatorName = key as keyof typeof OPERATOR_MAP;
if (logicData[operatorName]) {
return convertSingleQueryToPrismaWhereClause(operatorName, logicData, _isNegation);
}
}
};
export const jsonLogicToPrisma = (query: JsonLogicQuery) => {
try {
let logic = query.logic;
if (!logic) {
return {};
}
let prismaWhere: PrismaWhere = {};
let negateLogic = false;
// Case: Negation of "Any of these"
// Example: {"logic":{"!":{"or":[{"==":[{"var":"505d3c3c-aa71-4220-93a9-6fd1e1087939"},"1"]},{"==":[{"var":"505d3c3c-aa71-4220-93a9-6fd1e1087939"},"1"]}]}}}
if (logic["!"]) {
logic = logic["!"];
negateLogic = true;
}
// Case: All of these
if (logic.and) {
const where: PrismaWhere["AND"] = (prismaWhere[GROUP_OPERATOR_MAP["and"]] = []);
logic.and.forEach((and) => {
const res = convertQueriesToPrismaWhereClause(and);
if (!res) {
return;
}
where.push(res);
});
}
// Case: Any of these
else if (logic.or) {
const where: PrismaWhere["OR"] = (prismaWhere[GROUP_OPERATOR_MAP["or"]] = []);
logic.or.forEach((or) => {
const res = convertQueriesToPrismaWhereClause(or);
if (!res) {
return;
}
where.push(res);
});
}
if (negateLogic) {
prismaWhere = { NOT: { ...prismaWhere } };
}
return prismaWhere;
} catch (e) {
console.log("Error converting to prisma `where`", JSON.stringify(query), "Error is ", e);
return {};
}
};
Loaded 100 of 221 files, more files were not shown because too many files have changed in this diff. Show more