UNPKG

@checkfirst/nestjs-outlook

Version:

An opinionated NestJS module for Microsoft Outlook integration that provides easy access to Microsoft Graph API for emails, calendars, and more.

901 lines 64.7 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; var __asyncDelegator = (this && this.__asyncDelegator) || function (o) { var i, p; return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i; function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; } }; var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i; function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; } function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } } function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } function fulfill(value) { resume("next", value); } function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } }; var CalendarService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.CalendarService = void 0; const common_1 = require("@nestjs/common"); const event_emitter_1 = require("@nestjs/event-emitter"); const microsoft_graph_client_1 = require("@microsoft/microsoft-graph-client"); const axios_1 = require("axios"); const microsoft_auth_service_1 = require("../auth/microsoft-auth.service"); const schedule_1 = require("@nestjs/schedule"); const outlook_webhook_subscription_repository_1 = require("../../repositories/outlook-webhook-subscription.repository"); const outlook_delta_link_repository_1 = require("../../repositories/outlook-delta-link.repository"); const constants_1 = require("../../constants"); const event_types_enum_1 = require("../../enums/event-types.enum"); const typeorm_1 = require("@nestjs/typeorm"); const microsoft_user_entity_1 = require("../../entities/microsoft-user.entity"); const typeorm_2 = require("typeorm"); const delta_sync_service_1 = require("../shared/delta-sync.service"); const retry_util_1 = require("../../utils/retry.util"); const user_id_converter_service_1 = require("../shared/user-id-converter.service"); const resource_type_enum_1 = require("../../enums/resource-type.enum"); const microsoft_subscription_service_1 = require("../subscription/microsoft-subscription.service"); const outlook_api_executor_util_1 = require("../../utils/outlook-api-executor.util"); const graph_rate_limiter_service_1 = require("../shared/graph-rate-limiter.service"); const OUTLOOK_EVENT_CREATED = event_types_enum_1.OutlookEventTypes.EVENT_CREATED; const OUTLOOK_EVENT_UPDATED = event_types_enum_1.OutlookEventTypes.EVENT_UPDATED; const OUTLOOK_EVENT_DELETED = event_types_enum_1.OutlookEventTypes.EVENT_DELETED; const EVENT_TYPE_TO_CHANGE_TYPE = { [OUTLOOK_EVENT_CREATED]: "created", [OUTLOOK_EVENT_UPDATED]: "updated", [OUTLOOK_EVENT_DELETED]: "deleted", }; function isNewEvent(change) { var _a; if (!change.createdDateTime) { return true; } const lastModified = new Date((_a = change.lastModifiedDateTime) !== null && _a !== void 0 ? _a : change.createdDateTime).getTime(); const created = new Date(change.createdDateTime).getTime(); return lastModified - created <= 1000; } function detectEventType(change) { if (change["@removed"]) { return OUTLOOK_EVENT_DELETED; } return isNewEvent(change) ? OUTLOOK_EVENT_CREATED : OUTLOOK_EVENT_UPDATED; } let CalendarService = CalendarService_1 = class CalendarService { constructor(microsoftAuthService, webhookSubscriptionRepository, eventEmitter, microsoftConfig, deltaLinkRepository, microsoftUserRepository, deltaSyncService, userIdConverter, subscriptionService, rateLimiter) { this.microsoftAuthService = microsoftAuthService; this.webhookSubscriptionRepository = webhookSubscriptionRepository; this.eventEmitter = eventEmitter; this.microsoftConfig = microsoftConfig; this.deltaLinkRepository = deltaLinkRepository; this.microsoftUserRepository = microsoftUserRepository; this.deltaSyncService = deltaSyncService; this.userIdConverter = userIdConverter; this.subscriptionService = subscriptionService; this.rateLimiter = rateLimiter; this.logger = new common_1.Logger(CalendarService_1.name); this.syncLocks = new Map(); } async getDefaultCalendarId(externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessToken({ externalUserId }); const response = await axios_1.default.get("https://graph.microsoft.com/v1.0/me/calendar", { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); if (!response.data.id) { throw new Error("Failed to retrieve calendar ID"); } return response.data.id; } catch (error) { this.logger.error("Error getting default calendar ID:", error); throw new Error("Failed to get calendar ID from Microsoft"); } } async createEvent(event, externalUserId, calendarId) { await this.rateLimiter.acquirePermit(externalUserId); try { const accessToken = await this.microsoftAuthService.getUserAccessToken({ externalUserId }); const client = microsoft_graph_client_1.Client.init({ authProvider: (done) => { done(null, accessToken); }, }); const createdEvent = (await client .api(`/me/calendars/${calendarId}/events`) .post(event)); return { event: createdEvent, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to create Outlook calendar event: ${errorMessage}`); throw new Error(`Failed to create Outlook calendar event: ${errorMessage}`); } finally { this.rateLimiter.releasePermit(externalUserId); } } async updateEvent(eventId, updates, externalUserId, calendarId) { await this.rateLimiter.acquirePermit(externalUserId); try { const accessToken = await this.microsoftAuthService.getUserAccessToken({ externalUserId }); const client = microsoft_graph_client_1.Client.init({ authProvider: (done) => { done(null, accessToken); }, }); this.logger.log(`Updating event ${eventId} in calendar ${calendarId} for user ${externalUserId}`); const updatedEvent = (await client .api(`/me/calendars/${calendarId}/events/${eventId}`) .patch(updates)); return { event: updatedEvent, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to update Outlook calendar event: ${errorMessage}`); throw new Error(`Failed to update Outlook calendar event: ${errorMessage}`); } finally { this.rateLimiter.releasePermit(externalUserId); } } async deleteEvent(event, externalUserId, calendarId) { await this.rateLimiter.acquirePermit(externalUserId); try { const internalUserId = await this.userIdConverter.toInternalUserId(externalUserId); const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId }); const client = microsoft_graph_client_1.Client.init({ authProvider: (done) => { done(null, accessToken); }, }); this.logger.log(`Deleting event ${event.id} from calendar ${calendarId} for user ${externalUserId}`); await (0, retry_util_1.retryWithBackoff)(async () => { await client .api(`/me/calendars/${calendarId}/events/${event.id}`) .delete(); }, { maxRetries: 3, retryDelayMs: 1000 }); } catch (error) { if ((0, retry_util_1.is404Error)(error)) { this.logger.warn(`Outlook calendar event ${event.id} not found (already deleted), treating as successful deletion`); return; } const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to delete Outlook calendar event after retries: ${errorMessage}`); throw new Error(`Failed to delete Outlook calendar event: ${errorMessage}`); } finally { this.rateLimiter.releasePermit(externalUserId); } } async createBatchEvents(events, externalUserId, calendarId) { if (events.length === 0) { return []; } try { const internalUserId = await this.userIdConverter.toInternalUserId(externalUserId); const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId }); const results = []; const BATCH_SIZE = 20; for (let i = 0; i < events.length; i += BATCH_SIZE) { const batchEvents = events.slice(i, i + BATCH_SIZE); await this.rateLimiter.acquirePermit(externalUserId); try { const batchPayload = { requests: batchEvents.map((event, index) => ({ id: `${index}`, method: 'POST', url: `/me/calendars/${calendarId}/events`, body: event, headers: { 'Content-Type': 'application/json', }, })), }; this.logger.log(`Creating batch of ${batchEvents.length} events in calendar ${calendarId} for user ${externalUserId}`); const batchResponse = await (0, retry_util_1.retryWithBackoff)(async () => { const response = await axios_1.default.post('https://graph.microsoft.com/v1.0/$batch', batchPayload, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); return response.data; }, { maxRetries: 3, retryDelayMs: 1000 }); batchResponse.responses.forEach((response, batchIndex) => { const globalIndex = i + batchIndex; if (response.status === 201) { results.push({ index: globalIndex, success: true, event: response.body, }); this.logger.debug(`Successfully created event at index ${globalIndex}`); } else { const errorMessage = JSON.stringify(response.body); results.push({ index: globalIndex, success: false, error: `HTTP ${response.status}: ${errorMessage}`, }); this.logger.error(`Failed to create event at index ${globalIndex}: ${errorMessage}`); } }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; batchEvents.forEach((_, batchIndex) => { const globalIndex = i + batchIndex; results.push({ index: globalIndex, success: false, error: `Batch request failed: ${errorMessage}`, }); }); this.logger.error(`Batch creation failed for ${batchEvents.length} events: ${errorMessage}`); } finally { this.rateLimiter.releasePermit(externalUserId); } } const successCount = results.filter(r => r.success).length; const failCount = results.filter(r => !r.success).length; this.logger.log(`Batch creation complete: ${successCount} succeeded, ${failCount} failed out of ${events.length} total`); return results; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error(`Failed to execute batch creation: ${errorMessage}`); return events.map((_, index) => ({ index, success: false, error: `Batch creation failed: ${errorMessage}`, })); } } async updateBatchEvents(updates, externalUserId, calendarId) { if (updates.length === 0) { return []; } try { const internalUserId = await this.userIdConverter.toInternalUserId(externalUserId); const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId }); const results = []; const BATCH_SIZE = 20; for (let i = 0; i < updates.length; i += BATCH_SIZE) { const batchUpdates = updates.slice(i, i + BATCH_SIZE); const batchPayload = { requests: batchUpdates.map((update, index) => ({ id: `${index}`, method: 'PATCH', url: `/me/calendars/${calendarId}/events/${update.eventId}`, body: update.updates, headers: { 'Content-Type': 'application/json', }, })), }; this.logger.log(`Updating batch of ${batchUpdates.length} events in calendar ${calendarId} for user ${externalUserId}`); try { const batchResponse = await (0, retry_util_1.retryWithBackoff)(async () => { const response = await axios_1.default.post('https://graph.microsoft.com/v1.0/$batch', batchPayload, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); return response.data; }, { maxRetries: 3, retryDelayMs: 1000 }); batchResponse.responses.forEach((response, batchIndex) => { const globalIndex = i + batchIndex; if (response.status === 200) { results.push({ index: globalIndex, success: true, event: response.body, }); this.logger.debug(`Successfully updated event at index ${globalIndex}`); } else { const errorMessage = JSON.stringify(response.body); results.push({ index: globalIndex, success: false, error: `HTTP ${response.status}: ${errorMessage}`, }); this.logger.error(`Failed to update event at index ${globalIndex}: ${errorMessage}`); } }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; batchUpdates.forEach((_, batchIndex) => { const globalIndex = i + batchIndex; results.push({ index: globalIndex, success: false, error: `Batch request failed: ${errorMessage}`, }); }); this.logger.error(`Batch update failed for ${batchUpdates.length} events: ${errorMessage}`); } } const successCount = results.filter(r => r.success).length; const failCount = results.filter(r => !r.success).length; this.logger.log(`Batch update complete: ${successCount} succeeded, ${failCount} failed out of ${updates.length} total`); return results; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error(`Failed to execute batch update: ${errorMessage}`); return updates.map((_, index) => ({ index, success: false, error: `Batch update failed: ${errorMessage}`, })); } } async deleteBatchEvents(eventIds, externalUserId, calendarId) { if (eventIds.length === 0) { return []; } try { const internalUserId = await this.userIdConverter.toInternalUserId(externalUserId); const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId }); const results = []; const BATCH_SIZE = 20; for (let i = 0; i < eventIds.length; i += BATCH_SIZE) { const batchEventIds = eventIds.slice(i, i + BATCH_SIZE); const batchPayload = { requests: batchEventIds.map((eventId, index) => ({ id: `${index}`, method: 'DELETE', url: `/me/calendars/${calendarId}/events/${eventId}`, })), }; this.logger.log(`Deleting batch of ${batchEventIds.length} events from calendar ${calendarId} for user ${externalUserId}`); try { const batchResponse = await (0, retry_util_1.retryWithBackoff)(async () => { const response = await axios_1.default.post('https://graph.microsoft.com/v1.0/$batch', batchPayload, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }); return response.data; }, { maxRetries: 3, retryDelayMs: 1000 }); batchResponse.responses.forEach((response, index) => { const eventId = batchEventIds[index]; if (response.status === 204 || response.status === 404) { results.push({ id: eventId, success: true }); if (response.status === 404) { this.logger.warn(`Event ${eventId} not found (already deleted), treating as successful deletion`); } } else { const errorMessage = response.body ? JSON.stringify(response.body) : 'Unknown error'; results.push({ id: eventId, success: false, error: `HTTP ${response.status}: ${errorMessage}`, }); this.logger.error(`Failed to delete event ${eventId}: ${errorMessage}`); } }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; batchEventIds.forEach(eventId => { results.push({ id: eventId, success: false, error: `Batch request failed: ${errorMessage}`, }); }); this.logger.error(`Batch deletion failed for ${batchEventIds.length} events: ${errorMessage}`); } } const successCount = results.filter(r => r.success).length; const failCount = results.filter(r => !r.success).length; this.logger.log(`Batch deletion complete: ${successCount} succeeded, ${failCount} failed out of ${eventIds.length} total`); return results; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error(`Failed to execute batch deletion: ${errorMessage}`); return eventIds.map(id => ({ id, success: false, error: `Batch deletion failed: ${errorMessage}`, })); } } async createWebhookSubscription(externalUserId) { var _a, _b; const internalUserId = await this.userIdConverter.externalToInternal(externalUserId, { cache: false }); const correlationId = `webhook-${internalUserId}-${Date.now()}`; this.logger.log(`[${correlationId}] Starting webhook subscription creation for user ${internalUserId}`); try { this.logger.log(`[${correlationId}] Fetching access token for user ${internalUserId}`); const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId, cache: false }); this.logger.log(`[${correlationId}] Successfully obtained access token`); const expirationDateTime = new Date(); expirationDateTime.setHours(expirationDateTime.getHours() + 72); const appUrl = this.microsoftConfig.backendBaseUrl || "http://localhost:3000"; const basePath = this.microsoftConfig.basePath; const basePathUrl = basePath ? `${appUrl}/${basePath}` : appUrl; const webhookPath = this.microsoftConfig.calendarWebhookPath || '/calendar/webhook'; const notificationUrl = `${basePathUrl}${webhookPath}`; const subscriptionData = { changeType: "created,updated,deleted", notificationUrl, lifecycleNotificationUrl: notificationUrl, resource: "/me/events", expirationDateTime: expirationDateTime.toISOString(), clientState: `user_${internalUserId}_${Math.random().toString(36).substring(2, 15)}`, }; this.logger.log(`[${correlationId}] Creating webhook subscription with notificationUrl: ${notificationUrl}`); this.logger.debug(`[${correlationId}] Subscription data: ${JSON.stringify(subscriptionData)}`); this.logger.log(`[${correlationId}] Sending POST request to Microsoft Graph API`); const response = await axios_1.default.post("https://graph.microsoft.com/v1.0/subscriptions", subscriptionData, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); this.logger.log(`[${correlationId}] Created webhook subscription ${response.data.id || "unknown"} for user ${internalUserId}`); this.logger.log(`[${correlationId}] Saving subscription to database (internalUserId: ${internalUserId}, externalUserId: ${externalUserId})`); await this.webhookSubscriptionRepository.saveSubscription({ subscriptionId: response.data.id, userId: internalUserId, resource: response.data.resource, changeType: response.data.changeType, clientState: response.data.clientState || "", notificationUrl: response.data.notificationUrl, expirationDateTime: response.data.expirationDateTime ? new Date(response.data.expirationDateTime) : new Date(), }); this.logger.log(`[${correlationId}] Successfully stored subscription in database`); this.logger.log(`[${correlationId}] Webhook subscription creation completed successfully`); return response.data; } catch (error) { if (axios_1.default.isAxiosError(error)) { this.logger.error(`[${correlationId}] Microsoft Graph API error: Status ${(_a = error.response) === null || _a === void 0 ? void 0 : _a.status}, ` + `Message: ${JSON.stringify((_b = error.response) === null || _b === void 0 ? void 0 : _b.data)}`); } else { this.logger.error(`[${correlationId}] Failed to create webhook subscription:`, error); } throw new Error(`Failed to create webhook subscription: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async renewWebhookSubscription(subscriptionId, internalUserId) { var _a, _b; const correlationId = `renew-${subscriptionId}-${Date.now()}`; try { this.logger.log(`[${correlationId}] Attempting to renew subscription ${subscriptionId} for user ${internalUserId}`); const user = await this.microsoftUserRepository.findOne({ where: { id: internalUserId, isActive: true } }); if (!user) { this.logger.warn(`[${correlationId}] User ${internalUserId} not found or inactive. ` + `Deactivating subscription ${subscriptionId}`); await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); throw new Error(`Cannot renew subscription ${subscriptionId}: ` + `User ${internalUserId} not found or inactive. ` + `Subscription has been automatically deactivated.`); } this.logger.debug(`[${correlationId}] User ${internalUserId} validated successfully`); const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId }); this.logger.debug(`[${correlationId}] Access token obtained`); const expirationDateTime = new Date(); expirationDateTime.setHours(expirationDateTime.getHours() + 72); const renewalData = { expirationDateTime: expirationDateTime.toISOString(), }; this.logger.debug(`[${correlationId}] Calling Microsoft Graph API to renew subscription`); const response = await axios_1.default.patch(`https://graph.microsoft.com/v1.0/subscriptions/${subscriptionId}`, renewalData, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); this.logger.debug(`[${correlationId}] Microsoft Graph API returned status: ${response.status}`); if (response.data.expirationDateTime) { await this.webhookSubscriptionRepository.updateSubscriptionExpiration(subscriptionId, new Date(response.data.expirationDateTime)); this.logger.debug(`[${correlationId}] Updated local database with new expiration: ${response.data.expirationDateTime}`); } this.logger.log(`[${correlationId}] Successfully renewed subscription ${subscriptionId}. ` + `New expiration: ${response.data.expirationDateTime}`); return response.data; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; if (axios_1.default.isAxiosError(error)) { const statusCode = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status; if (statusCode === 404) { this.logger.warn(`[${correlationId}] Subscription ${subscriptionId} not found at Microsoft. ` + `Deactivating in local database.`); await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); throw new Error(`Subscription ${subscriptionId} not found at Microsoft. ` + `Subscription has been automatically deactivated.`); } if (statusCode === 401 || statusCode === 403) { this.logger.error(`[${correlationId}] Authentication failed for subscription ${subscriptionId}. ` + `Status: ${statusCode}, Response: ${JSON.stringify((_b = error.response) === null || _b === void 0 ? void 0 : _b.data)}`); } if (statusCode === 429) { this.logger.warn(`[${correlationId}] Rate limited by Microsoft Graph API for subscription ${subscriptionId}`); } } this.logger.error(`[${correlationId}] Failed to renew subscription ${subscriptionId}: ${errorMessage}`); throw new Error(`Failed to renew webhook subscription: ${errorMessage}`); } } async getSubscription(subscriptionId) { const subscription = await this.webhookSubscriptionRepository.findBySubscriptionId(subscriptionId); if (!subscription) { return null; } return subscription; } async deleteWebhookSubscription(subscriptionId, userId) { var _a; const correlationId = `delete-sub-${subscriptionId}-${Date.now()}`; try { this.logger.log(`[${correlationId}] Deleting calendar subscription ${subscriptionId} for user ${userId}`); const internalUserId = await this.userIdConverter.toInternalUserId(userId); const accessToken = await this.microsoftAuthService.getUserAccessToken({ internalUserId, includeInactive: true }); this.logger.debug(`[${correlationId}] Access token obtained`); this.logger.debug(`[${correlationId}] Calling MicrosoftSubscriptionService to delete subscription`); this.logger.log(`Deleted webhook subscription: ${subscriptionId}`); await this.subscriptionService.deleteSubscription(subscriptionId, accessToken); this.logger.log(`[${correlationId}] Successfully deleted subscription at Microsoft`); await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); this.logger.debug(`[${correlationId}] Deactivated subscription in local database`); const otherActiveSubscriptions = await this.webhookSubscriptionRepository.count({ where: { userId: internalUserId, isActive: true, subscriptionId: (0, typeorm_2.Not)(subscriptionId) } }); if (otherActiveSubscriptions === 0) { const updateCriteria = typeof userId === 'string' ? { externalUserId: userId } : { id: userId }; await this.microsoftUserRepository.update(updateCriteria, { isActive: false }); this.logger.debug(`[${correlationId}] Marked Microsoft user as inactive - no other subscriptions remain`); } else { this.logger.debug(`[${correlationId}] User still has ${otherActiveSubscriptions} other active subscription(s), keeping user active`); } this.logger.log(`[${correlationId}] Successfully deleted calendar subscription ${subscriptionId}`); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) { this.logger.log(`[${correlationId}] Subscription ${subscriptionId} not found at Microsoft, ` + `cleaning up local database`); await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); this.logger.log(`[${correlationId}] Successfully cleaned up orphaned subscription ${subscriptionId}`); return true; } this.logger.error(`[${correlationId}] Failed to delete subscription ${subscriptionId}: ${errorMessage}`); throw new Error(`Failed to delete webhook subscription: ${errorMessage}`); } } async getActiveSubscription(externalUserId) { var _a; try { const internalUserId = await this.userIdConverter.externalToInternal(externalUserId); this.logger.log(`[getActiveSubscription] Getting active subscription for user ${externalUserId} (internalUserId: ${internalUserId})`); const subscription = await this.webhookSubscriptionRepository.findActiveByUserId(internalUserId); this.logger.log(`[getActiveSubscription] Found subscription: ${subscription === null || subscription === void 0 ? void 0 : subscription.subscriptionId}`); return (_a = subscription === null || subscription === void 0 ? void 0 : subscription.subscriptionId) !== null && _a !== void 0 ? _a : null; } catch (_b) { this.logger.debug(`No active subscription for user ${externalUserId}`); return null; } } async renewSubscriptions() { try { const expiringSubscriptions = await this.webhookSubscriptionRepository.findSubscriptionsNeedingRenewal(24); if (expiringSubscriptions.length === 0) { this.logger.debug("No subscriptions need renewal"); return; } this.logger.log(`Found ${String(expiringSubscriptions.length)} subscriptions that need renewal`); for (const subscription of expiringSubscriptions) { try { await this.renewWebhookSubscription(subscription.subscriptionId, subscription.userId); } catch (error) { this.logger.error(`Failed to renew subscription ${subscription.subscriptionId}:`, error); } } } catch (error) { this.logger.error("Error in subscription renewal job:", error); } } async handleOutlookWebhook(notificationItem, useStreaming = false) { try { const { subscriptionId, clientState, resource, changeType } = notificationItem; this.logger.debug(`Received webhook notification for subscription: ${subscriptionId || "unknown"}`); this.logger.debug(`Resource: ${resource || "unknown"}, ChangeType: ${String(changeType || "unknown")}`); const { success, internalUserId, message } = await this.validateWebhookSubscription(subscriptionId, clientState); if (!success || !internalUserId) { this.logger.error('validateWebhookSubscription failed', message || 'Unknown error'); return { success: false, message: message || 'Unknown error' }; } const totalProcessed = useStreaming ? await this.processChangesStreaming(internalUserId, String(subscriptionId || ''), resource || '') : await this.processChangesBuffering(internalUserId, String(subscriptionId || ''), resource || ''); return { success: true, message: `Processed ${totalProcessed} events` }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Error processing webhook notification: ${errorMessage}`); return { success: false, message: errorMessage }; } } async fetchAndSortChanges(externalUserId, forceReset = false, dateRange) { const client = await this.getAuthenticatedClient(externalUserId); const requestUrl = "/me/events/delta"; try { this.logger.log(`[fetchAndSortChanges] Starting delta fetch for user ${externalUserId} (forceReset: ${forceReset})`); const items = await this.deltaSyncService.fetchAndSortChanges(client, requestUrl, externalUserId, forceReset, dateRange); this.logger.log(`[fetchAndSortChanges] ✅ Successfully fetched ${items.length} calendar changes for user ${externalUserId}`); return items; } catch (error) { const errorDetails = this.extractErrorDetails(error); this.logger.error(`[fetchAndSortChanges] ❌ Failed to fetch delta changes for user ${externalUserId}`, { userId: externalUserId, forceReset, errorType: errorDetails.type, statusCode: errorDetails.statusCode, errorCode: errorDetails.code, errorMessage: errorDetails.message, timestamp: new Date().toISOString(), }); this.logger.error("Error fetching delta changes:", error); throw error; } } extractErrorDetails(error) { if (!error || typeof error !== 'object') { return { type: 'unknown', statusCode: 'N/A', code: 'N/A', message: String(error), }; } if ('statusCode' in error) { const statusCode = error.statusCode; const code = 'code' in error ? String(error.code) : 'N/A'; const body = 'body' in error ? String(error.body) : 'N/A'; if (statusCode === -1) { return { type: 'network_error', statusCode: -1, code: code, message: 'Network connectivity failure - unable to reach Microsoft Graph API. Check internet connection, firewall, or proxy settings.', }; } return { type: 'graph_api_error', statusCode: statusCode, code: code, message: body, }; } if ('stack' in error && Array.isArray(error.stack) && error.stack.length > 0) { const firstError = error.stack[0]; if (firstError && typeof firstError === 'object') { const statusCode = 'statusCode' in firstError ? firstError.statusCode : 'N/A'; const code = 'code' in firstError ? String(firstError.code) : 'N/A'; const body = 'body' in firstError ? String(firstError.body) : 'N/A'; if (typeof statusCode === 'number' && statusCode === -1) { return { type: 'network_error', statusCode: -1, code: code, message: 'Network connectivity failure - unable to reach Microsoft Graph API', }; } return { type: 'graph_api_error', statusCode: statusCode, code: code, message: body, }; } } return { type: 'generic_error', statusCode: 'N/A', code: 'N/A', message: error instanceof Error ? error.message : JSON.stringify(error), }; } streamCalendarChanges(externalUserId_1) { return __asyncGenerator(this, arguments, function* streamCalendarChanges_1(externalUserId, forceReset = false, dateRange, saveDeltaLink = true) { const client = yield __await(this.getAuthenticatedClient(externalUserId)); const requestUrl = "/me/events/delta"; try { this.logger.log(`[streamCalendarChanges] Starting stream for user ${externalUserId} (saveDeltaLink: ${saveDeltaLink})`); const deltaLink = yield __await(yield* __asyncDelegator(__asyncValues(this.deltaSyncService.streamDeltaChanges(client, requestUrl, externalUserId, forceReset, dateRange, saveDeltaLink)))); this.logger.log(`[streamCalendarChanges] Completed streaming for user ${externalUserId}, deltaLink: ${deltaLink ? 'received' : 'none'}`); return yield __await(deltaLink); } catch (error) { this.logger.error(`[streamCalendarChanges] Error streaming delta changes:`, error instanceof Error ? error.message : String(error)); throw error; } }); } async getAuthenticatedClient(externalUserId) { const accessToken = await this.microsoftAuthService.getUserAccessToken({ externalUserId }); return microsoft_graph_client_1.Client.init({ authProvider: (done) => { done(null, accessToken); }, }); } async saveDeltaLink(externalUserId, deltaLink) { const internalUserId = await this.userIdConverter.externalToInternal(externalUserId); await this.deltaLinkRepository.saveDeltaLink(internalUserId, resource_type_enum_1.ResourceType.CALENDAR, deltaLink); this.logger.log(`[saveDeltaLink] Saved delta link for user ${externalUserId} (internal: ${internalUserId})`); } async getEventDetails(resource, externalUserId) { var _a; try { const accessToken = await this.microsoftAuthService.getUserAccessToken({ externalUserId }); const response = await axios_1.default.get(`https://graph.microsoft.com/v1.0/${resource}`, { headers: { Authorization: `Bearer ${accessToken}`, }, }); return response.data; } catch (error) { if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) { this.logger.debug(`Event not found (404) for resource ${resource}, likely deleted - returning null`); return null; } this.logger.error("Error fetching event details:", error); throw error; } } async getEventsBatch(eventIds, externalUserId) { return this.getEventsBatchInternal(eventIds, externalUserId, new Map()); } async getEventsBatchInternal(eventIds, externalUserId, retryCount, maxRetries = 2) { if (eventIds.length === 0) { return []; } if (eventIds.length > 20) { this.logger.warn(`[getEventsBatch] Called with ${eventIds.length} events, exceeding limit. Only first 20 will be fetched.`); } await this.rateLimiter.acquirePermit(externalUserId); try { const accessToken = await this.microsoftAuthService.getUserAccessToken({ externalUserId }); const batchPayload = { requests: eventIds.slice(0, 20).map((eventId, index) => ({ id: `${index}`, method: 'GET', url: `/me/events/${eventId}`, })), }; this.logger.debug(`[getEventsBatch] Fetching ${batchPayload.requests.length} events in batch for user ${externalUserId}`); const response = await (0, outlook_api_executor_util_1.executeGraphApiCall)(() => axios_1.default.post('https://graph.microsoft.com/v1.0/$batch', batchPayload, { headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, }), { maxRetries: 3, retryDelayMs: 1000, logger: this.logger, resourceName: `batch events (user: ${externalUserId})`, }); if (!response) { this.logger.error(`[getEventsBatch] Batch request returned null for user ${externalUserId}`); return []; } const events = []; const rateLimitedEventIds = []; let successCount = 0; let notFoundCount = 0; let errorCount = 0; for (const batchResponse of response.data.responses) { const eventId = eventIds[parseInt(batchResponse.id, 10)]; if (batchResponse.status === 200) { events.push(batchResponse.body); successCount++; } else if (batchResponse.status === 404) { this.logger.warn(`[getEventsBatch] Event ${eventId} not found (404), likely deleted`); notFoundCount++; } else if (batchResponse.status === 429) { rateLimitedEventIds.push(eventId); this.logger.warn(`[getEventsBatch] Event ${eventId} rate limited (429) within batch response`); errorCount++; } else { this.logger.error(`[getEventsBatch] Event ${eventId} failed: status ${batchResponse.status}, ` + `body: ${JSON.stringify(batchResponse.body)}`); errorCount++; } } this.logger.log(`[getEventsBatch] Batch complete for user ${externalUserId}: ` + `success=${successCount}, notFound=${notFoundCount}, errors=${errorCount}`); if (rateLimitedEventIds.length > 0) { const retryableEvents = rateLimitedEventIds.filter(id => { const count = retryCount.get(id) || 0; return count < maxRetries; }); if (retryableEvents.length > 0) { const retryAfter = this.extractRetryAfterFromResponse(response); if (retryAfter) { this.rateLimiter.handleRateLimitResponse(externalUserId, retryAfter); } retryableEvents.forEach(id => { retryCount.set(id, (retryCount.get(id) || 0) + 1); }); const maxAttempt = Math.max(...retryableEvents.map(id => retryCount.get(id) || 0)); this.logger.log(`[getEventsBatch] Retrying ${retryableEvents.length} rate-limited events ` + `(attempt ${maxAttempt}/${maxRetries})`); const retriedEvents = await this.getEventsBatchInternal(retryableEvents, externalUserId, retryCount, maxRetries); events.push(...retriedEvents); } else { this.logger.error(`[getEventsBatch] ${rateLimitedEventIds.length} events exceeded max retries - DATA LOSS WARNING`); } } return events; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error(`[getEventsBatch] B