import {
CalendarEvent,
Calendar,
TimeSlot,
AvailabilityConstraints,
EventSearchCriteria,
FreetimeSearchCriteria,
CreateEventInput,
ApiResponse,
Attendee
} from '../types';
/**
* Abstract base class for calendar providers
* Defines the interface that all calendar providers must implement
*/
export abstract class CalendarProvider {
protected accessToken: string;
protected refreshToken?: string;
protected tokenExpiry: Date;
constructor(accessToken: string, refreshToken?: string, tokenExpiry?: Date) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.tokenExpiry = tokenExpiry || new Date();
}
// Abstract methods that must be implemented by providers
abstract getCalendars(): Promise<ApiResponse<Calendar[]>>;
abstract getEvents(
calendarId: string,
startDate: Date,
endDate: Date,
query?: string
): Promise<ApiResponse<CalendarEvent[]>>;
abstract getEvent(
calendarId: string,
eventId: string
): Promise<ApiResponse<CalendarEvent>>;
abstract createEvent(
calendarId: string,
eventData: CreateEventInput
): Promise<ApiResponse<CalendarEvent>>;
abstract updateEvent(
calendarId: string,
eventId: string,
updates: Partial<CreateEventInput>
): Promise<ApiResponse<CalendarEvent>>;
abstract deleteEvent(
calendarId: string,
eventId: string
): Promise<ApiResponse<boolean>>;
abstract findFreeTime(
criteria: FreetimeSearchCriteria
): Promise<ApiResponse<TimeSlot[]>>;
abstract checkAvailability(
emailAddresses: string[],
startTime: Date,
endTime: Date
): Promise<ApiResponse<Record<string, boolean>>>;
// Common utility methods
protected isTokenExpired(): boolean {
return new Date() >= this.tokenExpiry;
}
protected abstract refreshAccessToken(): Promise<void>;
protected async ensureValidToken(): Promise<void> {
if (this.isTokenExpired() && this.refreshToken) {
await this.refreshAccessToken();
}
}
// Batch operations (optional implementations)
async batchCreateEvents(
calendarId: string,
events: CreateEventInput[]
): Promise<ApiResponse<CalendarEvent[]>> {
// Default implementation: sequential creation
const results: CalendarEvent[] = [];
const errors: string[] = [];
for (const eventData of events) {
try {
const response = await this.createEvent(calendarId, eventData);
if (response.success && response.data) {
results.push(response.data);
} else {
errors.push(response.error?.message || 'Unknown error');
}
} catch (error) {
errors.push(error instanceof Error ? error.message : 'Unknown error');
}
}
return {
success: errors.length === 0,
data: results,
error: errors.length > 0 ? {
code: 'BATCH_PARTIAL_FAILURE',
message: `${errors.length} events failed to create`,
details: { errors }
} : undefined
};
}
async batchUpdateEvents(
calendarId: string,
updates: Array<{ eventId: string; updates: Partial<CreateEventInput> }>
): Promise<ApiResponse<CalendarEvent[]>> {
// Default implementation: sequential updates
const results: CalendarEvent[] = [];
const errors: string[] = [];
for (const { eventId, updates: eventUpdates } of updates) {
try {
const response = await this.updateEvent(calendarId, eventId, eventUpdates);
if (response.success && response.data) {
results.push(response.data);
} else {
errors.push(response.error?.message || 'Unknown error');
}
} catch (error) {
errors.push(error instanceof Error ? error.message : 'Unknown error');
}
}
return {
success: errors.length === 0,
data: results,
error: errors.length > 0 ? {
code: 'BATCH_PARTIAL_FAILURE',
message: `${errors.length} events failed to update`,
details: { errors }
} : undefined
};
}
// Time zone utilities
protected formatDateForProvider(date: Date): string {
return date.toISOString();
}
protected parseDateFromProvider(dateString: string): Date {
return new Date(dateString);
}
// Free time calculation utilities
protected calculateFreeSlots(
events: CalendarEvent[],
startDate: Date,
endDate: Date,
durationMinutes: number,
constraints?: AvailabilityConstraints
): TimeSlot[] {
const freeSlots: TimeSlot[] = [];
const sortedEvents = events.sort((a, b) => a.startTime.getTime() - b.startTime.getTime());
let currentTime = new Date(startDate);
for (const event of sortedEvents) {
// Check if there's a gap before this event
const gapDuration = event.startTime.getTime() - currentTime.getTime();
const gapMinutes = gapDuration / (1000 * 60);
if (gapMinutes >= durationMinutes) {
const slotEnd = new Date(Math.min(event.startTime.getTime(), endDate.getTime()));
if (this.isSlotValid(currentTime, slotEnd, durationMinutes, constraints)) {
freeSlots.push({
start: new Date(currentTime),
end: slotEnd,
available: true
});
}
}
currentTime = new Date(Math.max(currentTime.getTime(), event.endTime.getTime()));
}
// Check for a slot after the last event
const finalGapDuration = endDate.getTime() - currentTime.getTime();
const finalGapMinutes = finalGapDuration / (1000 * 60);
if (finalGapMinutes >= durationMinutes) {
if (this.isSlotValid(currentTime, endDate, durationMinutes, constraints)) {
freeSlots.push({
start: new Date(currentTime),
end: new Date(endDate),
available: true
});
}
}
return freeSlots;
}
private isSlotValid(
start: Date,
end: Date,
durationMinutes: number,
constraints?: AvailabilityConstraints
): boolean {
if (!constraints) return true;
// Check working hours
if (constraints.workingHours) {
const dayName = start.toLocaleDateString('en-US', { weekday: 'lowercase' }) as keyof typeof constraints.workingHours;
const daySchedule = constraints.workingHours[dayName];
if (!daySchedule) return false; // No working hours defined for this day
const startTime = start.toTimeString().slice(0, 5); // HH:MM format
const endTime = end.toTimeString().slice(0, 5);
if (startTime < daySchedule.startTime || endTime > daySchedule.endTime) {
return false;
}
}
// Check weekend exclusion
if (constraints.excludeWeekends) {
const dayOfWeek = start.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) return false; // Sunday or Saturday
}
// Check minimum notice period
if (constraints.minimumNoticePeriod) {
const now = new Date();
const noticeTime = constraints.minimumNoticePeriod * 60 * 1000; // Convert to milliseconds
if (start.getTime() - now.getTime() < noticeTime) {
return false;
}
}
return true;
}
}
// Provider factory for creating provider instances
export class ProviderFactory {
static async createProvider(
type: 'google' | 'outlook' | 'apple' | 'caldav',
accessToken: string,
refreshToken?: string,
tokenExpiry?: Date
): Promise<CalendarProvider> {
switch (type) {
case 'google':
const { GoogleCalendarProvider } = await import('./GoogleCalendarProvider');
return new GoogleCalendarProvider(accessToken, refreshToken, tokenExpiry);
case 'outlook':
// Future implementation
throw new Error('Outlook provider not yet implemented');
case 'apple':
// Future implementation
throw new Error('Apple Calendar provider not yet implemented');
case 'caldav':
// Future implementation
throw new Error('CalDAV provider not yet implemented');
default:
throw new Error(`Unknown provider type: ${type}`);
}
}
}