widgettdc-api / apps /backend /src /services /ingestion /MicrosoftGraphAdapter.ts
Kraft102's picture
Update backend source
34367da verified
/**
* 🏢 Microsoft Graph Email & SharePoint Harvester
* Bruger Graph API til at søge i TDC Outlook og SharePoint
*/
import { logger } from '../../utils/logger.js';
export interface GraphSearchResult {
id: string;
title: string;
url: string;
summary: string;
type: 'email' | 'document' | 'site' | 'list';
from?: string;
received?: string;
modified?: string;
}
export class MicrosoftGraphAdapter {
private accessToken: string | null = null;
private graphBase = 'https://graph.microsoft.com/v1.0';
constructor(accessToken?: string) {
this.accessToken = accessToken || process.env.MS_GRAPH_TOKEN || null;
}
setAccessToken(token: string) {
this.accessToken = token;
logger.info('🔐 Microsoft Graph access token set');
}
async isAvailable(): Promise<boolean> {
if (!this.accessToken) return false;
try {
const response = await fetch(`${this.graphBase}/me`, {
headers: { 'Authorization': `Bearer ${this.accessToken}` }
});
return response.ok;
} catch {
return false;
}
}
async searchEmails(query: string, limit = 25): Promise<GraphSearchResult[]> {
if (!this.accessToken) {
logger.warn('⚠️ No Graph access token - use setAccessToken()');
return [];
}
try {
// Search messages
const response = await fetch(
`${this.graphBase}/me/messages?$search="${encodeURIComponent(query)}"&$top=${limit}&$select=id,subject,from,receivedDateTime,bodyPreview,webLink`,
{
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'ConsistencyLevel': 'eventual'
}
}
);
if (!response.ok) {
logger.error(`Graph email search failed: ${response.status}`);
return [];
}
const data = await response.json();
return (data.value || []).map((msg: any) => ({
id: msg.id,
title: msg.subject || '(No subject)',
url: msg.webLink || `https://outlook.office365.com/mail/deeplink/read/${msg.id}`,
summary: msg.bodyPreview?.slice(0, 300) || '',
type: 'email' as const,
from: msg.from?.emailAddress?.address || 'Unknown',
received: msg.receivedDateTime
}));
} catch (err) {
logger.error('Graph email search error:', err);
return [];
}
}
async searchSharePoint(query: string, limit = 25): Promise<GraphSearchResult[]> {
if (!this.accessToken) return [];
try {
const searchBody = {
requests: [{
entityTypes: ['driveItem', 'listItem', 'site'],
query: { queryString: query },
from: 0,
size: limit
}]
};
const response = await fetch(`${this.graphBase}/search/query`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(searchBody)
});
if (!response.ok) {
logger.error(`Graph SharePoint search failed: ${response.status}`);
return [];
}
const data = await response.json();
const results: GraphSearchResult[] = [];
for (const resultSet of data.value || []) {
for (const container of resultSet.hitsContainers || []) {
for (const hit of container.hits || []) {
const resource = hit.resource || {};
results.push({
id: resource.id || hit.hitId,
title: resource.name || resource.displayName || query,
url: resource.webUrl || '',
summary: hit.summary?.slice(0, 300) || '',
type: this.detectType(resource['@odata.type']),
modified: resource.lastModifiedDateTime
});
}
}
}
return results;
} catch (err) {
logger.error('Graph SharePoint search error:', err);
return [];
}
}
async searchAll(query: string, limit = 20): Promise<GraphSearchResult[]> {
const [emails, docs] = await Promise.all([
this.searchEmails(query, limit),
this.searchSharePoint(query, limit)
]);
return [...emails, ...docs];
}
async getSites(): Promise<{ id: string; name: string; url: string }[]> {
if (!this.accessToken) return [];
try {
const response = await fetch(
`${this.graphBase}/sites?search=*&$top=50`,
{ headers: { 'Authorization': `Bearer ${this.accessToken}` } }
);
if (!response.ok) return [];
const data = await response.json();
return (data.value || []).map((site: any) => ({
id: site.id,
name: site.displayName,
url: site.webUrl
}));
} catch {
return [];
}
}
private detectType(odataType: string): 'email' | 'document' | 'site' | 'list' {
if (!odataType) return 'document';
if (odataType.includes('site')) return 'site';
if (odataType.includes('listItem')) return 'list';
return 'document';
}
}
// Singleton instance
export const graphAdapter = new MicrosoftGraphAdapter();