mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
3a8fe8c7af
* perf(app-store): convert services to factory functions to narrow SDK type exports This change converts CRM, Calendar, Payment, and Analytics services from exporting classes to exporting factory functions that return interface types. This prevents SDK types (HubSpot, Stripe, etc.) from leaking into the type system when TypeScript emits .d.ts files. Services modified: - CRM: hubspot, salesforce, closecom, pipedrive-crm, zoho-bigin, zohocrm - Calendar: all 13 calendar services - Payment: stripe, paypal, alby, btcpayserver, hitpay, mock-payment-app - Analytics: dub Video adapters were audited and already use factory pattern. WIP: Salesforce CRM service has a type error that needs fixing. Co-Authored-By: [email protected] <[email protected]> * fix(salesforce): add type assertion for appOptions in factory function Co-Authored-By: [email protected] <[email protected]> * fix: update remaining consumers to use factory functions - getAnalytics.ts: call factory function instead of new - getConnectedApps.ts: call factory function instead of new - salesforce/CrmService.ts: fix type assertion with default value - salesforce/routingForm/incompleteBookingAction.ts: use factory function - salesforce/routingFormBookingFormHandler.ts: use factory function Co-Authored-By: [email protected] <[email protected]> * fix(salesforce): add SalesforceCRM interface for Salesforce-specific methods - Create SalesforceCRM interface extending CRM with findUserEmailFromLookupField and incompleteBookingWriteToRecord methods - Add createSalesforceCrmServiceWithSalesforceType factory function for internal Salesforce modules - Update routing form files to use the new factory function with proper typing Co-Authored-By: [email protected] <[email protected]> * fix(salesforce): correct return type in SalesforceCRM interface The findUserEmailFromLookupField method returns { email: string; recordType: RoutingReasons } | undefined, not string | undefined. Updated the interface to match the actual implementation. Co-Authored-By: [email protected] <[email protected]> * fix: update all call sites to use factory functions - Update calendar API add files to use factory functions (7 files) - Update Google Calendar test files to use factory functions (3 files) - Update Salesforce test files to use SalesforceCRM interface - Add testMode parameter to createSalesforceCrmServiceWithSalesforceType - Remove unused @ts-expect-error directive in bookingScenario.ts Co-Authored-By: [email protected] <[email protected]> * fix: add GoogleCalendar interface and update factory functions for service-specific methods Co-Authored-By: [email protected] <[email protected]> * fix: add type annotation to attendee parameter in google-calendar.e2e.ts Co-Authored-By: [email protected] <[email protected]> * fix: update selected-calendars/route.ts to use GoogleCalendar factory function Co-Authored-By: [email protected] <[email protected]> * fix: update remaining GoogleCalendarService call sites to use factory function Co-Authored-By: [email protected] <[email protected]> * fix: remove explicit type annotation in google-calendar.e2e.ts to fix type error Co-Authored-By: [email protected] <[email protected]> * fix: update test mocks to use factory function pattern for calendar and payment services Co-Authored-By: [email protected] <[email protected]> * fix: convert MockPaymentService to factory function in setupVitest.ts Co-Authored-By: [email protected] <[email protected]> * fix(api-v2): call calendar service factory functions instead of using new Co-Authored-By: [email protected] <[email protected]> * perf(trpc): add TypeScript project references to reduce build file count - Add tsconfig.build.json to app-store with composite settings for declaration emit - Add tsconfig.server.build.json to trpc with project references - Add types and typesVersions to app-store package.json to redirect type resolution to dist-types - Add build:types script to app-store for generating declarations - Add dist-types to .gitignore This reduces the tRPC build file count from 7,733 to 5,618 files (27% reduction) by having TypeScript resolve to prebuilt .d.ts files instead of source files. Co-Authored-By: [email protected] <[email protected]> * fix(app-store): add fallback to typesVersions for CI compatibility The typesVersions now prefers dist-types but falls back to source files when dist-types don't exist. This fixes CI type-check failures while still allowing the optimization when dist-types are built. Co-Authored-By: [email protected] <[email protected]> * Added build dependency for tRPC on app-store * fix(app-store): exclude test directories from tsconfig.build.json Exclude __tests__, __mocks__, and tests directories from the build config to prevent test utility files (which import from outside rootDir) from being included in the declaration emit. Co-Authored-By: [email protected] <[email protected]> * fix(turbo): add build:types task definition for app-store Co-Authored-By: [email protected] <[email protected]> * fix(turbo): remove circular build dependency between trpc and app-store The build:types task for app-store transitively imports from @calcom/features, which imports from @calcom/trpc. This creates a circular build dependency when @calcom/trpc#build depends on @calcom/app-store#build:types. Solution: Remove the turbo dependency. The typesVersions fallback pattern ['./dist-types/*', './*'] will use source files when dist-types/ doesn't exist, and use prebuilt declarations when they do exist (for faster builds). Also exclude dist-types from regular tsconfig.json to prevent 'Cannot write file' errors when dist-types/ exists. Co-Authored-By: [email protected] <[email protected]> * refactor(app-store): rename dist-types to types for consistency Rename the prebuilt TypeScript declarations folder from dist-types to types to match the convention used by @calcom/trpc and other packages in the monorepo. Updated files: - tsconfig.build.json: declarationDir and exclude - tsconfig.json: exclude - package.json: typesVersions paths - turbo.json: build:types outputs - .gitignore: ignore path Co-Authored-By: [email protected] <[email protected]> * fix(api-v2): update ICS calendar test to mock factory function instead of class prototype The IcsFeedCalendarService is now a factory function, not a class, so jest.spyOn(IcsFeedCalendarService.prototype, 'listCalendars') no longer works. Updated to use jest.spyOn(appStore, 'IcsFeedCalendarService').mockImplementation() to mock the factory function return value instead. Co-Authored-By: [email protected] <[email protected]> * refactor(app-store): rename factory functions to Build* naming convention - Rename Calendar factory functions from create*CalendarService to BuildCalendarService - Rename CRM factory functions from create*CrmService to BuildCrmService - Rename Payment factory functions from PaymentService to BuildPaymentService - Rename Analytics factory function from createDubAnalyticsService to BuildAnalyticsService - Update all index.ts re-exports to use new names - Update all call sites throughout the codebase - Update test mocks to use new factory function names This makes it clear that these are factory functions, not class constructors. Co-Authored-By: [email protected] <[email protected]> * fix(app-store): update remaining CalendarService and PaymentService imports to Build* names - Update api/add.ts files for applecalendar, caldavcalendar, exchange2013calendar, exchange2016calendar, exchangecalendar, ics-feedcalendar - Update mock-payment-app index.ts to export BuildPaymentService - Fix googlecalendar test import alias for createInMemoryDelegationCredentialForBuildCalendarService Co-Authored-By: [email protected] <[email protected]> * fix(tests): update payment service mocks to use BuildPaymentService - Update setupVitest.ts to use BuildPaymentService instead of PaymentService - Update handlePayment.test.ts mock to use BuildPaymentService Co-Authored-By: [email protected] <[email protected]> * refactor: address PR review comments - rename to Build* and remove unused code Co-Authored-By: [email protected] <[email protected]> * refactor: remove explanatory comments and fix indentation Co-Authored-By: [email protected] <[email protected]> * chore: update .gitignore to use *.tsbuildinfo pattern Co-Authored-By: [email protected] <[email protected]> * fix(api-v2): update IcsFeedCalendarService to BuildIcsFeedCalendarService in e2e test Co-Authored-By: [email protected] <[email protected]> * style: format CalendarService.test.ts Co-Authored-By: [email protected] <[email protected]> * Delete apps/ui-playground/next-env.d.ts --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
248 lines
8.3 KiB
TypeScript
248 lines
8.3 KiB
TypeScript
import {
|
|
Appointment,
|
|
Attendee,
|
|
CalendarView,
|
|
ConflictResolutionMode,
|
|
DateTime,
|
|
DeleteMode,
|
|
ExchangeService,
|
|
ExchangeVersion,
|
|
FolderId,
|
|
FolderView,
|
|
ItemId,
|
|
LegacyFreeBusyStatus,
|
|
MessageBody,
|
|
PropertySet,
|
|
SendInvitationsMode,
|
|
SendInvitationsOrCancellationsMode,
|
|
Uri,
|
|
WebCredentials,
|
|
WellKnownFolderName,
|
|
} from "ews-javascript-api";
|
|
|
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
|
// Probably don't need
|
|
// import { CALENDAR_INTEGRATIONS_TYPES } from "@calcom/lib/integrations/calendar/constants/generals";
|
|
import logger from "@calcom/lib/logger";
|
|
import type {
|
|
Calendar,
|
|
CalendarEvent,
|
|
EventBusyDate,
|
|
GetAvailabilityParams,
|
|
IntegrationCalendar,
|
|
NewCalendarEventType,
|
|
} from "@calcom/types/Calendar";
|
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
|
|
|
class ExchangeCalendarService implements Calendar {
|
|
private url = "";
|
|
private integrationName = "";
|
|
private log: typeof logger;
|
|
private readonly exchangeVersion: ExchangeVersion;
|
|
private credentials: Record<string, string>;
|
|
|
|
constructor(credential: CredentialPayload) {
|
|
this.integrationName = "exchange2013_calendar";
|
|
|
|
this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
|
|
|
const decryptedCredential = JSON.parse(
|
|
symmetricDecrypt(credential.key?.toString() || "", process.env.CALENDSO_ENCRYPTION_KEY || "")
|
|
);
|
|
const username = decryptedCredential.username;
|
|
const url = decryptedCredential.url;
|
|
const password = decryptedCredential.password;
|
|
|
|
this.url = url;
|
|
|
|
this.credentials = {
|
|
username,
|
|
password,
|
|
};
|
|
this.exchangeVersion = ExchangeVersion.Exchange2013;
|
|
}
|
|
|
|
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
|
try {
|
|
const appointment = new Appointment(this.getExchangeService()); // service instance of ExchangeService
|
|
appointment.Subject = event.title;
|
|
appointment.Start = DateTime.Parse(event.startTime); // moment string
|
|
appointment.End = DateTime.Parse(event.endTime); // moment string
|
|
appointment.Location = event.location || "Location not defined!";
|
|
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content
|
|
|
|
for (let i = 0; i < event.attendees.length; i++) {
|
|
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
|
}
|
|
|
|
if (event.team?.members) {
|
|
event.team.members.forEach((member) => {
|
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
|
});
|
|
}
|
|
|
|
await appointment.Save(SendInvitationsMode.SendToAllAndSaveCopy);
|
|
|
|
return {
|
|
uid: appointment.Id.UniqueId,
|
|
id: appointment.Id.UniqueId,
|
|
password: "",
|
|
type: "",
|
|
url: "",
|
|
additionalInfo: {},
|
|
};
|
|
} catch (reason) {
|
|
this.log.error(reason);
|
|
throw reason;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
|
try {
|
|
const appointment = await Appointment.Bind(
|
|
this.getExchangeService(),
|
|
new ItemId(uid),
|
|
new PropertySet()
|
|
);
|
|
appointment.Subject = event.title;
|
|
appointment.Start = DateTime.Parse(event.startTime); // moment string
|
|
appointment.End = DateTime.Parse(event.endTime); // moment string
|
|
appointment.Location = event.location || "Location not defined!";
|
|
appointment.Body = new MessageBody(event.description || ""); // you can not use any special character or escape the content
|
|
for (let i = 0; i < event.attendees.length; i++) {
|
|
appointment.RequiredAttendees.Add(new Attendee(event.attendees[i].email));
|
|
}
|
|
if (event.team?.members) {
|
|
event.team.members.forEach((member) => {
|
|
appointment.RequiredAttendees.Add(new Attendee(member.email));
|
|
});
|
|
}
|
|
appointment.Update(
|
|
ConflictResolutionMode.AlwaysOverwrite,
|
|
SendInvitationsOrCancellationsMode.SendToAllAndSaveCopy
|
|
);
|
|
} catch (reason) {
|
|
this.log.error(reason);
|
|
throw reason;
|
|
}
|
|
}
|
|
|
|
async deleteEvent(uid: string) {
|
|
try {
|
|
const appointment = await Appointment.Bind(
|
|
this.getExchangeService(),
|
|
new ItemId(uid),
|
|
new PropertySet()
|
|
);
|
|
// Delete the appointment. Note that the item ID will change when the item is moved to the Deleted Items folder.
|
|
appointment.Delete(DeleteMode.MoveToDeletedItems);
|
|
} catch (reason) {
|
|
this.log.error(reason);
|
|
throw reason;
|
|
}
|
|
}
|
|
|
|
async getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]> {
|
|
const { dateFrom, dateTo, selectedCalendars } = params;
|
|
try {
|
|
const externalCalendars: IntegrationCalendar[] = await this.listCalendars();
|
|
const calendarsToGetAppointmentsFrom = [];
|
|
for (let i = 0; i < selectedCalendars.length; i++) {
|
|
//Only select valid calendars! (We get all all active calendars on the instance! even from different users!)
|
|
for (let k = 0; k < externalCalendars.length; k++) {
|
|
if (selectedCalendars[i].externalId == externalCalendars[k].externalId) {
|
|
calendarsToGetAppointmentsFrom.push(selectedCalendars[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
const finaleRet = [];
|
|
for (let i = 0; i < calendarsToGetAppointmentsFrom.length; i++) {
|
|
const calendarFolderId = new FolderId(calendarsToGetAppointmentsFrom[i].externalId);
|
|
const localReturn = await this.getExchangeService()
|
|
.FindAppointments(
|
|
calendarFolderId,
|
|
new CalendarView(DateTime.Parse(dateFrom), DateTime.Parse(dateTo))
|
|
)
|
|
.then(function (params) {
|
|
const ret: EventBusyDate[] = [];
|
|
|
|
for (let k = 0; k < params.Items.length; k++) {
|
|
if (params.Items[k].LegacyFreeBusyStatus != LegacyFreeBusyStatus.Free) {
|
|
//Dont use this appointment if "ShowAs" was set to "free"
|
|
ret.push({
|
|
start: new Date(params.Items[k].Start.ToISOString()),
|
|
end: new Date(params.Items[k].End.ToISOString()),
|
|
});
|
|
}
|
|
}
|
|
return ret;
|
|
});
|
|
finaleRet.push(...localReturn);
|
|
}
|
|
|
|
return finaleRet;
|
|
} catch (reason) {
|
|
this.log.error(reason);
|
|
throw reason;
|
|
}
|
|
}
|
|
|
|
async listCalendars() {
|
|
try {
|
|
const allFolders: IntegrationCalendar[] = [];
|
|
return this.getExchangeService()
|
|
.FindFolders(WellKnownFolderName.MsgFolderRoot, new FolderView(1000))
|
|
.then(async (res) => {
|
|
for (const k in res.Folders) {
|
|
const f = res.Folders[k];
|
|
if (f.FolderClass == "IPF.Appointment") {
|
|
//Select parent folder for all calendars
|
|
allFolders.push({
|
|
externalId: f.Id.UniqueId,
|
|
name: f.DisplayName ?? "",
|
|
primary: true, //The first one is prime
|
|
integration: this.integrationName,
|
|
});
|
|
return await this.getExchangeService()
|
|
.FindFolders(f.Id, new FolderView(1000))
|
|
.then((res) => {
|
|
//Find all calendars inside calendar folder
|
|
res.Folders.forEach((fs) => {
|
|
allFolders.push({
|
|
externalId: fs.Id.UniqueId,
|
|
name: fs.DisplayName ?? "",
|
|
primary: false,
|
|
integration: this.integrationName,
|
|
});
|
|
});
|
|
return allFolders;
|
|
});
|
|
}
|
|
}
|
|
return allFolders;
|
|
}) as Promise<IntegrationCalendar[]>;
|
|
} catch (reason) {
|
|
this.log.error(reason);
|
|
throw reason;
|
|
}
|
|
}
|
|
|
|
private getExchangeService(): ExchangeService {
|
|
const exch1 = new ExchangeService(this.exchangeVersion);
|
|
exch1.Credentials = new WebCredentials(this.credentials.username, this.credentials.password);
|
|
exch1.Url = new Uri(this.url);
|
|
return exch1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Factory function that creates an Exchange 2013 Calendar service instance.
|
|
* This is exported instead of the class to prevent SDK types (like ews-javascript-api types)
|
|
* from leaking into the emitted .d.ts file.
|
|
*/
|
|
export default function BuildCalendarService(credential: CredentialPayload): Calendar {
|
|
return new ExchangeCalendarService(credential);
|
|
}
|