@vibesdotdev/integrations-microsoft 0.0.0-stage → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +126 -0
- package/README.md +120 -2
- package/dist/api-client-plugin.d.ts +3 -0
- package/dist/api-client-plugin.d.ts.map +1 -0
- package/dist/api-client-plugin.js +11 -0
- package/dist/api-client-plugin.js.map +1 -0
- package/dist/client/helper.d.ts +17 -0
- package/dist/client/helper.d.ts.map +1 -0
- package/dist/client/helper.js +18 -0
- package/dist/client/helper.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/runtime-client.d.ts +87 -0
- package/dist/client/runtime-client.d.ts.map +1 -0
- package/dist/client/runtime-client.js +47 -0
- package/dist/client/runtime-client.js.map +1 -0
- package/dist/cloud.plugin.d.ts +3 -0
- package/dist/cloud.plugin.d.ts.map +1 -0
- package/dist/cloud.plugin.js +19 -0
- package/dist/cloud.plugin.js.map +1 -0
- package/dist/email/index.d.ts +13 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +13 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/microsoft/index.d.ts +4 -0
- package/dist/email/microsoft/index.d.ts.map +1 -0
- package/dist/email/microsoft/index.js +4 -0
- package/dist/email/microsoft/index.js.map +1 -0
- package/dist/email/microsoft/microsoft.descriptor.d.ts +85 -0
- package/dist/email/microsoft/microsoft.descriptor.d.ts.map +1 -0
- package/dist/email/microsoft/microsoft.descriptor.js +79 -0
- package/dist/email/microsoft/microsoft.descriptor.js.map +1 -0
- package/dist/email/microsoft/microsoft.impl.d.ts +100 -0
- package/dist/email/microsoft/microsoft.impl.d.ts.map +1 -0
- package/dist/email/microsoft/microsoft.impl.js +323 -0
- package/dist/email/microsoft/microsoft.impl.js.map +1 -0
- package/dist/email/microsoft/outlook.mime.d.ts +18 -0
- package/dist/email/microsoft/outlook.mime.d.ts.map +1 -0
- package/dist/email/microsoft/outlook.mime.js +61 -0
- package/dist/email/microsoft/outlook.mime.js.map +1 -0
- package/dist/email/microsoft-email.plugin.d.ts +10 -0
- package/dist/email/microsoft-email.plugin.d.ts.map +1 -0
- package/dist/email/microsoft-email.plugin.js +29 -0
- package/dist/email/microsoft-email.plugin.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations-microsoft.plugin.d.ts +4 -0
- package/dist/integrations-microsoft.plugin.d.ts.map +1 -0
- package/dist/integrations-microsoft.plugin.js +15 -0
- package/dist/integrations-microsoft.plugin.js.map +1 -0
- package/dist/microsoft/graph-drive-fallback.d.ts +11 -0
- package/dist/microsoft/graph-drive-fallback.d.ts.map +1 -0
- package/dist/microsoft/graph-drive-fallback.js +90 -0
- package/dist/microsoft/graph-drive-fallback.js.map +1 -0
- package/dist/microsoft/graph-schemas.d.ts +2 -0
- package/dist/microsoft/graph-schemas.d.ts.map +1 -0
- package/dist/microsoft/graph-schemas.js +2 -0
- package/dist/microsoft/graph-schemas.js.map +1 -0
- package/dist/microsoft/graph-sharepoint-client.d.ts +219 -0
- package/dist/microsoft/graph-sharepoint-client.d.ts.map +1 -0
- package/dist/microsoft/graph-sharepoint-client.js +644 -0
- package/dist/microsoft/graph-sharepoint-client.js.map +1 -0
- package/dist/microsoft/index.d.ts +8 -0
- package/dist/microsoft/index.d.ts.map +1 -0
- package/dist/microsoft/index.js +8 -0
- package/dist/microsoft/index.js.map +1 -0
- package/dist/microsoft/microsoft-delegated-implementation.d.ts +20 -0
- package/dist/microsoft/microsoft-delegated-implementation.d.ts.map +1 -0
- package/dist/microsoft/microsoft-delegated-implementation.js +69 -0
- package/dist/microsoft/microsoft-delegated-implementation.js.map +1 -0
- package/dist/microsoft/microsoft-implementation.d.ts +22 -0
- package/dist/microsoft/microsoft-implementation.d.ts.map +1 -0
- package/dist/microsoft/microsoft-implementation.js +78 -0
- package/dist/microsoft/microsoft-implementation.js.map +1 -0
- package/dist/microsoft/secrets-fetch.d.ts +20 -0
- package/dist/microsoft/secrets-fetch.d.ts.map +1 -0
- package/dist/microsoft/secrets-fetch.js +47 -0
- package/dist/microsoft/secrets-fetch.js.map +1 -0
- package/dist/microsoft/sharepoint-operations.d.ts +23 -0
- package/dist/microsoft/sharepoint-operations.d.ts.map +1 -0
- package/dist/microsoft/sharepoint-operations.js +123 -0
- package/dist/microsoft/sharepoint-operations.js.map +1 -0
- package/dist/microsoft/system.integration.microsoft.delegated.descriptor.d.ts +85 -0
- package/dist/microsoft/system.integration.microsoft.delegated.descriptor.d.ts.map +1 -0
- package/dist/microsoft/system.integration.microsoft.delegated.descriptor.js +109 -0
- package/dist/microsoft/system.integration.microsoft.delegated.descriptor.js.map +1 -0
- package/dist/microsoft/system.integration.microsoft.descriptor.d.ts +69 -0
- package/dist/microsoft/system.integration.microsoft.descriptor.d.ts.map +1 -0
- package/dist/microsoft/system.integration.microsoft.descriptor.js +114 -0
- package/dist/microsoft/system.integration.microsoft.descriptor.js.map +1 -0
- package/dist/microsoft.consumer.plugin.d.ts +4 -0
- package/dist/microsoft.consumer.plugin.d.ts.map +1 -0
- package/dist/microsoft.consumer.plugin.js +13 -0
- package/dist/microsoft.consumer.plugin.js.map +1 -0
- package/dist/schemas/boundary.d.ts +69 -0
- package/dist/schemas/boundary.d.ts.map +1 -0
- package/dist/schemas/boundary.js +94 -0
- package/dist/schemas/boundary.js.map +1 -0
- package/dist/schemas/graph.d.ts +139 -0
- package/dist/schemas/graph.d.ts.map +1 -0
- package/dist/schemas/graph.js +161 -0
- package/dist/schemas/graph.js.map +1 -0
- package/dist/schemas/oauth.d.ts +131 -0
- package/dist/schemas/oauth.d.ts.map +1 -0
- package/dist/schemas/oauth.js +27 -0
- package/dist/schemas/oauth.js.map +1 -0
- package/dist/schemas/operations.d.ts +239 -0
- package/dist/schemas/operations.d.ts.map +1 -0
- package/dist/schemas/operations.js +29 -0
- package/dist/schemas/operations.js.map +1 -0
- package/dist/schemas/outlook.d.ts +241 -0
- package/dist/schemas/outlook.d.ts.map +1 -0
- package/dist/schemas/outlook.js +29 -0
- package/dist/schemas/outlook.js.map +1 -0
- package/dist/schemas/sharepoint.d.ts +67 -0
- package/dist/schemas/sharepoint.d.ts.map +1 -0
- package/dist/schemas/sharepoint.js +71 -0
- package/dist/schemas/sharepoint.js.map +1 -0
- package/dist/server/oauth.d.ts +9 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +67 -0
- package/dist/server/oauth.js.map +1 -0
- package/dist/sharepoint/index.d.ts +11 -0
- package/dist/sharepoint/index.d.ts.map +1 -0
- package/dist/sharepoint/index.js +11 -0
- package/dist/sharepoint/index.js.map +1 -0
- package/dist/sharepoint/sharepoint.adapter.descriptor.d.ts +10 -0
- package/dist/sharepoint/sharepoint.adapter.descriptor.d.ts.map +1 -0
- package/dist/sharepoint/sharepoint.adapter.descriptor.js +32 -0
- package/dist/sharepoint/sharepoint.adapter.descriptor.js.map +1 -0
- package/dist/sharepoint/sharepoint.adapter.impl.d.ts +195 -0
- package/dist/sharepoint/sharepoint.adapter.impl.d.ts.map +1 -0
- package/dist/sharepoint/sharepoint.adapter.impl.js +645 -0
- package/dist/sharepoint/sharepoint.adapter.impl.js.map +1 -0
- package/dist/sharepoint/sharepoint.schemas.d.ts +2 -0
- package/dist/sharepoint/sharepoint.schemas.d.ts.map +1 -0
- package/dist/sharepoint/sharepoint.schemas.js +2 -0
- package/dist/sharepoint/sharepoint.schemas.js.map +1 -0
- package/package.json +146 -4
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import { GraphJsonSchema, GraphJsonCollectionSchema, GraphDownloadSchema, GraphUploadSessionSchema } from "../schemas/graph.js";
|
|
2
|
+
import { settle, settleNow } from '@vibesdotdev/kernel';
|
|
3
|
+
import * as z from 'zod/v4';
|
|
4
|
+
import { CapabilityError } from '@vibesdotdev/kernel';
|
|
5
|
+
import { admitMicrosoft, MicrosoftGraphError, graphFailure, microsoftFailure, closeMicrosoftResponse } from "../schemas/boundary.js";
|
|
6
|
+
import { GraphSiteSchema, SharePointSiteSchema, GraphDriveSchema, GraphDriveItemSchema, DriveItemEntrySchema, DriveItemSearchResultSchema } from "./graph-schemas.js";
|
|
7
|
+
const GRAPH_API_BASE = 'https://graph.microsoft.com/v1.0';
|
|
8
|
+
/**
|
|
9
|
+
* Sentinel "site id" that targets the signed-in user's personal OneDrive
|
|
10
|
+
* (`/me/drive`) instead of a SharePoint site. Delegated only — app-only tokens
|
|
11
|
+
* have no `/me`. Used as a drive id by the explorer for the "OneDrive" entry.
|
|
12
|
+
*/
|
|
13
|
+
export const ONEDRIVE_SITE_ID = 'me';
|
|
14
|
+
/** Graph recommends a resumable upload session above ~4 MiB. */
|
|
15
|
+
const SIMPLE_UPLOAD_MAX = 4 * 1024 * 1024;
|
|
16
|
+
/** Upload-session chunk size: 5 MiB, a multiple of 320 KiB (a Graph requirement). */
|
|
17
|
+
const UPLOAD_CHUNK = 327680 * 16;
|
|
18
|
+
const GRAPH_GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19
|
+
/** Percent-encode each path segment while preserving `/` separators. */
|
|
20
|
+
function encodeDrivePath(path) {
|
|
21
|
+
return path
|
|
22
|
+
.split('/')
|
|
23
|
+
.filter((seg) => seg.length > 0)
|
|
24
|
+
.map(encodeURIComponent)
|
|
25
|
+
.join('/');
|
|
26
|
+
}
|
|
27
|
+
/** Normalize an upload body to bytes (buffering a stream when needed). */
|
|
28
|
+
async function toUint8Array(body) {
|
|
29
|
+
if (body instanceof ArrayBuffer)
|
|
30
|
+
return new Uint8Array(body);
|
|
31
|
+
if (body instanceof Blob) {
|
|
32
|
+
return new Uint8Array(await body.arrayBuffer());
|
|
33
|
+
}
|
|
34
|
+
const reader = body.getReader();
|
|
35
|
+
const chunks = [];
|
|
36
|
+
let total = 0;
|
|
37
|
+
for (;;) {
|
|
38
|
+
const { done, value } = await reader.read();
|
|
39
|
+
if (done)
|
|
40
|
+
break;
|
|
41
|
+
if (value) {
|
|
42
|
+
chunks.push(value);
|
|
43
|
+
total += value.byteLength;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const out = new Uint8Array(total);
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (const c of chunks) {
|
|
49
|
+
out.set(c, offset);
|
|
50
|
+
offset += c.byteLength;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function looksLikeGraphDriveId(id) {
|
|
55
|
+
return GRAPH_GUID_RE.test(id) || id.startsWith('b!') || id.startsWith('u!');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Microsoft Graph API client for SharePoint operations.
|
|
59
|
+
*
|
|
60
|
+
* Provides listSites(), browseDrive(), and downloadFile() methods.
|
|
61
|
+
* Uses the runtime-supplied authenticated transport for registered
|
|
62
|
+
* providers. App-only access requires the Sites.Read.All (read) /
|
|
63
|
+
* Sites.ReadWrite.All (write) + Files.Read.All/Files.ReadWrite.All APPLICATION
|
|
64
|
+
* permissions, admin-consented into the org's tenant — Sites.Selected does NOT
|
|
65
|
+
* satisfy the `GET /sites?search=*` site enumeration listSites() relies on.
|
|
66
|
+
*/
|
|
67
|
+
export class GraphSharePointClient {
|
|
68
|
+
integration;
|
|
69
|
+
authenticatedFetch;
|
|
70
|
+
fetchFn;
|
|
71
|
+
constructor(config) {
|
|
72
|
+
if (!config.integration && !config.authenticatedFetch) {
|
|
73
|
+
throw new CapabilityError('Microsoft Graph requires an authenticated transport.', 'UNAVAILABLE', { reason: 'microsoft-graph-auth-transport-required' });
|
|
74
|
+
}
|
|
75
|
+
this.integration = config.integration;
|
|
76
|
+
this.authenticatedFetch = config.authenticatedFetch;
|
|
77
|
+
// Bind the default global fetch to globalThis: it's invoked as `this.fetchFn(...)`,
|
|
78
|
+
// which would otherwise call native fetch with `this` = this client instance →
|
|
79
|
+
// workerd throws "Illegal invocation". An injected fetchFn is used as-is; tests
|
|
80
|
+
// and pre-authenticated upload-session requests retain ownership of that boundary.
|
|
81
|
+
this.fetchFn = config.fetchFn ?? fetch.bind(globalThis);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build a download URL operation result without streaming binary
|
|
85
|
+
* content through the dispatch path. Calls
|
|
86
|
+
* `/sites/{site-id}/drive/items/{item-id}` with `?$select=id,@microsoft.graph.downloadUrl`
|
|
87
|
+
* and returns the temporary URL Graph mints (~1h validity). Consumers
|
|
88
|
+
* fetch bytes directly; this keeps the JSON-only operations dispatch
|
|
89
|
+
* binary-free.
|
|
90
|
+
*/
|
|
91
|
+
async getDownloadUrl(siteId, itemId) {
|
|
92
|
+
// Do NOT use $select here: `@microsoft.graph.downloadUrl` is a default-returned
|
|
93
|
+
// instance annotation that Graph OMITS under $select (the same trap documented
|
|
94
|
+
// on getDownloadUrlByPath below). $select silently dropped the URL, so every
|
|
95
|
+
// item-id download — and thus every drawing OCR byte-fetch — 500'd with
|
|
96
|
+
// "Graph did not return @microsoft.graph.downloadUrl". Fetch the full item.
|
|
97
|
+
const url = `${this.driveRoot(siteId)}/items/${itemId}`;
|
|
98
|
+
const response = await this.executeRequest(url, GraphDownloadSchema);
|
|
99
|
+
const downloadUrl = response['@microsoft.graph.downloadUrl'];
|
|
100
|
+
if (!downloadUrl) {
|
|
101
|
+
throw new Error(`Graph did not return @microsoft.graph.downloadUrl for site '${siteId}' item '${itemId}'.`);
|
|
102
|
+
}
|
|
103
|
+
// Graph's downloadUrl is a pre-signed Azure Blob SAS URL; expiry is
|
|
104
|
+
// embedded in the SAS but not surfaced as an ISO string. Return null
|
|
105
|
+
// for now and let callers refresh on 403/expired errors.
|
|
106
|
+
return { url: downloadUrl, expiresAt: null };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Pre-authenticated download URL for a drive item addressed by its PATH (vs
|
|
110
|
+
* item id). Returns Graph's `@microsoft.graph.downloadUrl` — a short-lived
|
|
111
|
+
* Azure Blob SAS URL that needs no auth header, so it works directly in a
|
|
112
|
+
* browser `<img>`/`<iframe>`/download across origins (the object store has no
|
|
113
|
+
* stable anonymous URL otherwise). `null` if the item is missing.
|
|
114
|
+
*/
|
|
115
|
+
async getDownloadUrlByPath(siteId, path) {
|
|
116
|
+
// GET the full driveItem (no $select): `@microsoft.graph.downloadUrl` is a
|
|
117
|
+
// default-returned instance annotation for files but is unreliable under
|
|
118
|
+
// $select (the `@`-annotation is often omitted), which silently dropped the
|
|
119
|
+
// URL and forced the proxy fallback.
|
|
120
|
+
const url = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}`;
|
|
121
|
+
const attempt = await settle(() => this.executeRequest(url, GraphDownloadSchema));
|
|
122
|
+
if (attempt.ok)
|
|
123
|
+
return attempt.value['@microsoft.graph.downloadUrl'] ?? null;
|
|
124
|
+
if (attempt.cause instanceof MicrosoftGraphError && attempt.cause.status === 404)
|
|
125
|
+
return null;
|
|
126
|
+
throw attempt.cause;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* List SharePoint sites the app has access to.
|
|
130
|
+
*
|
|
131
|
+
* Returns normalized site objects with id, name, and webUrl.
|
|
132
|
+
* App-only requires the Sites.Read.All application permission so the
|
|
133
|
+
* `GET /sites?search=*` enumeration returns the tenant's sites.
|
|
134
|
+
*
|
|
135
|
+
* GET /sites?search=*&$select=id,name,webUrl,displayName
|
|
136
|
+
*
|
|
137
|
+
* @returns Array of SharePoint sites
|
|
138
|
+
*/
|
|
139
|
+
async listSites() {
|
|
140
|
+
// `search=*` lists all sites the caller can reach — required for app-only
|
|
141
|
+
// (client-credentials) tokens, which otherwise get no sites from a bare
|
|
142
|
+
// GET /sites; harmless for delegated (returns the user's accessible sites).
|
|
143
|
+
const url = `${GRAPH_API_BASE}/sites?search=*&$select=id,name,webUrl,displayName`;
|
|
144
|
+
const response = await this.executeRequest(url, GraphJsonCollectionSchema);
|
|
145
|
+
const allItems = await this.fetchAllPages(response.value, response['@odata.nextLink']);
|
|
146
|
+
return allItems
|
|
147
|
+
.map((item) => {
|
|
148
|
+
const parsed = GraphSiteSchema.safeParse(item);
|
|
149
|
+
if (!parsed.success)
|
|
150
|
+
throw new CapabilityError('Microsoft Graph returned a malformed resource.', 'UNAVAILABLE', { reason: 'microsoft-graph-response-invalid', cause: parsed.error });
|
|
151
|
+
const site = parsed.data;
|
|
152
|
+
return SharePointSiteSchema.parse({
|
|
153
|
+
id: site.id,
|
|
154
|
+
name: site.displayName || site.name,
|
|
155
|
+
webUrl: site.webUrl
|
|
156
|
+
});
|
|
157
|
+
})
|
|
158
|
+
.filter((site) => site !== null);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Return a site's default document library as a real Graph drive.
|
|
162
|
+
*
|
|
163
|
+
* Storage consumers pass a `driveId`, not a site id. SharePoint site ids are
|
|
164
|
+
* often composite strings (`host,siteCollectionId,webId`) that are brittle in
|
|
165
|
+
* agent/tool contexts, while Graph drive ids are the stable primitive accepted
|
|
166
|
+
* by `/drives/{id}` across browse/search/download operations.
|
|
167
|
+
*/
|
|
168
|
+
async getDefaultDrive(siteId) {
|
|
169
|
+
const raw = await this.executeRequest(`${GRAPH_API_BASE}/sites/${siteId}/drive?$select=id,name,webUrl,driveType`, GraphDriveSchema);
|
|
170
|
+
const parsed = GraphDriveSchema.safeParse(raw);
|
|
171
|
+
if (!parsed.success) {
|
|
172
|
+
throw new Error(`Graph default drive for site '${siteId}' was not a valid drive.`);
|
|
173
|
+
}
|
|
174
|
+
return parsed.data;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Browse folder contents within a SharePoint site's default drive.
|
|
178
|
+
*
|
|
179
|
+
* GET /sites/{site-id}/drive/root/children
|
|
180
|
+
*
|
|
181
|
+
* When folderPath is provided:
|
|
182
|
+
* GET /sites/{site-id}/drive/root:/{folderPath}:/children
|
|
183
|
+
*
|
|
184
|
+
* @param siteId - SharePoint site ID
|
|
185
|
+
* @param folderPath - Optional path relative to drive root (e.g. "Documents/Reports")
|
|
186
|
+
* @returns Array of drive item entries (files and folders)
|
|
187
|
+
*/
|
|
188
|
+
async browseDrive(siteId, folderPath) {
|
|
189
|
+
let url;
|
|
190
|
+
if (folderPath && folderPath.length > 0) {
|
|
191
|
+
// Encode per path segment (preserve literal `/` separators) like every
|
|
192
|
+
// other path method here — a flat encodeURIComponent flattens nested
|
|
193
|
+
// folder paths and breaks browseDrive for subfolders.
|
|
194
|
+
const encoded = encodeDrivePath(folderPath);
|
|
195
|
+
url = `${this.driveRoot(siteId)}/root:/${encoded}:/children`;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
url = `${this.driveRoot(siteId)}/root/children`;
|
|
199
|
+
}
|
|
200
|
+
const attempt = await settle(() => this.executeRequest(url, GraphJsonCollectionSchema));
|
|
201
|
+
if (!attempt.ok) {
|
|
202
|
+
const failure = attempt.cause;
|
|
203
|
+
if (siteId === ONEDRIVE_SITE_ID && !folderPath && failure instanceof MicrosoftGraphError && failure.status === 404)
|
|
204
|
+
return [];
|
|
205
|
+
throw failure;
|
|
206
|
+
}
|
|
207
|
+
const response = attempt.value;
|
|
208
|
+
const allItems = await this.fetchAllPages(response.value, response['@odata.nextLink']);
|
|
209
|
+
return allItems
|
|
210
|
+
.map((item) => this.normalizeDriveItem(item))
|
|
211
|
+
.filter((entry) => entry !== null);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Browse folder contents by stable DriveItem id.
|
|
215
|
+
*
|
|
216
|
+
* Use this when a caller already has an item id from search/list, or when
|
|
217
|
+
* Graph path addressing rejects an otherwise valid folder path.
|
|
218
|
+
*/
|
|
219
|
+
async browseDriveItem(siteId, itemId) {
|
|
220
|
+
const response = await this.executeRequest(`${this.driveRoot(siteId)}/items/${itemId}/children`, GraphJsonCollectionSchema);
|
|
221
|
+
const allItems = await this.fetchAllPages(response.value, response['@odata.nextLink']);
|
|
222
|
+
return allItems
|
|
223
|
+
.map((item) => this.normalizeDriveItem(item))
|
|
224
|
+
.filter((entry) => entry !== null);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* RECURSIVELY search a site/OneDrive drive for items matching `query`,
|
|
228
|
+
* at ANY depth, in a single Graph call.
|
|
229
|
+
*
|
|
230
|
+
* Uses Graph's server-side drive search:
|
|
231
|
+
* GET /sites/{site-id}/drive/root/search(q='{query}')
|
|
232
|
+
* GET /me/drive/root/search(q='{query}') (OneDrive sentinel)
|
|
233
|
+
*
|
|
234
|
+
* Graph indexes the whole drive server-side and matches on file/folder
|
|
235
|
+
* NAME and (for supported types) CONTENT — so a partial job number or
|
|
236
|
+
* filename matches files buried several folders deep (e.g. a PDF under
|
|
237
|
+
* "Projects to Bid/2026/Acme/Drawings") that the one-level {@link browseDrive}
|
|
238
|
+
* can never reach. `$select` includes `parentReference` so each hit carries
|
|
239
|
+
* its folder path for breadcrumbs / relinking.
|
|
240
|
+
*
|
|
241
|
+
* `scopePath` (optional, drive-root-relative) narrows the search to a
|
|
242
|
+
* subtree by addressing the folder before `:/search` — e.g.
|
|
243
|
+
* GET /sites/{id}/drive/root:/Projects to Bid:/search(q='...')
|
|
244
|
+
* Use it to keep results inside the org's bid folder.
|
|
245
|
+
*
|
|
246
|
+
* @param siteId SharePoint site id, or {@link ONEDRIVE_SITE_ID} for OneDrive
|
|
247
|
+
* @param query free-text query (job number, partial filename, content)
|
|
248
|
+
* @param opts.scopePath optional folder subtree to search within
|
|
249
|
+
* @param opts.limit cap on hits returned (default 25); paging stops early
|
|
250
|
+
* @returns Array of search hits with folder path metadata
|
|
251
|
+
*/
|
|
252
|
+
async searchDrive(siteId, query, opts) {
|
|
253
|
+
const trimmed = query.trim();
|
|
254
|
+
if (trimmed.length === 0)
|
|
255
|
+
return [];
|
|
256
|
+
const limit = Math.max(1, Math.min(opts?.limit ?? 25, 200));
|
|
257
|
+
// Graph's search(q='...') is a function call: the q value is
|
|
258
|
+
// single-quoted in the URL; escape embedded single quotes by doubling.
|
|
259
|
+
const q = encodeURIComponent(trimmed.replace(/'/g, "''"));
|
|
260
|
+
const base = this.driveRoot(siteId);
|
|
261
|
+
const root = opts?.scopePath && opts.scopePath.length > 0
|
|
262
|
+
? `${base}/root:/${encodeDrivePath(opts.scopePath)}:`
|
|
263
|
+
: `${base}/root`;
|
|
264
|
+
const select = '$select=id,name,size,file,folder,webUrl,lastModifiedDateTime,parentReference';
|
|
265
|
+
const url = `${root}/search(q='${q}')?${select}&$top=${Math.min(limit, 100)}`;
|
|
266
|
+
const attempt = await settle(() => this.executeRequest(url, GraphJsonCollectionSchema));
|
|
267
|
+
if (!attempt.ok) {
|
|
268
|
+
const failure = attempt.cause;
|
|
269
|
+
if (siteId === ONEDRIVE_SITE_ID && !opts?.scopePath && failure instanceof MicrosoftGraphError && failure.status === 404)
|
|
270
|
+
return [];
|
|
271
|
+
throw failure;
|
|
272
|
+
}
|
|
273
|
+
const response = attempt.value;
|
|
274
|
+
// Page until we have `limit` hits or run out — but never crawl forever.
|
|
275
|
+
const raw = [...response.value];
|
|
276
|
+
let link = response['@odata.nextLink'];
|
|
277
|
+
while (link && raw.length < limit) {
|
|
278
|
+
const page = await this.executeRequest(link, GraphJsonCollectionSchema);
|
|
279
|
+
raw.push(...page.value);
|
|
280
|
+
link = page['@odata.nextLink'];
|
|
281
|
+
}
|
|
282
|
+
return raw
|
|
283
|
+
.slice(0, limit)
|
|
284
|
+
.map((item) => this.normalizeSearchResult(item, siteId))
|
|
285
|
+
.filter((entry) => entry !== null);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Download file content from a SharePoint drive.
|
|
289
|
+
*
|
|
290
|
+
* Read Graph's item metadata, then fetch its preauthenticated download URL
|
|
291
|
+
* without forwarding Graph authorization across origins.
|
|
292
|
+
*
|
|
293
|
+
* Returns the raw binary content as a Blob.
|
|
294
|
+
* Callers can convert to ArrayBuffer, ReadableStream, or base64
|
|
295
|
+
* depending on their needs.
|
|
296
|
+
*
|
|
297
|
+
* @param siteId - SharePoint site ID
|
|
298
|
+
* @param itemId - Drive item ID (file)
|
|
299
|
+
* @returns Blob with file content
|
|
300
|
+
*/
|
|
301
|
+
async downloadFile(siteId, itemId) {
|
|
302
|
+
const root = `${this.driveRoot(siteId)}/items/${itemId}`;
|
|
303
|
+
const metadata = await this.executeRequest(root, GraphDownloadSchema);
|
|
304
|
+
const response = await this.downloadResponse(metadata, `${root}/content`);
|
|
305
|
+
const content = await settle(() => response.blob());
|
|
306
|
+
if (!content.ok)
|
|
307
|
+
throw microsoftFailure(content.cause);
|
|
308
|
+
return content.value;
|
|
309
|
+
}
|
|
310
|
+
// Write operations (object protocol — used by SharePointStorageAdapter.objectStore)
|
|
311
|
+
/**
|
|
312
|
+
* Upload bytes to `path` (relative to the site drive root), creating or
|
|
313
|
+
* overwriting the file. Simple PUT below ~4 MiB; resumable upload session
|
|
314
|
+
* (chunked) at or above it.
|
|
315
|
+
*/
|
|
316
|
+
async uploadFile(siteId, path, body, opts) {
|
|
317
|
+
const bytes = await toUint8Array(body);
|
|
318
|
+
const raw = bytes.byteLength < SIMPLE_UPLOAD_MAX
|
|
319
|
+
? await this.simpleUpload(siteId, path, bytes, opts?.contentType)
|
|
320
|
+
: await this.resumableUpload(siteId, path, bytes, opts?.conflictBehavior);
|
|
321
|
+
const normalized = this.normalizeDriveItem(raw);
|
|
322
|
+
if (!normalized) {
|
|
323
|
+
throw new Error(`Graph upload to '${path}' returned an unparseable drive item.`);
|
|
324
|
+
}
|
|
325
|
+
return normalized;
|
|
326
|
+
}
|
|
327
|
+
async simpleUpload(siteId, path, bytes, contentType) {
|
|
328
|
+
const url = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}:/content`;
|
|
329
|
+
const accessToken = await this.acquireAccessToken();
|
|
330
|
+
const response = await this.fetchResponse(url, {
|
|
331
|
+
method: 'PUT',
|
|
332
|
+
headers: {
|
|
333
|
+
Authorization: `Bearer ${accessToken}`,
|
|
334
|
+
'Content-Type': contentType ?? 'application/octet-stream'
|
|
335
|
+
},
|
|
336
|
+
body: new Blob([bytes])
|
|
337
|
+
});
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
throw await graphFailure(response);
|
|
340
|
+
}
|
|
341
|
+
return this.readGraphResponse(response, GraphJsonSchema);
|
|
342
|
+
}
|
|
343
|
+
async resumableUpload(siteId, path, bytes, conflictBehavior) {
|
|
344
|
+
const sessionUrl = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}:/createUploadSession`;
|
|
345
|
+
const session = await this.executeWriteJson(sessionUrl, 'POST', {
|
|
346
|
+
item: { '@microsoft.graph.conflictBehavior': conflictBehavior ?? 'replace' }
|
|
347
|
+
});
|
|
348
|
+
const uploadUrl = this.admitGraphResponse(GraphUploadSessionSchema, session).uploadUrl;
|
|
349
|
+
if (!uploadUrl) {
|
|
350
|
+
throw new Error('Graph createUploadSession returned no uploadUrl.');
|
|
351
|
+
}
|
|
352
|
+
const total = bytes.byteLength;
|
|
353
|
+
let offset = 0;
|
|
354
|
+
let last = null;
|
|
355
|
+
while (offset < total) {
|
|
356
|
+
const end = Math.min(offset + UPLOAD_CHUNK, total);
|
|
357
|
+
const chunk = bytes.subarray(offset, end);
|
|
358
|
+
// The upload-session URL is pre-authenticated — no Authorization header.
|
|
359
|
+
last = await this.fetchResponse(uploadUrl, {
|
|
360
|
+
method: 'PUT',
|
|
361
|
+
// Content-Length is set by fetch from the body (setting it manually
|
|
362
|
+
// throws on workerd). Content-Range is the required upload-session header.
|
|
363
|
+
headers: { 'Content-Range': `bytes ${offset}-${end - 1}/${total}` },
|
|
364
|
+
body: new Blob([chunk])
|
|
365
|
+
}, false);
|
|
366
|
+
if (!last.ok && last.status !== 202) {
|
|
367
|
+
throw await graphFailure(last);
|
|
368
|
+
}
|
|
369
|
+
offset = end;
|
|
370
|
+
if (offset < total)
|
|
371
|
+
await closeMicrosoftResponse(last);
|
|
372
|
+
}
|
|
373
|
+
if (!last) {
|
|
374
|
+
throw new Error('Graph resumable upload produced no response.');
|
|
375
|
+
}
|
|
376
|
+
// The final chunk's 200/201 response carries the completed DriveItem.
|
|
377
|
+
return this.readGraphResponse(last, GraphJsonSchema);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Create a folder named `name` under `parentPath` (drive-root-relative; empty
|
|
381
|
+
* for the root). `conflictBehavior` defaults to `fail` so existing folders
|
|
382
|
+
* are not clobbered.
|
|
383
|
+
*/
|
|
384
|
+
async createFolder(siteId, parentPath, name, conflictBehavior = 'fail') {
|
|
385
|
+
const url = parentPath && parentPath.length > 0
|
|
386
|
+
? `${this.driveRoot(siteId)}/root:/${encodeDrivePath(parentPath)}:/children`
|
|
387
|
+
: `${this.driveRoot(siteId)}/root/children`;
|
|
388
|
+
const raw = await this.executeWriteJson(url, 'POST', {
|
|
389
|
+
name,
|
|
390
|
+
folder: {},
|
|
391
|
+
'@microsoft.graph.conflictBehavior': conflictBehavior
|
|
392
|
+
});
|
|
393
|
+
const normalized = this.normalizeDriveItem(raw);
|
|
394
|
+
if (!normalized) {
|
|
395
|
+
throw new Error(`Graph createFolder '${name}' returned an unparseable drive item.`);
|
|
396
|
+
}
|
|
397
|
+
return normalized;
|
|
398
|
+
}
|
|
399
|
+
/** Delete a drive item by its id. */
|
|
400
|
+
async deleteItem(siteId, itemId) {
|
|
401
|
+
await this.executeDelete(`${this.driveRoot(siteId)}/items/${itemId}`);
|
|
402
|
+
}
|
|
403
|
+
/** Delete a drive item by its drive-root-relative path. */
|
|
404
|
+
async deletePath(siteId, path) {
|
|
405
|
+
await this.executeDelete(`${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}`);
|
|
406
|
+
}
|
|
407
|
+
/** Fetch metadata for the item at `path`, or `null` if it does not exist. */
|
|
408
|
+
async headPath(siteId, path) {
|
|
409
|
+
const url = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}?$select=id,name,size,file,lastModifiedDateTime,webUrl`;
|
|
410
|
+
const accessToken = await this.acquireAccessToken();
|
|
411
|
+
const response = await this.fetchResponse(url, {
|
|
412
|
+
method: 'GET',
|
|
413
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' }
|
|
414
|
+
});
|
|
415
|
+
if (response.status === 404) {
|
|
416
|
+
await closeMicrosoftResponse(response);
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
throw await graphFailure(response);
|
|
421
|
+
}
|
|
422
|
+
const item = await this.readGraphResponse(response, GraphDriveItemSchema);
|
|
423
|
+
return {
|
|
424
|
+
id: item.id, size: item.size, mimeType: item.file?.mimeType ?? '',
|
|
425
|
+
lastModified: item.lastModifiedDateTime ?? '', webUrl: item.webUrl
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
/** Stream the bytes of the item at `path`, or `null` if it does not exist. */
|
|
429
|
+
async getStream(siteId, path) {
|
|
430
|
+
const root = `${this.driveRoot(siteId)}/root:/${encodeDrivePath(path)}`;
|
|
431
|
+
const metadata = await settle(() => this.executeRequest(root, GraphDownloadSchema));
|
|
432
|
+
if (!metadata.ok) {
|
|
433
|
+
if (metadata.cause instanceof MicrosoftGraphError && metadata.cause.status === 404)
|
|
434
|
+
return null;
|
|
435
|
+
throw metadata.cause;
|
|
436
|
+
}
|
|
437
|
+
const response = await this.downloadResponse(metadata.value, `${root}:/content`);
|
|
438
|
+
return response.body;
|
|
439
|
+
}
|
|
440
|
+
/** Metadata URLs and Graph's proxy fallback both leave credentials at Graph. */
|
|
441
|
+
async downloadResponse(metadata, contentUrl) {
|
|
442
|
+
let downloadUrl = metadata['@microsoft.graph.downloadUrl'];
|
|
443
|
+
let response;
|
|
444
|
+
if (downloadUrl) {
|
|
445
|
+
response = await this.fetchResponse(this.downloadTarget(downloadUrl, contentUrl), {
|
|
446
|
+
method: 'GET', headers: { Accept: 'application/octet-stream' }
|
|
447
|
+
}, false);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
const accessToken = await this.acquireAccessToken();
|
|
451
|
+
response = await this.fetchResponse(contentUrl, {
|
|
452
|
+
method: 'GET', redirect: 'manual',
|
|
453
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/octet-stream' }
|
|
454
|
+
});
|
|
455
|
+
const location = response.headers.get('location');
|
|
456
|
+
if ([301, 302, 303, 307, 308].includes(response.status) && location !== null) {
|
|
457
|
+
// Release Graph's body before allocating the separate credential-free request.
|
|
458
|
+
await closeMicrosoftResponse(response);
|
|
459
|
+
downloadUrl = this.downloadTarget(location, contentUrl);
|
|
460
|
+
response = await this.fetchResponse(downloadUrl, {
|
|
461
|
+
method: 'GET', headers: { Accept: 'application/octet-stream' }
|
|
462
|
+
}, false);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (!response.ok)
|
|
466
|
+
throw await graphFailure(response);
|
|
467
|
+
return response;
|
|
468
|
+
}
|
|
469
|
+
downloadTarget(location, base) {
|
|
470
|
+
const target = settleNow(() => new URL(location, base));
|
|
471
|
+
if (!target.ok || target.value.protocol !== 'https:' || target.value.username || target.value.password) {
|
|
472
|
+
throw new CapabilityError('Microsoft Graph returned an invalid download URL.', 'INVALID_INPUT', {
|
|
473
|
+
reason: 'microsoft-graph-download-url-invalid'
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return target.value.href;
|
|
477
|
+
}
|
|
478
|
+
// Shared write helpers
|
|
479
|
+
async executeWriteJson(url, method, jsonBody) {
|
|
480
|
+
const accessToken = await this.acquireAccessToken();
|
|
481
|
+
const response = await this.fetchResponse(url, {
|
|
482
|
+
method,
|
|
483
|
+
headers: {
|
|
484
|
+
Authorization: `Bearer ${accessToken}`,
|
|
485
|
+
'Content-Type': 'application/json',
|
|
486
|
+
Accept: 'application/json'
|
|
487
|
+
},
|
|
488
|
+
body: JSON.stringify(jsonBody)
|
|
489
|
+
});
|
|
490
|
+
if (!response.ok) {
|
|
491
|
+
throw await graphFailure(response);
|
|
492
|
+
}
|
|
493
|
+
return this.readGraphResponse(response, GraphJsonSchema);
|
|
494
|
+
}
|
|
495
|
+
async executeDelete(url) {
|
|
496
|
+
const accessToken = await this.acquireAccessToken();
|
|
497
|
+
const response = await this.fetchResponse(url, {
|
|
498
|
+
method: 'DELETE',
|
|
499
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
500
|
+
});
|
|
501
|
+
if (!response.ok && response.status !== 204) {
|
|
502
|
+
throw await graphFailure(response);
|
|
503
|
+
}
|
|
504
|
+
await closeMicrosoftResponse(response);
|
|
505
|
+
}
|
|
506
|
+
/** Retrieve a token only for legacy direct callers; registered providers use Secrets. */
|
|
507
|
+
async acquireAccessToken() {
|
|
508
|
+
if (this.authenticatedFetch)
|
|
509
|
+
return '';
|
|
510
|
+
const integration = this.integration;
|
|
511
|
+
if (!integration) {
|
|
512
|
+
throw new CapabilityError('Microsoft Graph has no token acquirer.', 'UNAVAILABLE', { reason: 'microsoft-graph-auth-transport-required' });
|
|
513
|
+
}
|
|
514
|
+
const scopes = ['https://graph.microsoft.com/.default'];
|
|
515
|
+
const result = await integration.acquireToken(scopes);
|
|
516
|
+
return result.accessToken;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Graph drive base for the storage drive identifier. The sentinel
|
|
520
|
+
* {@link ONEDRIVE_SITE_ID} (`'me'`) targets the signed-in user's personal
|
|
521
|
+
* OneDrive (`/me/drive`, delegated only). Composite SharePoint site ids keep
|
|
522
|
+
* using `/sites/{id}/drive` for backwards compatibility; real Graph drive ids
|
|
523
|
+
* use `/drives/{id}`.
|
|
524
|
+
*/
|
|
525
|
+
driveRoot(siteOrDriveId) {
|
|
526
|
+
if (siteOrDriveId === ONEDRIVE_SITE_ID)
|
|
527
|
+
return `${GRAPH_API_BASE}/me/drive`;
|
|
528
|
+
if (looksLikeGraphDriveId(siteOrDriveId)) {
|
|
529
|
+
return `${GRAPH_API_BASE}/drives/${siteOrDriveId}`;
|
|
530
|
+
}
|
|
531
|
+
return `${GRAPH_API_BASE}/sites/${siteOrDriveId}/drive`;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Execute a JSON-bearing Graph API request with auth header.
|
|
535
|
+
*/
|
|
536
|
+
async executeRequest(url, schema) {
|
|
537
|
+
// Pagination URLs carry our bearer: only the owning Graph origin is admitted.
|
|
538
|
+
const target = new URL(url);
|
|
539
|
+
if (target.origin !== 'https://graph.microsoft.com')
|
|
540
|
+
throw new CapabilityError('Invalid Microsoft Graph continuation origin.', 'INVALID_INPUT', { reason: 'microsoft-graph-origin-invalid' });
|
|
541
|
+
const accessToken = await this.acquireAccessToken();
|
|
542
|
+
const response = await this.fetchResponse(target.href, {
|
|
543
|
+
method: 'GET', redirect: 'error',
|
|
544
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' }
|
|
545
|
+
});
|
|
546
|
+
if (!response.ok)
|
|
547
|
+
throw await graphFailure(response);
|
|
548
|
+
return this.readGraphResponse(response, schema);
|
|
549
|
+
}
|
|
550
|
+
/** All Graph transports preserve typed, value-safe failures. */
|
|
551
|
+
async fetchResponse(url, init, authenticated = true) {
|
|
552
|
+
const fetcher = authenticated ? this.authenticatedFetch ?? this.fetchFn : this.fetchFn;
|
|
553
|
+
const headers = new Headers(init.headers);
|
|
554
|
+
if (authenticated && this.authenticatedFetch)
|
|
555
|
+
headers.delete('authorization');
|
|
556
|
+
const response = await settle(() => fetcher(url, { ...init, headers }));
|
|
557
|
+
if (!response.ok)
|
|
558
|
+
throw microsoftFailure(response.cause);
|
|
559
|
+
return response.value;
|
|
560
|
+
}
|
|
561
|
+
admitGraphResponse(schema, value) {
|
|
562
|
+
const parsed = schema.safeParse(value);
|
|
563
|
+
if (parsed.success)
|
|
564
|
+
return parsed.data;
|
|
565
|
+
throw new CapabilityError('Microsoft Graph returned an invalid response.', 'UNAVAILABLE', {
|
|
566
|
+
reason: 'microsoft-graph-response-invalid', cause: parsed.error
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
async readGraphResponse(response, schema) {
|
|
570
|
+
const decoded = await settle(() => response.json());
|
|
571
|
+
if (!decoded.ok && decoded.cause instanceof CapabilityError)
|
|
572
|
+
throw decoded.cause;
|
|
573
|
+
if (!decoded.ok)
|
|
574
|
+
throw new CapabilityError('Microsoft Graph returned invalid JSON.', 'UNAVAILABLE', {
|
|
575
|
+
reason: 'microsoft-graph-response-invalid', cause: microsoftFailure(decoded.cause)
|
|
576
|
+
});
|
|
577
|
+
return this.admitGraphResponse(schema, decoded.value);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Follow @odata.nextLink for paginated responses.
|
|
581
|
+
*/
|
|
582
|
+
async fetchAllPages(initialItems, nextLink) {
|
|
583
|
+
const allItems = [...initialItems];
|
|
584
|
+
let link = nextLink;
|
|
585
|
+
while (link) {
|
|
586
|
+
const response = await this.executeRequest(link, GraphJsonCollectionSchema);
|
|
587
|
+
allItems.push(...response.value);
|
|
588
|
+
link = response['@odata.nextLink'];
|
|
589
|
+
}
|
|
590
|
+
return allItems;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Normalize a raw Graph DriveItem into our simplified DriveItemEntry.
|
|
594
|
+
*/
|
|
595
|
+
normalizeDriveItem(item) {
|
|
596
|
+
const parsed = GraphDriveItemSchema.safeParse(item);
|
|
597
|
+
if (!parsed.success)
|
|
598
|
+
throw new CapabilityError('Microsoft Graph returned a malformed resource.', 'UNAVAILABLE', { reason: 'microsoft-graph-response-invalid', cause: parsed.error });
|
|
599
|
+
const driveItem = parsed.data;
|
|
600
|
+
return DriveItemEntrySchema.parse({
|
|
601
|
+
id: driveItem.id,
|
|
602
|
+
name: driveItem.name,
|
|
603
|
+
size: driveItem.size,
|
|
604
|
+
lastModified: driveItem.lastModifiedDateTime ?? '',
|
|
605
|
+
mimeType: driveItem.file?.mimeType ?? '',
|
|
606
|
+
isFolder: driveItem.folder !== undefined,
|
|
607
|
+
webUrl: driveItem.webUrl
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Normalize a raw Graph search-hit DriveItem into a DriveItemSearchResult,
|
|
612
|
+
* deriving the drive-root-relative `parentPath` from `parentReference.path`.
|
|
613
|
+
*
|
|
614
|
+
* Graph's `parentReference.path` looks like `/drive/root:/Projects to Bid/2026`
|
|
615
|
+
* (URL-encoded segments). We strip the `/drive/root:` (or `/drives/{id}/root:`)
|
|
616
|
+
* prefix and decode each segment so consumers get `Projects to Bid/2026`.
|
|
617
|
+
*/
|
|
618
|
+
normalizeSearchResult(item, siteId) {
|
|
619
|
+
const base = this.normalizeDriveItem(item);
|
|
620
|
+
if (!base)
|
|
621
|
+
return null;
|
|
622
|
+
const resource = admitMicrosoft(GraphDriveItemSchema, item);
|
|
623
|
+
const rawPath = resource.parentReference?.path ?? '';
|
|
624
|
+
const parentId = resource.parentReference?.id ?? '';
|
|
625
|
+
// `/drive/root:/A/B` or `/drives/{id}/root:/A/B` → `A/B`. No `root:` → ''.
|
|
626
|
+
const afterRoot = rawPath.includes('root:') ? rawPath.split('root:')[1] ?? '' : '';
|
|
627
|
+
const parentPath = afterRoot
|
|
628
|
+
.split('/')
|
|
629
|
+
.filter((seg) => seg.length > 0)
|
|
630
|
+
.map((seg) => {
|
|
631
|
+
try {
|
|
632
|
+
return decodeURIComponent(seg);
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
throw new CapabilityError('Microsoft Graph returned an invalid encoded path.', 'UNAVAILABLE', {
|
|
636
|
+
reason: 'microsoft-graph-response-invalid', cause: microsoftFailure(error)
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
.join('/');
|
|
641
|
+
return DriveItemSearchResultSchema.parse({ ...base, parentPath, parentId, siteId });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
//# sourceMappingURL=graph-sharepoint-client.js.map
|