microsoft-onedrive-mock 1.0.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/src/batch.ts ADDED
@@ -0,0 +1,91 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Request, Response } from 'express';
3
+ import { createApp } from './index';
4
+
5
+ export const handleBatchRequest = async (req: Request, res: Response) => {
6
+ try {
7
+ const body = req.body;
8
+ if (!body || !Array.isArray(body.requests)) {
9
+ res.status(400).json({ error: { message: "Invalid batch payload" } });
10
+ return;
11
+ }
12
+
13
+ // We use an internal app instance without auth/latency middleware to process routes cleanly
14
+ const internalApp = createApp({ serverLagBefore: 0, serverLagAfter: 0 });
15
+
16
+ const batchResponses = await Promise.all(body.requests.map(async (part: any) => {
17
+ const { id, method, url, body: partBody, headers: partHeaders } = part;
18
+
19
+ // Reconstruct path. Graph batches often send relative urls like /me/drive/root
20
+ // We append /v1.0/ to map to our internal routes if missing
21
+ const requestPath = url.startsWith('/v1.0') ? url : `/v1.0${url.startsWith('/') ? '' : '/'}${url}`;
22
+
23
+ return new Promise((resolve) => {
24
+ const simulatedReq: any = {
25
+ method: method || 'GET',
26
+ url: requestPath,
27
+ headers: partHeaders || {},
28
+ body: partBody,
29
+ query: {}
30
+ };
31
+
32
+ // Inject auth header from primary request to pass auth middleware
33
+ if (req.headers.authorization && !simulatedReq.headers.authorization) {
34
+ simulatedReq.headers.authorization = req.headers.authorization;
35
+ }
36
+
37
+ const simulatedRes: any = {
38
+ statusCode: 200,
39
+ headers: {},
40
+ charset: 'utf-8',
41
+ status: function (code: number) {
42
+ this.statusCode = code;
43
+ return this;
44
+ },
45
+ set: function (headerKey: string, headerValue: string) {
46
+ this.headers[headerKey] = headerValue;
47
+ return this;
48
+ },
49
+ setHeader: function (headerKey: string, headerValue: string) {
50
+ this.headers[headerKey] = headerValue;
51
+ return this;
52
+ },
53
+ json: function (data: any) {
54
+ this.data = data;
55
+ this.end();
56
+ },
57
+ send: function (data: any) {
58
+ // Normally this would be string/buffer, but let's just hold it
59
+ this.data = data;
60
+ this.end();
61
+ },
62
+ end: function () {
63
+ resolve({
64
+ id,
65
+ status: this.statusCode,
66
+ headers: this.headers,
67
+ body: this.data
68
+ });
69
+ }
70
+ };
71
+
72
+ // Bypass async handlers and directly feed to internal instance
73
+ internalApp(simulatedReq as Request, simulatedRes as Response, () => {
74
+ // Fallback next
75
+ resolve({
76
+ id,
77
+ status: 404,
78
+ body: { error: { message: "Not found within batch router" } }
79
+ });
80
+ });
81
+ });
82
+ }));
83
+
84
+ res.json({
85
+ responses: batchResponses
86
+ });
87
+
88
+ } catch (error: any) {
89
+ res.status(500).json({ error: { message: "Internal server error during batch", details: error.message } });
90
+ }
91
+ };
package/src/index.ts ADDED
@@ -0,0 +1,102 @@
1
+ import express, { Request } from 'express';
2
+ import cors from 'cors';
3
+ import { driveStore } from './store';
4
+ import { createV1Router } from './routes/v1';
5
+ import { handleBatchRequest } from './batch';
6
+ import { AppConfig } from './types';
7
+
8
+ export * from './types';
9
+
10
+ const createApp = (config: AppConfig = {}) => {
11
+ if (!config.apiEndpoint) {
12
+ config.apiEndpoint = "";
13
+ }
14
+
15
+ const app = express();
16
+ app.use(cors({
17
+ // For downloads Microsoft uses specific headers sometimes, expose ETag
18
+ exposedHeaders: ['ETag', 'Date', 'Content-Length', 'Location']
19
+ }));
20
+ app.set('etag', false);
21
+
22
+ // Latency simulator
23
+ app.use(async (req, res, next) => {
24
+ const delay = Math.floor(Math.random() * 21);
25
+ if (delay > 0) {
26
+ await new Promise(resolve => setTimeout(resolve, delay));
27
+ }
28
+ next();
29
+ });
30
+
31
+ app.use(express.json({
32
+ verify: (req: Request, res, buf) => {
33
+ req.rawBody = buf;
34
+ }
35
+ }));
36
+ app.use(express.text({
37
+ type: ['multipart/mixed', 'multipart/related', 'text/*', 'application/xml', 'application/octet-stream'],
38
+ verify: (req: Request, res, buf) => {
39
+ req.rawBody = buf;
40
+ }
41
+ }));
42
+
43
+ // Explicit raw body for binary uploads
44
+ app.use(express.raw({
45
+ type: '*/*',
46
+ limit: '50mb',
47
+ verify: (req: Request, res, buf) => {
48
+ req.rawBody = buf;
49
+ }
50
+ }));
51
+
52
+ // Batch Route
53
+ app.post('/v1.0/$batch', handleBatchRequest);
54
+
55
+ // Debug
56
+ app.post('/debug/clear', (req, res) => {
57
+ driveStore.clear();
58
+ res.status(200).send('Cleared');
59
+ });
60
+
61
+ // Health Check
62
+ app.get('/', (req, res) => {
63
+ res.status(200).send('OK');
64
+ });
65
+
66
+ // Auth Middleware
67
+ const validTokens = ['valid-token', 'another-valid-token'];
68
+ app.use((req, res, next) => {
69
+ const authHeaderVal = req.headers.authorization;
70
+ const authHeader = Array.isArray(authHeaderVal) ? authHeaderVal[0] : authHeaderVal;
71
+
72
+ if (!authHeader) {
73
+ res.status(401).json({ error: { code: "unauthenticated", message: "Unauthorized: No token provided" } });
74
+ return;
75
+ }
76
+
77
+ const token = authHeader.split(' ')[1];
78
+ if (!validTokens.includes(token)) {
79
+ res.status(401).json({ error: { code: "unauthenticated", message: "Unauthorized: Invalid token" } });
80
+ return;
81
+ }
82
+ next();
83
+ });
84
+
85
+ app.use(createV1Router());
86
+
87
+ return app;
88
+ };
89
+
90
+ const startServer = (port: number, host: string = 'localhost', config: AppConfig = {}) => {
91
+ const app = createApp(config);
92
+ return app.listen(port, host, () => {
93
+ console.log(`Server is running on http://${host}:${port}`);
94
+ });
95
+ };
96
+
97
+ if (require.main === module) {
98
+ const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3006;
99
+ startServer(port);
100
+ }
101
+
102
+ export { createApp, startServer, driveStore };
@@ -0,0 +1,227 @@
1
+ import express, { Request, Response } from 'express';
2
+ import { driveStore } from '../store';
3
+
4
+ export const createV1Router = () => {
5
+ const app = express.Router();
6
+
7
+ // Helper to apply ?$select=id,name
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ const applySelect = (item: any, selectQuery?: string | string[] | qs.ParsedQs | qs.ParsedQs[]) => {
10
+ if (!selectQuery || typeof selectQuery !== 'string') return item;
11
+ const fields = selectQuery.split(',').map(f => f.trim());
12
+ if (fields.length === 0) return item;
13
+
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ const result: any = {};
16
+ for (const field of fields) {
17
+ if (item[field] !== undefined) {
18
+ result[field] = item[field];
19
+ }
20
+ }
21
+ return result;
22
+ };
23
+
24
+ // GET /me/drive/root
25
+ app.get('/v1.0/me/drive/root', (req: Request, res: Response) => {
26
+ const root = driveStore.getItem('root');
27
+ if (!root) return res.status(404).json({ error: { message: "Root not found" } });
28
+ res.json(applySelect(root, req.query.$select as string));
29
+ });
30
+
31
+ // GET /me/drive/items/{id}
32
+ app.get('/v1.0/me/drive/items/:itemId', (req: Request, res: Response) => {
33
+ let itemId = req.params.itemId as string;
34
+ if (itemId === 'root') itemId = 'root'; // Already handled if just mapping
35
+
36
+ const item = driveStore.getItem(itemId);
37
+ if (!item) {
38
+ res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
39
+ return;
40
+ }
41
+ res.json(applySelect(item, req.query.$select as string));
42
+ });
43
+
44
+ // GET /me/drive/items/{id}/children
45
+ app.get('/v1.0/me/drive/items/:itemId/children', (req: Request, res: Response) => {
46
+ const itemId = req.params.itemId as string;
47
+ const children = driveStore.listItems(itemId);
48
+ const mappedChildren = children.map(c => applySelect(c, req.query.$select as string));
49
+ res.json({ value: mappedChildren });
50
+ });
51
+
52
+ // GET /me/drive/root/search(q='query')
53
+ app.get('/v1.0/me/drive/root/search\\(q=\':query\'\\)', (req: Request, res: Response) => {
54
+ let query = (req.params.query as string) || "";
55
+ // Decode URI component incase it's URL encoded like %20 or %27
56
+ query = decodeURIComponent(query);
57
+ // Strip out the trailing quote matching if it caught the literal '
58
+ if (query.endsWith("'")) query = query.slice(0, -1);
59
+ query = query.toLowerCase();
60
+
61
+ // recursively find items
62
+ const allItems = driveStore.getAllItems();
63
+ const results = allItems.filter(item => item.name.toLowerCase().includes(query) && item.id !== 'root');
64
+
65
+ const mappedResults = results.map(c => applySelect(c, req.query.$select as string));
66
+ res.json({ value: mappedResults });
67
+ });
68
+
69
+ // POST /me/drive/items/{parent-id}/children (Create Metadata / Folder)
70
+ app.post('/v1.0/me/drive/items/:itemId/children', (req: Request, res: Response) => {
71
+ const parentId = req.params.itemId as string;
72
+ const body = req.body || {};
73
+
74
+ let parentItem = driveStore.getItem(parentId);
75
+ if (!parentItem && parentId === 'root') {
76
+ parentItem = driveStore.createItem({ id: 'root', name: 'root' }, true);
77
+ } else if (!parentItem) {
78
+ res.status(404).json({ error: { code: "itemNotFound", message: "Parent not found" } });
79
+ return;
80
+ }
81
+
82
+ const isFolder = !!body.folder;
83
+ const newItem = driveStore.createItem({
84
+ name: body.name || "Untitled",
85
+ parentReference: { id: parentId },
86
+ ...body
87
+ }, isFolder);
88
+
89
+ res.status(201).json(applySelect(newItem, req.query.$select as string));
90
+ });
91
+
92
+ // DELETE /me/drive/items/{id}
93
+ app.delete('/v1.0/me/drive/items/:itemId', (req: Request, res: Response) => {
94
+ const fileId = req.params.itemId as string;
95
+ if (fileId === 'root') {
96
+ res.status(400).json({ error: { message: "Cannot delete root" } });
97
+ return;
98
+ }
99
+
100
+ const item = driveStore.getItem(fileId);
101
+ if (!item) {
102
+ res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
103
+ return;
104
+ }
105
+
106
+ const ifMatch = req.header('If-Match');
107
+ if (ifMatch && ifMatch !== item.eTag) {
108
+ res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
109
+ return;
110
+ }
111
+
112
+ driveStore.deleteItem(fileId);
113
+ res.status(204).send();
114
+ });
115
+
116
+ // GET /me/drive/items/{id}/content
117
+ app.get('/v1.0/me/drive/items/:itemId/content', (req: Request, res: Response) => {
118
+ const fileId = req.params.itemId as string;
119
+ const file = driveStore.getItem(fileId);
120
+
121
+ if (!file || file.folder) {
122
+ res.status(404).json({ error: { code: "itemNotFound", message: "File not found" } });
123
+ return;
124
+ }
125
+
126
+ if (file.file?.mimeType) {
127
+ res.setHeader('Content-Type', file.file.mimeType);
128
+ }
129
+
130
+ if (file.content === undefined) {
131
+ res.send("");
132
+ return;
133
+ }
134
+
135
+ if (Buffer.isBuffer(file.content)) {
136
+ res.send(file.content);
137
+ } else if (typeof file.content === 'object') {
138
+ res.json(file.content);
139
+ } else {
140
+ res.send(file.content);
141
+ }
142
+ });
143
+
144
+ // PUT /me/drive/items/{parent-id}:/{filename}:/content
145
+ app.put('/v1.0/me/drive/items/:parentId\\:/:filename\\:/content', (req: Request, res: Response) => {
146
+ const parentId = req.params.parentId as string;
147
+ const filename = req.params.filename as string;
148
+
149
+ // Find existing or create
150
+ const children = driveStore.listItems(parentId);
151
+ let item = children.find(c => c.name === filename);
152
+
153
+ const content = req.rawBody !== undefined ? req.rawBody : req.body;
154
+ const headerMime = req.headers['content-type'];
155
+ const mimeType = (Array.isArray(headerMime) ? headerMime[0] : headerMime) || 'application/octet-stream';
156
+
157
+ const isNew = !item;
158
+
159
+ if (item) {
160
+ const ifMatch = req.header('If-Match');
161
+ if (ifMatch && ifMatch !== item.eTag) {
162
+ res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
163
+ return;
164
+ }
165
+
166
+ // Update
167
+ item = driveStore.updateItem(item.id, { content, file: { mimeType } })!;
168
+ } else {
169
+ // Create
170
+ item = driveStore.createItem({
171
+ name: filename,
172
+ parentReference: { id: parentId },
173
+ content,
174
+ file: { mimeType }
175
+ });
176
+ }
177
+
178
+ res.status(isNew ? 201 : 200).json(applySelect(item, req.query.$select as string));
179
+ });
180
+
181
+ // PUT /me/drive/items/{id}/content
182
+ app.put('/v1.0/me/drive/items/:itemId/content', (req: Request, res: Response) => {
183
+ const itemId = req.params.itemId as string;
184
+ let item = driveStore.getItem(itemId);
185
+
186
+ if (!item || item.folder) {
187
+ res.status(404).json({ error: { code: "itemNotFound", message: "Item not found" } });
188
+ return;
189
+ }
190
+
191
+ const ifMatch = req.header('If-Match');
192
+ if (ifMatch && ifMatch !== item.eTag) {
193
+ res.status(412).json({ error: { code: "PreconditionFailed", message: "ETag mismatch" } });
194
+ return;
195
+ }
196
+
197
+ const content = req.rawBody !== undefined ? req.rawBody : req.body;
198
+ const headerMime = req.headers['content-type'];
199
+ const mimeType = (Array.isArray(headerMime) ? headerMime[0] : headerMime) || item.file?.mimeType || 'application/octet-stream';
200
+
201
+ item = driveStore.updateItem(item.id, { content, file: { mimeType } })!;
202
+ res.status(200).json(applySelect(item, req.query.$select as string));
203
+ });
204
+
205
+ // Delta Query
206
+ // GET /me/drive/root/delta
207
+ app.get('/v1.0/me/drive/root/delta', (req: Request, res: Response) => {
208
+ const tokenStr = req.query.token as string;
209
+
210
+ let token: string | undefined = undefined;
211
+ if (tokenStr) token = tokenStr;
212
+
213
+ const result = driveStore.getDelta(token);
214
+
215
+ const host = req.headers.host || 'localhost';
216
+ const protocol = req.protocol || 'http';
217
+ const baseUrl = `${protocol}://${host}`;
218
+
219
+ res.json({
220
+ '@odata.context': `${baseUrl}/v1.0/$metadata#Collection(driveItem)`,
221
+ '@odata.deltaLink': `${baseUrl}/v1.0/me/drive/root/delta?token=${result.deltaLink}`,
222
+ value: result.items
223
+ });
224
+ });
225
+
226
+ return app;
227
+ };
package/src/store.ts ADDED
@@ -0,0 +1,191 @@
1
+ import * as crypto from 'crypto';
2
+ import { DriveItem } from './types';
3
+
4
+ export class DriveStore {
5
+ private items: Map<string, DriveItem>;
6
+ // To support delta we need a linear history of items created/updated/deleted
7
+ private deltaHistory: DriveItem[];
8
+
9
+ constructor() {
10
+ this.items = new Map();
11
+ this.deltaHistory = [];
12
+ }
13
+
14
+ private calculateStats(content: unknown): { size: number, sha1Hash: string } {
15
+ let buffer: Buffer;
16
+ if (typeof content === 'string') {
17
+ buffer = Buffer.from(content);
18
+ } else if (Buffer.isBuffer(content)) {
19
+ buffer = content;
20
+ } else if (content === undefined || content === null) {
21
+ buffer = Buffer.from('');
22
+ } else {
23
+ buffer = Buffer.from(JSON.stringify(content));
24
+ }
25
+
26
+ return {
27
+ size: buffer.length,
28
+ sha1Hash: crypto.createHash('sha1').update(buffer).digest('hex')
29
+ };
30
+ }
31
+
32
+ createItem(item: Partial<DriveItem> & { name: string }, isFolder = false): DriveItem {
33
+ if (!item.name) {
34
+ throw new Error("Item name is required");
35
+ }
36
+ const id = item.id || Math.random().toString(36).substring(7);
37
+ const now = new Date().toISOString();
38
+
39
+ const newItem: DriveItem = {
40
+ createdDateTime: now,
41
+ lastModifiedDateTime: now,
42
+ ...item,
43
+ id,
44
+ name: item.name,
45
+ eTag: `W/"1"`,
46
+ cTag: `"cTag-1"`,
47
+ size: 0,
48
+ parentReference: item.parentReference || { driveId: "b!", id: "root" }
49
+ };
50
+
51
+ if (isFolder) {
52
+ newItem.folder = { childCount: 0 };
53
+ } else {
54
+ const stats = this.calculateStats(item.content);
55
+ newItem.size = stats.size;
56
+ newItem.file = {
57
+ mimeType: item.file?.mimeType || "application/octet-stream",
58
+ hashes: { sha1Hash: stats.sha1Hash }
59
+ };
60
+ }
61
+
62
+ this.items.set(id, newItem);
63
+ this.addDeltaHistory(newItem);
64
+
65
+ // Update parent childCount if making a folder
66
+ if (newItem.parentReference && newItem.parentReference.id) {
67
+ this.incrementParentChildCount(newItem.parentReference.id, 1);
68
+ }
69
+
70
+ return newItem;
71
+ }
72
+
73
+ updateItem(id: string, updates: Partial<DriveItem>): DriveItem | null {
74
+ const item = this.items.get(id);
75
+ if (!item) return null;
76
+
77
+ // Extract internal version number from etag to increment
78
+ const currentVersion = parseInt(item.eTag.replace(/\D/g, '') || "1", 10);
79
+ const newVersion = currentVersion + 1;
80
+
81
+ const statsUpdates: Record<string, unknown> = {};
82
+ if (updates.content !== undefined && item.file) {
83
+ const stats = this.calculateStats(updates.content);
84
+ statsUpdates.size = stats.size;
85
+ statsUpdates.file = {
86
+ ...item.file,
87
+ hashes: { sha1Hash: stats.sha1Hash }
88
+ };
89
+ }
90
+
91
+ const updatedItem: DriveItem = {
92
+ ...item,
93
+ ...updates,
94
+ ...statsUpdates,
95
+ eTag: `W/"${newVersion}"`,
96
+ cTag: `"cTag-${newVersion}"`,
97
+ lastModifiedDateTime: updates.lastModifiedDateTime || new Date().toISOString()
98
+ };
99
+
100
+ this.items.set(id, updatedItem);
101
+ this.addDeltaHistory(updatedItem);
102
+ return updatedItem;
103
+ }
104
+
105
+ getItem(id: string): DriveItem | null {
106
+ return this.items.get(id) || null;
107
+ }
108
+
109
+ deleteItem(id: string): boolean {
110
+ const item = this.items.get(id);
111
+ if (!item) return false;
112
+
113
+ const deleted = this.items.delete(id);
114
+ if (deleted) {
115
+ const deletedItem = {
116
+ ...item,
117
+ deleted: { state: "deleted" },
118
+ lastModifiedDateTime: new Date().toISOString()
119
+ };
120
+ this.addDeltaHistory(deletedItem);
121
+
122
+ // Decrement parent count
123
+ if (item.parentReference && item.parentReference.id) {
124
+ this.incrementParentChildCount(item.parentReference.id, -1);
125
+ }
126
+ }
127
+ return deleted;
128
+ }
129
+
130
+ listItems(parentId?: string): DriveItem[] {
131
+ const allItems = Array.from(this.items.values());
132
+ if (!parentId) return allItems;
133
+
134
+ return allItems.filter(i => i.parentReference?.id === parentId);
135
+ }
136
+
137
+ getAllItems(): DriveItem[] {
138
+ return Array.from(this.items.values());
139
+ }
140
+
141
+ clear(): void {
142
+ this.items.clear();
143
+ this.deltaHistory = [];
144
+
145
+ // Always recreate a standard root folder
146
+ this.createItem({ id: 'root', name: 'root' }, true);
147
+ }
148
+
149
+ // Delta History (simulated changes API)
150
+ private addDeltaHistory(item: DriveItem) {
151
+ this.deltaHistory.push(JSON.parse(JSON.stringify(item)));
152
+ }
153
+
154
+ getDeltaToken(): string {
155
+ return String(this.deltaHistory.length);
156
+ }
157
+
158
+ getDelta(token?: string): { items: DriveItem[], deltaLink: string } {
159
+ const tokenIndex = token ? parseInt(token, 10) : 0;
160
+ const start = isNaN(tokenIndex) ? 0 : Math.max(0, tokenIndex);
161
+
162
+ const items = this.deltaHistory.slice(start);
163
+
164
+ // In MS Graph, if a file transitions multiple states, delta should ideally just return the latest state
165
+ // but for a mock, returning the log or deduping by id to latest state is standard.
166
+ // Let's dedupe to match real API behavior mostly (returns latest state within the page)
167
+ const dedupedMap = new Map<string, DriveItem>();
168
+ for (const item of items) {
169
+ dedupedMap.set(item.id, item);
170
+ }
171
+
172
+ const dedupedItems = Array.from(dedupedMap.values());
173
+ const newToken = String(this.deltaHistory.length);
174
+
175
+ return {
176
+ items: dedupedItems,
177
+ deltaLink: newToken
178
+ };
179
+ }
180
+
181
+ private incrementParentChildCount(parentId: string, amount: number) {
182
+ const parent = this.items.get(parentId);
183
+ if (parent && parent.folder) {
184
+ parent.folder.childCount = Math.max(0, parent.folder.childCount + amount);
185
+ }
186
+ }
187
+ }
188
+
189
+ export const driveStore = new DriveStore();
190
+ // Initialize root on boot
191
+ driveStore.clear();
package/src/types.ts ADDED
@@ -0,0 +1,59 @@
1
+ export interface AppConfig {
2
+ apiEndpoint?: string;
3
+ serverLagBefore?: number;
4
+ serverLagAfter?: number;
5
+ }
6
+
7
+ declare global {
8
+ // eslint-disable-next-line @typescript-eslint/no-namespace
9
+ namespace Express {
10
+ interface Request {
11
+ rawBody?: Buffer | string;
12
+ }
13
+ }
14
+ }
15
+
16
+ // Basic representation of a Microsoft Graph DriveItem
17
+ export interface DriveItem {
18
+ id: string;
19
+ name: string;
20
+ eTag: string;
21
+ cTag: string;
22
+ createdBy?: { user: { displayName: string } };
23
+ lastModifiedBy?: { user: { displayName: string } };
24
+ createdDateTime: string;
25
+ lastModifiedDateTime: string;
26
+ size: number;
27
+ parentReference?: {
28
+ driveId?: string;
29
+ driveType?: string;
30
+ id?: string;
31
+ path?: string;
32
+ };
33
+ file?: {
34
+ mimeType: string;
35
+ hashes?: {
36
+ quickXorHash?: string;
37
+ sha1Hash?: string;
38
+ sha256Hash?: string;
39
+ };
40
+ };
41
+ folder?: {
42
+ childCount: number;
43
+ };
44
+ deleted?: {
45
+ state: string;
46
+ };
47
+ '@microsoft.graph.downloadUrl'?: string;
48
+
49
+ // Internal usage for mock state
50
+ content?: unknown;
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ export interface DeltaResponse {
55
+ '@odata.context': string;
56
+ '@odata.nextLink'?: string;
57
+ '@odata.deltaLink'?: string;
58
+ value: DriveItem[];
59
+ }