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.

550 lines 30.2 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 __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 resource_type_enum_1 = require("../../enums/resource-type.enum"); const retry_util_1 = require("../../utils/retry.util"); 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) { this.microsoftAuthService = microsoftAuthService; this.webhookSubscriptionRepository = webhookSubscriptionRepository; this.eventEmitter = eventEmitter; this.microsoftConfig = microsoftConfig; this.deltaLinkRepository = deltaLinkRepository; this.microsoftUserRepository = microsoftUserRepository; this.deltaSyncService = deltaSyncService; this.logger = new common_1.Logger(CalendarService_1.name); this.syncLocks = new Map(); } async getDefaultCalendarId(externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(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) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(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}`); } } async deleteEvent(event, externalUserId, calendarId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); 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 client .api(`/me/calendars/${calendarId}/events/${event.id}`) .delete()); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to delete Outlook calendar event: ${errorMessage}`); throw new Error(`Failed to delete Outlook calendar event: ${errorMessage}`); } } async createWebhookSubscription(externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); 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 notificationUrl = `${basePathUrl}/calendar/webhook`; const subscriptionData = { changeType: "created,updated,deleted", notificationUrl, lifecycleNotificationUrl: notificationUrl, resource: "/me/events", expirationDateTime: expirationDateTime.toISOString(), clientState: `user_${externalUserId}_${Math.random().toString(36).substring(2, 15)}`, }; this.logger.debug(`Creating webhook subscription with notificationUrl: ${notificationUrl}`); this.logger.debug(`Subscription data: ${JSON.stringify(subscriptionData)}`); 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(`Created webhook subscription ${response.data.id || "unknown"} for user ${externalUserId}`); const internalUserId = parseInt(externalUserId, 10); 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.debug(`Stored subscription`); return response.data; } catch (error) { this.logger.error("Failed to create webhook subscription:", error); throw new Error("Failed to create webhook subscription"); } } async renewWebhookSubscription(subscriptionId, externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); const expirationDateTime = new Date(); expirationDateTime.setHours(expirationDateTime.getHours() + 72); const renewalData = { expirationDateTime: expirationDateTime.toISOString(), }; const response = await axios_1.default.patch(`https://graph.microsoft.com/v1.0/subscriptions/${subscriptionId}`, renewalData, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); if (response.data.expirationDateTime) { await this.webhookSubscriptionRepository.updateSubscriptionExpiration(subscriptionId, new Date(response.data.expirationDateTime)); } this.logger.log(`Renewed webhook subscription: ${subscriptionId}`); return response.data; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to renew webhook subscription: ${errorMessage}`); throw new Error(`Failed to renew webhook subscription: ${errorMessage}`); } } async renewWebhookSubscriptionByUserId(subscriptionId, internalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByUserId(internalUserId); const expirationDateTime = new Date(); expirationDateTime.setHours(expirationDateTime.getHours() + 72); const renewalData = { expirationDateTime: expirationDateTime.toISOString(), }; const response = await axios_1.default.patch(`https://graph.microsoft.com/v1.0/subscriptions/${subscriptionId}`, renewalData, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); if (response.data.expirationDateTime) { await this.webhookSubscriptionRepository.updateSubscriptionExpiration(subscriptionId, new Date(response.data.expirationDateTime)); } this.logger.log(`Renewed webhook subscription: ${subscriptionId}`); return response.data; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to renew webhook subscription: ${errorMessage}`); throw new Error(`Failed to renew webhook subscription: ${errorMessage}`); } } async deleteWebhookSubscription(subscriptionId, externalUserId) { var _a; try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); await axios_1.default.delete(`https://graph.microsoft.com/v1.0/subscriptions/${subscriptionId}`, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); await this.microsoftUserRepository.update({ externalUserId }, { isActive: false }); this.logger.log(`Deleted webhook subscription: ${subscriptionId}`); return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Failed to delete webhook subscription: ${errorMessage}`); if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 404) { await this.webhookSubscriptionRepository.deactivateSubscription(subscriptionId); this.logger.log(`Subscription not found, removed from database: ${subscriptionId}`); return true; } throw new Error(`Failed to delete webhook subscription: ${errorMessage}`); } } 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.renewWebhookSubscriptionByUserId(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, externalUserId, message } = await this.validateWebhookSubscription(subscriptionId, clientState); if (!success || !externalUserId) { this.logger.error('validateWebhookSubscription failed', message || 'Unknown error'); return { success: false, message: message || 'Unknown error' }; } const totalProcessed = useStreaming ? await this.processChangesStreaming(String(externalUserId), String(subscriptionId || ''), resource || '') : await this.processChangesBuffering(String(externalUserId), 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 { const items = await this.deltaSyncService.fetchAndSortChanges(client, requestUrl, externalUserId, forceReset, dateRange); return items; } catch (error) { this.logger.error("Error fetching delta changes:", error); throw error; } } streamCalendarChanges(externalUserId_1) { return __asyncGenerator(this, arguments, function* streamCalendarChanges_1(externalUserId, forceReset = false, dateRange, saveDeltaLink = true) { var _a, e_1, _b, _c; const client = yield __await(this.getAuthenticatedClient(externalUserId)); const requestUrl = "/me/events/delta"; try { this.logger.log(`[streamCalendarChanges] Starting stream for user ${externalUserId} (saveDeltaLink: ${saveDeltaLink})`); try { for (var _d = true, _e = __asyncValues(this.deltaSyncService.streamDeltaChanges(client, requestUrl, externalUserId, forceReset, dateRange, saveDeltaLink)), _f; _f = yield __await(_e.next()), _a = _f.done, !_a; _d = true) { _c = _f.value; _d = false; const batch = _c; this.logger.debug(`[streamCalendarChanges] Yielding batch of ${batch.length} events for user ${externalUserId}`); yield yield __await(batch); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (!_d && !_a && (_b = _e.return)) yield __await(_b.call(_e)); } finally { if (e_1) throw e_1.error; } } this.logger.log(`[streamCalendarChanges] Completed streaming for user ${externalUserId}`); } catch (error) { this.logger.error(`[streamCalendarChanges] Error streaming delta changes:`, error); throw error; } }); } async getAuthenticatedClient(externalUserId) { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); return microsoft_graph_client_1.Client.init({ authProvider: (done) => { done(null, accessToken); }, }); } async getEventDetails(resource, externalUserId) { try { const accessToken = await this.microsoftAuthService.getUserAccessTokenByExternalUserId(externalUserId); const response = await axios_1.default.get(`https://graph.microsoft.com/v1.0/${resource}`, { headers: { Authorization: `Bearer ${accessToken}`, }, }); return response.data; } catch (error) { this.logger.error("Error fetching event details:", error); throw error; } } importEventsStream(externalUserId, options) { return __asyncGenerator(this, arguments, function* importEventsStream_1() { var _a; const batchSize = (_a = options === null || options === void 0 ? void 0 : options.batchSize) !== null && _a !== void 0 ? _a : 100; try { this.logger.log(`Starting event stream for user ${externalUserId} (batchSize: ${batchSize})`); const client = yield __await(this.getAuthenticatedClient(externalUserId)); const requestUrl = this.buildRequestUrl(options, batchSize); let nextLink = requestUrl; const buffer = []; let totalFetched = 0; while (nextLink) { this.logger.debug(`Fetching page: ${nextLink}`); const response = (yield __await((0, retry_util_1.retryWithBackoff)(() => client.api(nextLink).get()))); const items = response.value; buffer.push(...items); totalFetched += items.length; while (buffer.length >= batchSize) { const chunk = buffer.splice(0, batchSize); this.logger.debug(`Yielding chunk of ${chunk.length} items (total fetched: ${totalFetched})`); yield yield __await(chunk); } nextLink = response["@odata.nextLink"]; if (nextLink) { yield __await((0, retry_util_1.delay)(200)); } } if (buffer.length > 0) { this.logger.debug(`Yielding final chunk of ${buffer.length} items`); yield yield __await(buffer); } this.logger.log(`Completed streaming ${totalFetched} events for user ${externalUserId}`); this.eventEmitter.emit(event_types_enum_1.OutlookEventTypes.IMPORT_COMPLETED, { userId: externalUserId, totalEvents: totalFetched, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.logger.error(`Error streaming events for user ${externalUserId}: ${errorMessage}`); throw error; } }); } async initializeDeltaSync(externalUserId) { this.logger.log(`Initializing delta sync tracking for user ${externalUserId}`); try { const client = await this.getAuthenticatedClient(externalUserId); await this.deltaSyncService.initializeDeltaLink(client, "/me/events/delta", Number(externalUserId), resource_type_enum_1.ResourceType.CALENDAR); this.logger.log(`Delta tracking enabled for user ${externalUserId} (all events)`); } catch (error) { this.logger.error(`Failed to initialize delta sync for user ${externalUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } async processChangesStreaming(externalUserId, subscriptionId, resource) { var _a, e_2, _b, _c; let totalProcessed = 0; let batchCount = 0; this.logger.log(`[processChangesStreaming] Using STREAMING mode for user ${externalUserId}`); try { for (var _d = true, _e = __asyncValues(this.streamCalendarChanges(externalUserId)), _f; _f = await _e.next(), _a = _f.done, !_a; _d = true) { _c = _f.value; _d = false; const changeBatch = _c; batchCount++; this.logger.log(`[processChangesStreaming] Processing batch ${batchCount} with ${changeBatch.length} changes`); if (changeBatch.length === 0) { this.logger.warn(`[processChangesStreaming] Received empty batch ${batchCount}`); continue; } for (const change of changeBatch) { this.processDeltaEventChange(change, externalUserId, subscriptionId, resource); totalProcessed++; } this.logger.log(`[processChangesStreaming] Batch ${batchCount} processed: ${changeBatch.length} events (total: ${totalProcessed})`); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (!_d && !_a && (_b = _e.return)) await _b.call(_e); } finally { if (e_2) throw e_2.error; } } this.logger.log(`[processChangesStreaming] Completed: ${totalProcessed} events across ${batchCount} batches`); return totalProcessed; } async processChangesBuffering(externalUserId, subscriptionId, resource) { this.logger.log(`[processChangesBuffering] Using BUFFERING mode for user ${externalUserId}`); const allChanges = await this.fetchAndSortChanges(externalUserId); if (allChanges.length === 0) { this.logger.warn(`[processChangesBuffering] No changes found`); return 0; } this.logger.log(`[processChangesBuffering] Fetched ${allChanges.length} changes, processing batch`); let totalProcessed = 0; for (const change of allChanges) { this.processDeltaEventChange(change, externalUserId, subscriptionId, resource); totalProcessed++; } this.logger.log(`[processChangesBuffering] Completed: ${totalProcessed} events processed`); return totalProcessed; } processDeltaEventChange(change, externalUserId, subscriptionId, resource) { const eventType = detectEventType(change); this.logger.debug(`[processDeltaEventChange] Event ${change.id || "unknown"}: created=${change.createdDateTime}, modified=${change.lastModifiedDateTime}, type=${eventType}`); const resourceData = { id: change.id || "", userId: Number(externalUserId), subscriptionId, resource, changeType: EVENT_TYPE_TO_CHANGE_TYPE[eventType], data: change, }; this.eventEmitter.emit(eventType, resourceData); this.logger.log(`[processDeltaEventChange] Emitted ${eventType} for event ID: ${change.id || "unknown"}`); } async validateWebhookSubscription(subscriptionId, clientState) { const subscription = await this.webhookSubscriptionRepository.findBySubscriptionId(subscriptionId || ""); if (!subscription) { this.logger.warn(`Unknown subscription ID: ${subscriptionId || "unknown"}`); return { success: false, message: "Unknown subscription" }; } if (subscription.clientState && clientState !== subscription.clientState) { this.logger.warn("Client state mismatch"); return { success: false, message: "Client state mismatch" }; } const externalUserId = subscription.userId; if (!externalUserId) { this.logger.warn("Could not determine external user ID from client state"); return { success: false, message: "Invalid client state format" }; } return { success: true, externalUserId }; } buildRequestUrl(options, batchSize) { var _a, _b; const dateinterval = 5 * 365 * 24 * 60 * 60 * 1000; const defaultStartDate = new Date(); const defaultEndDate = new Date(Date.now() + dateinterval); const startDate = (_a = options === null || options === void 0 ? void 0 : options.startDate) !== null && _a !== void 0 ? _a : defaultStartDate; const endDate = (_b = options === null || options === void 0 ? void 0 : options.endDate) !== null && _b !== void 0 ? _b : defaultEndDate; const startDateStr = startDate.toISOString(); const endDateStr = endDate.toISOString(); let url = `/me/calendarView?startDateTime=${startDateStr}&endDateTime=${endDateStr}`; url += `&$orderby=start/dateTime&$top=${batchSize !== null && batchSize !== void 0 ? batchSize : 100}`; return url; } }; exports.CalendarService = CalendarService; __decorate([ (0, schedule_1.Cron)(schedule_1.CronExpression.EVERY_HOUR), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], CalendarService.prototype, "renewSubscriptions", null); exports.CalendarService = CalendarService = CalendarService_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, common_1.Inject)((0, common_1.forwardRef)(() => microsoft_auth_service_1.MicrosoftAuthService))), __param(3, (0, common_1.Inject)(constants_1.MICROSOFT_CONFIG)), __param(5, (0, typeorm_1.InjectRepository)(microsoft_user_entity_1.MicrosoftUser)), __metadata("design:paramtypes", [microsoft_auth_service_1.MicrosoftAuthService, outlook_webhook_subscription_repository_1.OutlookWebhookSubscriptionRepository, event_emitter_1.EventEmitter2, Object, outlook_delta_link_repository_1.OutlookDeltaLinkRepository, typeorm_2.Repository, delta_sync_service_1.DeltaSyncService]) ], CalendarService); //# sourceMappingURL=calendar.service.js.map