Files
c28eb90928 chore: Upgrade prisma to 6.7.0 (#21264)
* chore: Upgrade prisma to 6.7.0

* Build fixes

* type fixes

Signed-off-by: Omar López <[email protected]>

* Update schema.prisma

* Patching

* Revert "Update schema.prisma"

This reverts commit 47d8618bf89ef4d007b30084df766f17281e21a1.

* Revert "Patching"

This reverts commit a1d2e3040e71690a44d4324db95d73b4d68c6adb.

* Revert schema changes

Signed-off-by: Omar López <[email protected]>

* WIP

Signed-off-by: Omar López <[email protected]>

* Update getPublicEvent.ts

* Update imports

Signed-off-by: Omar López <[email protected]>

* Update gitignore

Signed-off-by: Omar López <[email protected]>

* update remaining imports

Signed-off-by: Omar López <[email protected]>

* Delete .cursor/config.json

* Discard changes to packages/features/eventtypes/lib/getPublicEvent.ts

* Update _get.ts

* Update user.ts

* Update .gitignore

* update

* Update WorkflowStepContainer.tsx

* Update next-auth-custom-adapter.ts

* Update getPublicEvent.ts

* Update workflow.ts

* Update next-auth-custom-adapter.ts

* Update next-auth-options.ts

* Update bookingScenario.ts

* fix missing imports

* upgrades prismock

Signed-off-by: Omar López <[email protected]>

* patches prismock

Signed-off-by: Omar López <[email protected]>

* Update reschedule.test.ts

* Update prisma.ts

* patch prismock

Signed-off-by: Omar López <[email protected]>

* fix enums imports

Signed-off-by: Omar López <[email protected]>

* Revert "Update prisma.ts"

This reverts commit 64edcf8db54171ff4456c209d563b5d431d99619.

* Revert "patch prismock"

This reverts commit e95819113dc9d88e7130947aa120cd42710977c8.

* fix patch

* Fix test that overrun the boundary, it shouldn't test too much

* Move prisma import to changeSMSLockState

* Bring back broken test without illegal imports

* Merge with main and fix filter hosts by same round robin host

* Fixed buildDryRunBooking fn tests

* Fix and move ooo create or update handler test

* Fix packages/features/eventtypes/lib/isCurrentlyAvailable.test.ts

* Fix packages/trpc/server/routers/viewer/organizations/listMembers.handler.test.ts

* Mock @calcom/prisma

* Fix: verify-email.test.ts

* fix: Moved WebhookService test and fixed default import mock

* Fix: Added missing prisma mock, handleNewBooking uses that of course

* We're not testing createContext here

* fix: Prisma mock fix for listMembers.test.ts

* More fixes to broken testcases

* Forgot to remove borked test

* Prevent the need to mock a lot of dependencies by moving out buildBaseWhereCondition to its own file

* Temporarily skip getCalendarEvents, needs a rewrite

* Fix: turns out you can access protected in testcases

* fix further mocks

* Added packages/features/insights/server/buildBaseWhereCondition.ts, types

* Always great to have a mock and then not use it

* And one less again.

* fix: confirm.handler.test, didn't mock prisma

* fix: Address minor nit by @eunjae & fix ImpersonationProvider test

* Updated isPrismaAvailableCheck that doesn't crash on import

* fix: Get Prisma directly from the client, it usually involves the Validator and does not need 'local' inclusion

* Add zod-prisma-types without the generator enabled (commented out)

* Uncomment and see what happens

* Change method of import as imports did not work in Input Schemas

* Remove custom 'zod' booking model, it does not belong with Prisma

* Fix all other global Model imports

* Rewrite most schema includes AND remove barrel file

* Add bookingCreateBodySchema to features/bookings

* Flurry of type fixes for compatibility with new zod gen

* Refactor out the custom prisma type createEventTypeInput

* Work around nullable eventTypeLocations

* HandlePayment type fix

* More fixes, final fix remaining is CompleteEventType

* Should fix a bunch more booking related type errors

* Missed one

* Some props missing from BookingCreateBodySchema

* Fix location type in handleChildrenEventTypes

* Little bit hacky imo but it works

* Final type error \o/

* Forgot to include Prisma

* Do not include zod-utils in booker/types

* Oops, was already including Booker/types

* Fix membership type, also disallow updating createdAt/updatedAt, make part of patch/post

* Fix api v1 type errors

* Fix EventTypeDescription typings

* Remove getParserWithGeneric, use userBodySchema with UserSchema

* use centralized timeZoneSchema

* Implement feedback by @zomars

* Couple of WIP pushes

* Fix tests

* Type fixes in `handleChildrenEventTypes` test

* Try and parse metadata before use

* Change zod-prisma-types configuration for optimal performance

* Fix prisma validator error in `prisma/selects/credential`

* Disable seperate relations model, hits a bug

* Import absolute - this makes rollup work in @platform/libraries

* Attempt at removing resolutions override

* Refactor using `Prisma.validator` to `satisfies`

* Build atoms using @calcom/prisma/client

* Build atoms using @calcom/prisma/client

* fixes

* Update eventTypeSelect.ts

* Adjust `eventTypeMetaDataSchemaWithUntypedApps` from `unknown` to `record(any)`

* `EventTypeDescription` rely on `descriptionAsSafeHTML` instead of `description`

* Add `seatsPerTimeSlot` to event type public select

* Fix typing in `users-public-view` getServerSide props

* Add missing `schedulingType` to prop

* chore: bump platform libraries

* Function return type is illegal, not sure how this passed eslint (#21567)

* Merged with main

* Update updateTokenObject.ts

* Update handleResponse.ts

* Update index.ts

* Update handleChildrenEventTypes.ts

* Update booking-idempotency-key.ts

* Update WebhookService.test.ts

* Update events.test.ts

* Update queued-response.test.ts

* Update events.test.ts

* Update getRoutedUrl.test.ts

* fix: type checks

Signed-off-by: Omar López <[email protected]>

* fixes

Signed-off-by: Omar López <[email protected]>

* chore: bump platform libraries

* Update yarn.lock

* more fixes

Signed-off-by: Omar López <[email protected]>

* fixes

Signed-off-by: Omar López <[email protected]>

* biuld fixes

* chore: bump platform libraries

* Update conferencing.repository.ts

* Update conferencing.repository.ts

* Update getCalendarsEvents.test.ts

* Update vite.config.js

* chore: bump platform libraries

* Update users.ts

* Discard changes to docs/api-reference/v2/openapi.json

* Update vite.config.ts

* updated platform libraries

* Update get.handler.test.ts

* Update get.handler.test.ts

* Update schema.prisma

* Discard changes to docs/api-reference/v2/openapi.json

* Update next-auth-custom-adapter.ts

* Update team.ts

* Flurry of type fixes

* Fix majority of insight related type errors

* Type fixes for unlink of account

* Make user nullable again

* Fixed a bunch of unit tests and one type error

* Attempted mock fix

* Attempted fix for Attribute type

* Ensure default import becomes prisma, but not direct usage

* Import default as prisma in prisma.module

* Add attributeOption to attribute type

* Fix calcom/prisma mock

* Refactor Prisma client imports to @calcom/prisma/client

Updated all imports from '@prisma/client' to '@calcom/prisma/client' across tests and repository files for consistency and to use the correct Prisma client package. This change improves maintainability and ensures the correct client is referenced throughout the codebase.

* Undo removal of max-warnings=0 to get main to merge

* Remove unit tests for e2e fixtures, provide new prisma mock

* Mock @calcom/prisma in event manager

* Mock @calcom/prisma in event manager

* Add correct format even with --no-verify

* Mock prisma in CalendarManager

* Add mock for permission-check.service

* Better injection in PrismaApiKeyRepository imports

* More mock fixes :)

* Fix listMembers.handler.test

* Fix User import

* Appropriately adjust all types to be imported as types, there were a lot of types imported as normal deps

* Why was this a thing?

* Strictly speaking; Not using prismock anymore

* Ditched patch file for prismock

* Fix output.service.ts platform type imports, need concrete for plainToClass

* Better typing and tests for unlinkConnectedAccount.handler

* Small type fix

* Disable calendar cache tests as they are dependent on prismock

* chore: bump platform lib

* getRoutedUrl test remove of unused import

* Extract select to external const on getEventTypesFromDB

* Direct select of userSelect from selects/user

* fix type error from merging 23653

* Fixed integration tests by removing hardcoded values that were possible due to mocking, but as its now directly hitting the db no longer

* fix: vite config atoms prisma client type location

* revert: example app prisma client

* revert: example app prisma client

* bump platform libs

* fix: use class instead of type for DI of PlatformBookingsService

* update platform libs

* remove unused variable

* chore: generate prisma client for api v2

* fix: api v2 e2e

* fix: atoms e2e

* fix: atoms e2e

* fix: atoms e2e

* fix: api v2 e2e

* fix: tsconfig apiv2 enums

* publish libraries

* Simplify check for existence teamId

---------

Signed-off-by: Omar López <[email protected]>
Co-authored-by: Alex van Andel <[email protected]>
Co-authored-by: Joe Au-Yeung <[email protected]>
Co-authored-by: supalarry <[email protected]>
Co-authored-by: cal.com <[email protected]>
Co-authored-by: Morgan <[email protected]>
Co-authored-by: Benny Joo <[email protected]>
2025-09-11 15:27:50 +01:00

307 lines
12 KiB
TypeScript

/* eslint-disable @typescript-eslint/triple-slash-reference */
/// <reference path="../../../types/ical.d.ts"/>
import ICAL from "ical.js";
import dayjs from "@calcom/dayjs";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import type {
Calendar,
IntegrationCalendar,
EventBusyDate,
CalendarEvent,
NewCalendarEventType,
} from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
// for Apple's Travel Time feature only (for now)
const getTravelDurationInSeconds = (vevent: ICAL.Component) => {
const travelDuration: ICAL.Duration = vevent.getFirstPropertyValue("x-apple-travel-duration");
if (!travelDuration) return 0;
// we can't rely on this being a valid duration and it's painful to check, so just try and catch if anything throws
try {
const travelSeconds = travelDuration.toSeconds();
// integer validation as we can never be sure with ical.js
if (!Number.isInteger(travelSeconds)) return 0;
return travelSeconds;
} catch (e) {
return 0;
}
};
const applyTravelDuration = (event: ICAL.Event, seconds: number) => {
if (seconds <= 0) return event;
// move event start date back by the specified travel time
event.startDate.second -= seconds;
return event;
};
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
export default class ICSFeedCalendarService implements Calendar {
private urls: string[] = [];
protected integrationName = "ics-feed_calendar";
constructor(credential: CredentialPayload) {
const { urls } = JSON.parse(symmetricDecrypt(credential.key as string, CALENDSO_ENCRYPTION_KEY));
this.urls = urls;
}
createEvent(_event: CalendarEvent, _credentialId: number): Promise<NewCalendarEventType> {
console.warn("createEvent called on ICS (read-only) feed");
return Promise.resolve({
uid: _event.uid || "",
type: this.integrationName,
id: "",
password: "",
url: "",
additionalInfo: { calWarnings: ["ICS feed is read-only"] },
});
}
deleteEvent(_uid: string, _event: CalendarEvent, _externalCalendarId?: string): Promise<unknown> {
console.warn("deleteEvent called on ICS (read-only) feed");
return Promise.resolve();
}
updateEvent(
_uid: string,
_event: CalendarEvent,
_externalCalendarId?: string
): Promise<NewCalendarEventType | NewCalendarEventType[]> {
console.warn("updateEvent called on ICS (read-only) feed");
return Promise.resolve({
uid: _event.uid || "",
type: this.integrationName,
id: "",
password: "",
url: "",
additionalInfo: { calWarnings: ["ICS feed is read-only"] },
});
}
fetchCalendars = async (): Promise<{ url: string; vcalendar: ICAL.Component }[]> => {
const reqPromises = await Promise.allSettled(this.urls.map((x) => fetch(x).then((y) => [x, y])));
const reqs = reqPromises
.filter((x) => x.status === "fulfilled")
.map((x) => (x as PromiseFulfilledResult<[string, Response]>).value);
const res = await Promise.all(reqs.map((x) => x[1].text().then((y) => [x[0], y])));
return res
.map((x) => {
try {
const jcalData = ICAL.parse(x[1]);
return {
url: x[0],
vcalendar: new ICAL.Component(jcalData),
};
} catch (e) {
console.error("Error parsing calendar object: ", e);
return null;
}
})
.filter((x) => x !== null) as { url: string; vcalendar: ICAL.Component }[];
};
/**
* getUserTimezoneFromDB() retrieves the timezone of a user from the database.
*
* @param {number} id - The user's unique identifier.
* @returns {Promise<string | undefined>} - A Promise that resolves to the user's timezone or "Europe/London" as a default value if the timezone is not found.
*/
getUserTimezoneFromDB = async (id: number): Promise<string | undefined> => {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const user = await prisma.user.findUnique({
where: {
id,
},
select: {
timeZone: true,
},
});
return user?.timeZone;
};
/**
* getUserId() extracts the user ID from the first calendar in an array of IntegrationCalendars.
*
* @param {IntegrationCalendar[]} selectedCalendars - An array of IntegrationCalendars.
* @returns {number | null} - The user ID associated with the first calendar in the array, or null if the array is empty or the user ID is not found.
*/
getUserId = (selectedCalendars: IntegrationCalendar[]): number | null => {
if (selectedCalendars.length === 0) {
return null;
}
return selectedCalendars[0].userId || null;
};
async getAvailability(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyDate[]> {
const startISOString = new Date(dateFrom).toISOString();
const calendars = await this.fetchCalendars();
const userId = this.getUserId(selectedCalendars);
// we use the userId from selectedCalendars to fetch the user's timeZone from the database primarily for all-day events without any timezone information
const userTimeZone = userId ? await this.getUserTimezoneFromDB(userId) : "Europe/London";
const events: { start: string; end: string }[] = [];
calendars.forEach(({ vcalendar }) => {
const vevents = vcalendar.getAllSubcomponents("vevent");
vevents.forEach((vevent) => {
// if event status is free or transparent, DON'T return (unlike usual getAvailability)
//
// commented out because a lot of public ICS feeds that describe stuff like
// public holidays have them marked as transparent. if that is explicitly
// added to cal.com as an ICS feed, it should probably not be ignored.
// if (vevent?.getFirstPropertyValue("transp") === "TRANSPARENT") return;
const event = new ICAL.Event(vevent);
const dtstartProperty = vevent.getFirstProperty("dtstart");
const tzidFromDtstart = dtstartProperty ? (dtstartProperty as any).jCal[1].tzid : undefined;
const dtstart: { [key: string]: string } | undefined = vevent?.getFirstPropertyValue("dtstart");
const timezone = dtstart ? dtstart["timezone"] : undefined;
// We check if the dtstart timezone is in UTC which is actually represented by Z instead, but not recognized as that in ICAL.js as UTC
const isUTC = timezone === "Z";
// Fix precedence: prioritize TZID from DTSTART property, then standalone TZID, then UTC, then fallback
const tzid: string | undefined =
tzidFromDtstart || vevent?.getFirstPropertyValue("tzid") || (isUTC ? "UTC" : timezone);
// In case of icalendar, when only tzid is available without vtimezone, we need to add vtimezone explicitly to take care of timezone diff
if (!vcalendar.getFirstSubcomponent("vtimezone")) {
const timezoneToUse = tzid || userTimeZone;
if (timezoneToUse) {
try {
const timezoneComp = new ICAL.Component("vtimezone");
timezoneComp.addPropertyWithValue("tzid", timezoneToUse);
const standard = new ICAL.Component("standard");
// get timezone offset
const tzoffsetfrom = dayjs(event.startDate.toJSDate()).tz(timezoneToUse).format("Z");
const tzoffsetto = dayjs(event.endDate.toJSDate()).tz(timezoneToUse).format("Z");
// set timezone offset
standard.addPropertyWithValue("tzoffsetfrom", tzoffsetfrom);
standard.addPropertyWithValue("tzoffsetto", tzoffsetto);
// provide a standard dtstart
standard.addPropertyWithValue("dtstart", "1601-01-01T00:00:00");
timezoneComp.addSubcomponent(standard);
vcalendar.addSubcomponent(timezoneComp);
} catch (e) {
// Adds try-catch to ensure the code proceeds when Apple Calendar provides non-standard TZIDs
console.log("error in adding vtimezone", e);
}
} else {
console.error("No timezone found");
}
}
let vtimezone = null;
if (tzid) {
const allVtimezones = vcalendar.getAllSubcomponents("vtimezone");
vtimezone = allVtimezones.find((vtz) => vtz.getFirstPropertyValue("tzid") === tzid);
}
if (!vtimezone) {
vtimezone = vcalendar.getFirstSubcomponent("vtimezone");
}
// mutate event to consider travel time
applyTravelDuration(event, getTravelDurationInSeconds(vevent));
if (event.isRecurring()) {
let maxIterations = 365;
if (["HOURLY", "SECONDLY", "MINUTELY"].includes(event.getRecurrenceTypes())) {
console.error(`Won't handle [${event.getRecurrenceTypes()}] recurrence`);
return;
}
const start = dayjs(dateFrom);
const end = dayjs(dateTo);
const startDate = ICAL.Time.fromDateTimeString(startISOString);
startDate.hour = event.startDate.hour;
startDate.minute = event.startDate.minute;
startDate.second = event.startDate.second;
const iterator = event.iterator(startDate);
let current: ICAL.Time;
let currentEvent;
let currentStart = null;
let currentError;
while (
maxIterations > 0 &&
(currentStart === null || currentStart.isAfter(end) === false) &&
// this iterator was poorly implemented, normally done is expected to be
// returned
(current = iterator.next())
) {
maxIterations -= 1;
try {
// @see https://github.com/mozilla-comm/ical.js/issues/514
currentEvent = event.getOccurrenceDetails(current);
} catch (error) {
if (error instanceof Error && error.message !== currentError) {
currentError = error.message;
}
}
if (!currentEvent) return;
// do not mix up caldav and icalendar! For the recurring events here, the timezone
// provided is relevant, not as pointed out in https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5
// where recurring events are always in utc (in caldav!). Thus, apply the time zone here.
if (vtimezone) {
const zone = new ICAL.Timezone(vtimezone);
currentEvent.startDate = currentEvent.startDate.convertToZone(zone);
currentEvent.endDate = currentEvent.endDate.convertToZone(zone);
}
currentStart = dayjs(currentEvent.startDate.toJSDate());
if (currentStart.isBetween(start, end) === true) {
events.push({
start: currentStart.toISOString(),
end: dayjs(currentEvent.endDate.toJSDate()).toISOString(),
});
}
}
if (maxIterations <= 0) {
console.warn("could not find any occurrence for recurring event in 365 iterations");
}
return;
}
if (vtimezone) {
const zone = new ICAL.Timezone(vtimezone);
event.startDate = event.startDate.convertToZone(zone);
event.endDate = event.endDate.convertToZone(zone);
}
const finalStartISO = dayjs(event.startDate.toJSDate()).toISOString();
const finalEndISO = dayjs(event.endDate.toJSDate()).toISOString();
return events.push({
start: finalStartISO,
end: finalEndISO,
});
});
});
return Promise.resolve(events);
}
async listCalendars(): Promise<IntegrationCalendar[]> {
const vcals = await this.fetchCalendars();
return vcals.map(({ url, vcalendar }) => {
const name: string = vcalendar.getFirstPropertyValue("x-wr-calname");
return {
name,
readOnly: true,
externalId: url,
integrationName: this.integrationName,
};
});
}
}