@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
JavaScript
"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