@awolve/myoffice 1.6.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/README.md +271 -0
- package/dist/auth/cache-plugin.d.ts +20 -0
- package/dist/auth/cache-plugin.d.ts.map +1 -0
- package/dist/auth/cache-plugin.js +49 -0
- package/dist/auth/cache-plugin.js.map +1 -0
- package/dist/auth/config.d.ts +40 -0
- package/dist/auth/config.d.ts.map +1 -0
- package/dist/auth/config.js +135 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/device-code.d.ts +2 -0
- package/dist/auth/device-code.d.ts.map +1 -0
- package/dist/auth/device-code.js +40 -0
- package/dist/auth/device-code.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/login.d.ts +7 -0
- package/dist/auth/login.d.ts.map +1 -0
- package/dist/auth/login.js +22 -0
- package/dist/auth/login.js.map +1 -0
- package/dist/auth/token-manager.d.ts +4 -0
- package/dist/auth/token-manager.d.ts.map +1 -0
- package/dist/auth/token-manager.js +85 -0
- package/dist/auth/token-manager.js.map +1 -0
- package/dist/cli/formatter.d.ts +5 -0
- package/dist/cli/formatter.d.ts.map +1 -0
- package/dist/cli/formatter.js +317 -0
- package/dist/cli/formatter.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +973 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/handler.d.ts +8 -0
- package/dist/core/handler.d.ts.map +1 -0
- package/dist/core/handler.js +327 -0
- package/dist/core/handler.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +924 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/calendar.d.ts +124 -0
- package/dist/tools/calendar.d.ts.map +1 -0
- package/dist/tools/calendar.js +129 -0
- package/dist/tools/calendar.js.map +1 -0
- package/dist/tools/chats.d.ts +66 -0
- package/dist/tools/chats.d.ts.map +1 -0
- package/dist/tools/chats.js +102 -0
- package/dist/tools/chats.js.map +1 -0
- package/dist/tools/contacts.d.ts +138 -0
- package/dist/tools/contacts.d.ts.map +1 -0
- package/dist/tools/contacts.js +189 -0
- package/dist/tools/contacts.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/mail.d.ts +138 -0
- package/dist/tools/mail.d.ts.map +1 -0
- package/dist/tools/mail.js +187 -0
- package/dist/tools/mail.js.map +1 -0
- package/dist/tools/onedrive.d.ts +125 -0
- package/dist/tools/onedrive.d.ts.map +1 -0
- package/dist/tools/onedrive.js +203 -0
- package/dist/tools/onedrive.js.map +1 -0
- package/dist/tools/planner.d.ts +390 -0
- package/dist/tools/planner.d.ts.map +1 -0
- package/dist/tools/planner.js +693 -0
- package/dist/tools/planner.js.map +1 -0
- package/dist/tools/sharepoint.d.ts +138 -0
- package/dist/tools/sharepoint.d.ts.map +1 -0
- package/dist/tools/sharepoint.js +156 -0
- package/dist/tools/sharepoint.js.map +1 -0
- package/dist/tools/tasks.d.ts +107 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +131 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/teams.d.ts +66 -0
- package/dist/tools/teams.d.ts.map +1 -0
- package/dist/tools/teams.js +69 -0
- package/dist/tools/teams.js.map +1 -0
- package/dist/utils/graph-client.d.ts +15 -0
- package/dist/utils/graph-client.d.ts.map +1 -0
- package/dist/utils/graph-client.js +126 -0
- package/dist/utils/graph-client.js.map +1 -0
- package/dist/utils/signature.d.ts +2 -0
- package/dist/utils/signature.d.ts.map +1 -0
- package/dist/utils/signature.js +17 -0
- package/dist/utils/signature.js.map +1 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +20 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import { readFile } from 'fs/promises';
|
|
4
|
+
import { graphRequest, graphList, graphUpload, graphUploadLarge } from '../utils/graph-client.js';
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Schemas
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Plans (read-only)
|
|
9
|
+
export const listPlansSchema = z.object({
|
|
10
|
+
maxItems: z.number().optional().describe('Maximum number of plans. Default: 50'),
|
|
11
|
+
});
|
|
12
|
+
export const getPlanSchema = z.object({
|
|
13
|
+
planId: z.string().describe('The plan ID'),
|
|
14
|
+
});
|
|
15
|
+
// Buckets
|
|
16
|
+
export const listBucketsSchema = z.object({
|
|
17
|
+
planId: z.string().describe('The plan ID'),
|
|
18
|
+
});
|
|
19
|
+
export const createBucketSchema = z.object({
|
|
20
|
+
planId: z.string().describe('The plan ID'),
|
|
21
|
+
name: z.string().describe('Bucket name'),
|
|
22
|
+
});
|
|
23
|
+
export const updateBucketSchema = z.object({
|
|
24
|
+
bucketId: z.string().describe('The bucket ID'),
|
|
25
|
+
name: z.string().describe('New bucket name'),
|
|
26
|
+
});
|
|
27
|
+
export const deleteBucketSchema = z.object({
|
|
28
|
+
bucketId: z.string().describe('The bucket ID'),
|
|
29
|
+
});
|
|
30
|
+
// Tasks
|
|
31
|
+
export const listPlannerTasksSchema = z.object({
|
|
32
|
+
planId: z.string().describe('The plan ID'),
|
|
33
|
+
bucketId: z.string().optional().describe('Filter by bucket ID'),
|
|
34
|
+
maxItems: z.number().optional().describe('Maximum number of tasks. Default: 100'),
|
|
35
|
+
});
|
|
36
|
+
export const getPlannerTaskSchema = z.object({
|
|
37
|
+
taskId: z.string().describe('The task ID'),
|
|
38
|
+
});
|
|
39
|
+
export const createPlannerTaskSchema = z.object({
|
|
40
|
+
planId: z.string().describe('The plan ID'),
|
|
41
|
+
title: z.string().describe('Task title'),
|
|
42
|
+
bucketId: z.string().optional().describe('Bucket ID to place the task in'),
|
|
43
|
+
assignments: z
|
|
44
|
+
.array(z.string())
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Email addresses of users to assign'),
|
|
47
|
+
dueDateTime: z.string().optional().describe('Due date (ISO format)'),
|
|
48
|
+
startDateTime: z.string().optional().describe('Start date (ISO format)'),
|
|
49
|
+
priority: z
|
|
50
|
+
.enum(['urgent', 'important', 'medium', 'low'])
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Task priority'),
|
|
53
|
+
progress: z
|
|
54
|
+
.enum(['notStarted', 'inProgress', 'completed'])
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('Task progress'),
|
|
57
|
+
});
|
|
58
|
+
export const updatePlannerTaskSchema = z.object({
|
|
59
|
+
taskId: z.string().describe('The task ID'),
|
|
60
|
+
title: z.string().optional().describe('New task title'),
|
|
61
|
+
bucketId: z.string().optional().describe('Move to different bucket'),
|
|
62
|
+
assignments: z
|
|
63
|
+
.array(z.string())
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Email addresses of users to assign (replaces existing)'),
|
|
66
|
+
clearAssignments: z.boolean().optional().describe('Remove all assignments'),
|
|
67
|
+
dueDateTime: z.string().optional().describe('New due date (ISO format)'),
|
|
68
|
+
clearDue: z.boolean().optional().describe('Clear the due date'),
|
|
69
|
+
startDateTime: z.string().optional().describe('New start date (ISO format)'),
|
|
70
|
+
priority: z
|
|
71
|
+
.enum(['urgent', 'important', 'medium', 'low'])
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('New priority'),
|
|
74
|
+
progress: z
|
|
75
|
+
.enum(['notStarted', 'inProgress', 'completed'])
|
|
76
|
+
.optional()
|
|
77
|
+
.describe('New progress'),
|
|
78
|
+
});
|
|
79
|
+
export const deletePlannerTaskSchema = z.object({
|
|
80
|
+
taskId: z.string().describe('The task ID'),
|
|
81
|
+
});
|
|
82
|
+
// Task Details
|
|
83
|
+
export const getPlannerTaskDetailsSchema = z.object({
|
|
84
|
+
taskId: z.string().describe('The task ID'),
|
|
85
|
+
});
|
|
86
|
+
export const updatePlannerTaskDetailsSchema = z.object({
|
|
87
|
+
taskId: z.string().describe('The task ID'),
|
|
88
|
+
description: z.string().optional().describe('Task description'),
|
|
89
|
+
checklist: z
|
|
90
|
+
.array(z.object({
|
|
91
|
+
title: z.string().describe('Checklist item title'),
|
|
92
|
+
isChecked: z.boolean().optional().describe('Whether item is checked'),
|
|
93
|
+
}))
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('Checklist items (replaces existing checklist)'),
|
|
96
|
+
});
|
|
97
|
+
// Checklist operations
|
|
98
|
+
export const addChecklistItemSchema = z.object({
|
|
99
|
+
taskId: z.string().describe('The task ID'),
|
|
100
|
+
title: z.string().describe('Checklist item title'),
|
|
101
|
+
isChecked: z.boolean().optional().describe('Whether item is checked. Default: false'),
|
|
102
|
+
});
|
|
103
|
+
export const removeChecklistItemSchema = z.object({
|
|
104
|
+
taskId: z.string().describe('The task ID'),
|
|
105
|
+
itemId: z.string().describe('The checklist item ID'),
|
|
106
|
+
});
|
|
107
|
+
export const toggleChecklistItemSchema = z.object({
|
|
108
|
+
taskId: z.string().describe('The task ID'),
|
|
109
|
+
itemId: z.string().describe('The checklist item ID'),
|
|
110
|
+
});
|
|
111
|
+
// References (attachments)
|
|
112
|
+
export const addPlannerTaskReferenceSchema = z.object({
|
|
113
|
+
taskId: z.string().describe('The task ID'),
|
|
114
|
+
url: z.string().url().describe('URL of the file or link to attach'),
|
|
115
|
+
alias: z.string().optional().describe('Display name for the reference'),
|
|
116
|
+
type: z
|
|
117
|
+
.enum(['Word', 'Excel', 'PowerPoint', 'OneNote', 'Project', 'Visio', 'Pdf', 'TeamsHostedApp', 'Other'])
|
|
118
|
+
.optional()
|
|
119
|
+
.describe('Type of reference (auto-detected if not provided)'),
|
|
120
|
+
});
|
|
121
|
+
export const removePlannerTaskReferenceSchema = z.object({
|
|
122
|
+
taskId: z.string().describe('The task ID'),
|
|
123
|
+
url: z.string().url().describe('URL of the reference to remove'),
|
|
124
|
+
});
|
|
125
|
+
export const uploadAndAttachSchema = z.object({
|
|
126
|
+
taskId: z.string().describe('The Planner task ID'),
|
|
127
|
+
localPath: z.string().describe('Local file path to upload'),
|
|
128
|
+
remotePath: z
|
|
129
|
+
.string()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe('Destination path in OneDrive. If omitted, uploads to "Planner Attachments/<filename>"'),
|
|
132
|
+
alias: z.string().optional().describe('Display name for the attachment'),
|
|
133
|
+
});
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// Helpers
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Priority mapping: semantic -> numeric
|
|
138
|
+
const PRIORITY_MAP = {
|
|
139
|
+
urgent: 1,
|
|
140
|
+
important: 3,
|
|
141
|
+
medium: 5,
|
|
142
|
+
low: 9,
|
|
143
|
+
};
|
|
144
|
+
// Reverse priority mapping for display
|
|
145
|
+
const PRIORITY_REVERSE = {
|
|
146
|
+
1: 'urgent',
|
|
147
|
+
3: 'important',
|
|
148
|
+
5: 'medium',
|
|
149
|
+
9: 'low',
|
|
150
|
+
};
|
|
151
|
+
// Progress mapping: semantic -> percentComplete
|
|
152
|
+
const PROGRESS_MAP = {
|
|
153
|
+
notStarted: 0,
|
|
154
|
+
inProgress: 50,
|
|
155
|
+
completed: 100,
|
|
156
|
+
};
|
|
157
|
+
// Reverse progress mapping for display
|
|
158
|
+
function progressToSemantic(percentComplete) {
|
|
159
|
+
if (percentComplete === 0)
|
|
160
|
+
return 'notStarted';
|
|
161
|
+
if (percentComplete === 100)
|
|
162
|
+
return 'completed';
|
|
163
|
+
return 'inProgress';
|
|
164
|
+
}
|
|
165
|
+
// Reference type detection from URL
|
|
166
|
+
// Valid types per Microsoft API: Other, Word, Excel, PowerPoint, OneNote, Project, Visio, Pdf, TeamsHostedApp
|
|
167
|
+
function detectReferenceType(url) {
|
|
168
|
+
const lowerUrl = url.toLowerCase();
|
|
169
|
+
if (lowerUrl.includes('.docx') || lowerUrl.includes('.doc'))
|
|
170
|
+
return 'Word';
|
|
171
|
+
if (lowerUrl.includes('.xlsx') || lowerUrl.includes('.xls'))
|
|
172
|
+
return 'Excel';
|
|
173
|
+
if (lowerUrl.includes('.pptx') || lowerUrl.includes('.ppt'))
|
|
174
|
+
return 'PowerPoint';
|
|
175
|
+
if (lowerUrl.includes('.one') || lowerUrl.includes('onenote'))
|
|
176
|
+
return 'OneNote';
|
|
177
|
+
if (lowerUrl.includes('.pdf'))
|
|
178
|
+
return 'Pdf';
|
|
179
|
+
if (lowerUrl.includes('.mpp'))
|
|
180
|
+
return 'Project';
|
|
181
|
+
if (lowerUrl.includes('.vsdx') || lowerUrl.includes('.vsd'))
|
|
182
|
+
return 'Visio';
|
|
183
|
+
// All other files (images, SharePoint URLs, OneDrive URLs, etc.) use 'Other'
|
|
184
|
+
return 'Other';
|
|
185
|
+
}
|
|
186
|
+
// Encode URL for use as reference key (special encoding required by Planner)
|
|
187
|
+
// Only encode characters that are problematic as JSON object keys, preserve URL structure
|
|
188
|
+
function encodeReferenceUrl(url) {
|
|
189
|
+
// Microsoft Planner requires periods and some special chars to be encoded
|
|
190
|
+
// but the URL must remain parseable (don't encode :, /, ?, &, =, etc.)
|
|
191
|
+
return url
|
|
192
|
+
.replace(/%/g, '%25') // Encode existing percent signs first
|
|
193
|
+
.replace(/\./g, '%2E') // Periods must be encoded
|
|
194
|
+
.replace(/#/g, '%23') // Hash/fragment
|
|
195
|
+
.replace(/\s/g, '%20'); // Spaces
|
|
196
|
+
}
|
|
197
|
+
// Get ETag for a resource
|
|
198
|
+
async function getETag(endpoint) {
|
|
199
|
+
const resource = await graphRequest(endpoint);
|
|
200
|
+
const etag = resource['@odata.etag'];
|
|
201
|
+
if (!etag) {
|
|
202
|
+
throw new Error('Resource does not have an ETag');
|
|
203
|
+
}
|
|
204
|
+
return etag;
|
|
205
|
+
}
|
|
206
|
+
// Simple cache for user ID lookups within a session
|
|
207
|
+
const userIdCache = new Map();
|
|
208
|
+
// Resolve email to user ID
|
|
209
|
+
async function resolveUserId(emailOrId) {
|
|
210
|
+
// If it looks like a GUID, assume it's already a user ID
|
|
211
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(emailOrId)) {
|
|
212
|
+
return emailOrId;
|
|
213
|
+
}
|
|
214
|
+
// Check cache
|
|
215
|
+
const cached = userIdCache.get(emailOrId.toLowerCase());
|
|
216
|
+
if (cached) {
|
|
217
|
+
return cached;
|
|
218
|
+
}
|
|
219
|
+
// Look up user by email
|
|
220
|
+
const user = await graphRequest(`/users/${encodeURIComponent(emailOrId)}`);
|
|
221
|
+
userIdCache.set(emailOrId.toLowerCase(), user.id);
|
|
222
|
+
return user.id;
|
|
223
|
+
}
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// Plans (Read-Only)
|
|
226
|
+
// ============================================================================
|
|
227
|
+
export async function listPlans(params) {
|
|
228
|
+
const { maxItems = 50 } = params;
|
|
229
|
+
const plans = await graphList('/me/planner/plans', { maxItems });
|
|
230
|
+
return plans.map((p) => ({
|
|
231
|
+
id: p.id,
|
|
232
|
+
title: p.title,
|
|
233
|
+
owner: p.owner,
|
|
234
|
+
createdDateTime: p.createdDateTime,
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
export async function getPlan(params) {
|
|
238
|
+
const { planId } = params;
|
|
239
|
+
const plan = await graphRequest(`/planner/plans/${planId}`);
|
|
240
|
+
return {
|
|
241
|
+
id: plan.id,
|
|
242
|
+
title: plan.title,
|
|
243
|
+
owner: plan.owner,
|
|
244
|
+
createdDateTime: plan.createdDateTime,
|
|
245
|
+
createdBy: plan.createdBy?.user,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// Buckets
|
|
250
|
+
// ============================================================================
|
|
251
|
+
export async function listBuckets(params) {
|
|
252
|
+
const { planId } = params;
|
|
253
|
+
const buckets = await graphList(`/planner/plans/${planId}/buckets`);
|
|
254
|
+
return buckets.map((b) => ({
|
|
255
|
+
id: b.id,
|
|
256
|
+
name: b.name,
|
|
257
|
+
planId: b.planId,
|
|
258
|
+
orderHint: b.orderHint,
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
export async function createBucket(params) {
|
|
262
|
+
const { planId, name } = params;
|
|
263
|
+
const bucket = await graphRequest('/planner/buckets', {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
body: { planId, name },
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
bucketId: bucket.id,
|
|
270
|
+
name: bucket.name,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
export async function updateBucket(params) {
|
|
274
|
+
const { bucketId, name } = params;
|
|
275
|
+
// Fetch ETag first
|
|
276
|
+
const etag = await getETag(`/planner/buckets/${bucketId}`);
|
|
277
|
+
const bucket = await graphRequest(`/planner/buckets/${bucketId}`, {
|
|
278
|
+
method: 'PATCH',
|
|
279
|
+
body: { name },
|
|
280
|
+
headers: { 'If-Match': etag },
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
success: true,
|
|
284
|
+
bucketId: bucket.id,
|
|
285
|
+
name: bucket.name,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export async function deleteBucket(params) {
|
|
289
|
+
const { bucketId } = params;
|
|
290
|
+
// Fetch ETag first
|
|
291
|
+
const etag = await getETag(`/planner/buckets/${bucketId}`);
|
|
292
|
+
await graphRequest(`/planner/buckets/${bucketId}`, {
|
|
293
|
+
method: 'DELETE',
|
|
294
|
+
headers: { 'If-Match': etag },
|
|
295
|
+
});
|
|
296
|
+
return { success: true, message: 'Bucket deleted' };
|
|
297
|
+
}
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Tasks
|
|
300
|
+
// ============================================================================
|
|
301
|
+
export async function listPlannerTasks(params) {
|
|
302
|
+
const { planId, bucketId, maxItems = 100 } = params;
|
|
303
|
+
let tasks = await graphList(`/planner/plans/${planId}/tasks`, { maxItems });
|
|
304
|
+
// Filter by bucket if specified
|
|
305
|
+
if (bucketId) {
|
|
306
|
+
tasks = tasks.filter((t) => t.bucketId === bucketId);
|
|
307
|
+
}
|
|
308
|
+
return tasks.map((t) => ({
|
|
309
|
+
id: t.id,
|
|
310
|
+
title: t.title,
|
|
311
|
+
bucketId: t.bucketId,
|
|
312
|
+
progress: progressToSemantic(t.percentComplete),
|
|
313
|
+
percentComplete: t.percentComplete,
|
|
314
|
+
priority: PRIORITY_REVERSE[t.priority] || 'medium',
|
|
315
|
+
dueDateTime: t.dueDateTime,
|
|
316
|
+
startDateTime: t.startDateTime,
|
|
317
|
+
assignedTo: Object.keys(t.assignments || {}),
|
|
318
|
+
createdDateTime: t.createdDateTime,
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
export async function getPlannerTask(params) {
|
|
322
|
+
const { taskId } = params;
|
|
323
|
+
const task = await graphRequest(`/planner/tasks/${taskId}`);
|
|
324
|
+
return {
|
|
325
|
+
id: task.id,
|
|
326
|
+
planId: task.planId,
|
|
327
|
+
bucketId: task.bucketId,
|
|
328
|
+
title: task.title,
|
|
329
|
+
progress: progressToSemantic(task.percentComplete),
|
|
330
|
+
percentComplete: task.percentComplete,
|
|
331
|
+
priority: PRIORITY_REVERSE[task.priority] || 'medium',
|
|
332
|
+
dueDateTime: task.dueDateTime,
|
|
333
|
+
startDateTime: task.startDateTime,
|
|
334
|
+
assignedTo: Object.keys(task.assignments || {}),
|
|
335
|
+
createdDateTime: task.createdDateTime,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
export async function createPlannerTask(params) {
|
|
339
|
+
const { planId, title, bucketId, assignments, dueDateTime, startDateTime, priority, progress } = params;
|
|
340
|
+
const body = {
|
|
341
|
+
planId,
|
|
342
|
+
title,
|
|
343
|
+
};
|
|
344
|
+
if (bucketId)
|
|
345
|
+
body.bucketId = bucketId;
|
|
346
|
+
if (dueDateTime)
|
|
347
|
+
body.dueDateTime = dueDateTime;
|
|
348
|
+
if (startDateTime)
|
|
349
|
+
body.startDateTime = startDateTime;
|
|
350
|
+
if (priority)
|
|
351
|
+
body.priority = PRIORITY_MAP[priority];
|
|
352
|
+
if (progress)
|
|
353
|
+
body.percentComplete = PROGRESS_MAP[progress];
|
|
354
|
+
// Resolve assignments to user IDs
|
|
355
|
+
if (assignments && assignments.length > 0) {
|
|
356
|
+
const assignmentObj = {};
|
|
357
|
+
for (const email of assignments) {
|
|
358
|
+
const userId = await resolveUserId(email);
|
|
359
|
+
assignmentObj[userId] = {
|
|
360
|
+
'@odata.type': '#microsoft.graph.plannerAssignment',
|
|
361
|
+
orderHint: ' !',
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
body.assignments = assignmentObj;
|
|
365
|
+
}
|
|
366
|
+
const task = await graphRequest('/planner/tasks', {
|
|
367
|
+
method: 'POST',
|
|
368
|
+
body,
|
|
369
|
+
});
|
|
370
|
+
return {
|
|
371
|
+
success: true,
|
|
372
|
+
taskId: task.id,
|
|
373
|
+
title: task.title,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
export async function updatePlannerTask(params) {
|
|
377
|
+
const { taskId, title, bucketId, assignments, clearAssignments, dueDateTime, clearDue, startDateTime, priority, progress } = params;
|
|
378
|
+
// Fetch task to get ETag and current assignments (if clearing)
|
|
379
|
+
const currentTask = await graphRequest(`/planner/tasks/${taskId}`);
|
|
380
|
+
const etag = currentTask['@odata.etag'];
|
|
381
|
+
const body = {};
|
|
382
|
+
if (title)
|
|
383
|
+
body.title = title;
|
|
384
|
+
if (bucketId)
|
|
385
|
+
body.bucketId = bucketId;
|
|
386
|
+
if (clearDue) {
|
|
387
|
+
body.dueDateTime = null;
|
|
388
|
+
}
|
|
389
|
+
else if (dueDateTime) {
|
|
390
|
+
body.dueDateTime = dueDateTime;
|
|
391
|
+
}
|
|
392
|
+
if (startDateTime)
|
|
393
|
+
body.startDateTime = startDateTime;
|
|
394
|
+
if (priority)
|
|
395
|
+
body.priority = PRIORITY_MAP[priority];
|
|
396
|
+
if (progress)
|
|
397
|
+
body.percentComplete = PROGRESS_MAP[progress];
|
|
398
|
+
// Handle assignments
|
|
399
|
+
if (clearAssignments) {
|
|
400
|
+
// Set all current assignments to null to remove them
|
|
401
|
+
const assignmentObj = {};
|
|
402
|
+
const currentAssignments = currentTask.assignments || {};
|
|
403
|
+
for (const userId of Object.keys(currentAssignments)) {
|
|
404
|
+
assignmentObj[userId] = null;
|
|
405
|
+
}
|
|
406
|
+
if (Object.keys(assignmentObj).length > 0) {
|
|
407
|
+
body.assignments = assignmentObj;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else if (assignments) {
|
|
411
|
+
// Resolve assignments to user IDs
|
|
412
|
+
const assignmentObj = {};
|
|
413
|
+
for (const email of assignments) {
|
|
414
|
+
const userId = await resolveUserId(email);
|
|
415
|
+
assignmentObj[userId] = {
|
|
416
|
+
'@odata.type': '#microsoft.graph.plannerAssignment',
|
|
417
|
+
orderHint: ' !',
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
body.assignments = assignmentObj;
|
|
421
|
+
}
|
|
422
|
+
const task = await graphRequest(`/planner/tasks/${taskId}`, {
|
|
423
|
+
method: 'PATCH',
|
|
424
|
+
body,
|
|
425
|
+
headers: { 'If-Match': etag },
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
success: true,
|
|
429
|
+
taskId: task.id,
|
|
430
|
+
title: task.title,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
export async function deletePlannerTask(params) {
|
|
434
|
+
const { taskId } = params;
|
|
435
|
+
// Fetch ETag first
|
|
436
|
+
const etag = await getETag(`/planner/tasks/${taskId}`);
|
|
437
|
+
await graphRequest(`/planner/tasks/${taskId}`, {
|
|
438
|
+
method: 'DELETE',
|
|
439
|
+
headers: { 'If-Match': etag },
|
|
440
|
+
});
|
|
441
|
+
return { success: true, message: 'Task deleted' };
|
|
442
|
+
}
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// Task Details
|
|
445
|
+
// ============================================================================
|
|
446
|
+
export async function getPlannerTaskDetails(params) {
|
|
447
|
+
const { taskId } = params;
|
|
448
|
+
const details = await graphRequest(`/planner/tasks/${taskId}/details`);
|
|
449
|
+
// Transform checklist to array for easier consumption
|
|
450
|
+
const checklistItems = Object.entries(details.checklist || {}).map(([id, item]) => ({
|
|
451
|
+
id,
|
|
452
|
+
title: item.title,
|
|
453
|
+
isChecked: item.isChecked,
|
|
454
|
+
}));
|
|
455
|
+
// Transform references to array
|
|
456
|
+
const referenceItems = Object.entries(details.references || {}).map(([url, ref]) => ({
|
|
457
|
+
url: decodeURIComponent(url),
|
|
458
|
+
alias: ref.alias,
|
|
459
|
+
type: ref.type,
|
|
460
|
+
}));
|
|
461
|
+
return {
|
|
462
|
+
taskId,
|
|
463
|
+
description: details.description,
|
|
464
|
+
checklist: checklistItems,
|
|
465
|
+
references: referenceItems,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
export async function updatePlannerTaskDetails(params) {
|
|
469
|
+
const { taskId, description, checklist } = params;
|
|
470
|
+
// Fetch ETag first
|
|
471
|
+
const etag = await getETag(`/planner/tasks/${taskId}/details`);
|
|
472
|
+
const body = {};
|
|
473
|
+
if (description !== undefined) {
|
|
474
|
+
body.description = description;
|
|
475
|
+
}
|
|
476
|
+
// Transform checklist array to object format
|
|
477
|
+
if (checklist) {
|
|
478
|
+
const checklistObj = {};
|
|
479
|
+
checklist.forEach((item, index) => {
|
|
480
|
+
// Generate a simple GUID-like key for new items
|
|
481
|
+
const key = crypto.randomUUID();
|
|
482
|
+
checklistObj[key] = {
|
|
483
|
+
'@odata.type': 'microsoft.graph.plannerChecklistItem',
|
|
484
|
+
title: item.title,
|
|
485
|
+
isChecked: item.isChecked ?? false,
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
body.checklist = checklistObj;
|
|
489
|
+
}
|
|
490
|
+
await graphRequest(`/planner/tasks/${taskId}/details`, {
|
|
491
|
+
method: 'PATCH',
|
|
492
|
+
body,
|
|
493
|
+
headers: { 'If-Match': etag },
|
|
494
|
+
});
|
|
495
|
+
return { success: true, message: 'Task details updated' };
|
|
496
|
+
}
|
|
497
|
+
// ============================================================================
|
|
498
|
+
// Checklist Operations
|
|
499
|
+
// ============================================================================
|
|
500
|
+
export async function addChecklistItem(params) {
|
|
501
|
+
const { taskId, title, isChecked = false } = params;
|
|
502
|
+
// Fetch ETag first
|
|
503
|
+
const etag = await getETag(`/planner/tasks/${taskId}/details`);
|
|
504
|
+
// Generate a new item ID
|
|
505
|
+
const itemId = crypto.randomUUID();
|
|
506
|
+
const body = {
|
|
507
|
+
checklist: {
|
|
508
|
+
[itemId]: {
|
|
509
|
+
'@odata.type': 'microsoft.graph.plannerChecklistItem',
|
|
510
|
+
title,
|
|
511
|
+
isChecked,
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
await graphRequest(`/planner/tasks/${taskId}/details`, {
|
|
516
|
+
method: 'PATCH',
|
|
517
|
+
body,
|
|
518
|
+
headers: { 'If-Match': etag },
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
success: true,
|
|
522
|
+
message: 'Checklist item added',
|
|
523
|
+
itemId,
|
|
524
|
+
title,
|
|
525
|
+
isChecked,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
export async function removeChecklistItem(params) {
|
|
529
|
+
const { taskId, itemId } = params;
|
|
530
|
+
// Fetch ETag first
|
|
531
|
+
const etag = await getETag(`/planner/tasks/${taskId}/details`);
|
|
532
|
+
// Set to null to remove
|
|
533
|
+
const body = {
|
|
534
|
+
checklist: {
|
|
535
|
+
[itemId]: null,
|
|
536
|
+
},
|
|
537
|
+
};
|
|
538
|
+
await graphRequest(`/planner/tasks/${taskId}/details`, {
|
|
539
|
+
method: 'PATCH',
|
|
540
|
+
body,
|
|
541
|
+
headers: { 'If-Match': etag },
|
|
542
|
+
});
|
|
543
|
+
return {
|
|
544
|
+
success: true,
|
|
545
|
+
message: 'Checklist item removed',
|
|
546
|
+
itemId,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
export async function toggleChecklistItem(params) {
|
|
550
|
+
const { taskId, itemId } = params;
|
|
551
|
+
// Fetch current details to get the current state
|
|
552
|
+
const details = await graphRequest(`/planner/tasks/${taskId}/details`);
|
|
553
|
+
const etag = details['@odata.etag'];
|
|
554
|
+
if (!etag) {
|
|
555
|
+
throw new Error('Could not get ETag for task details');
|
|
556
|
+
}
|
|
557
|
+
const currentItem = details.checklist?.[itemId];
|
|
558
|
+
if (!currentItem) {
|
|
559
|
+
throw new Error(`Checklist item ${itemId} not found`);
|
|
560
|
+
}
|
|
561
|
+
const newCheckedState = !currentItem.isChecked;
|
|
562
|
+
const body = {
|
|
563
|
+
checklist: {
|
|
564
|
+
[itemId]: {
|
|
565
|
+
'@odata.type': 'microsoft.graph.plannerChecklistItem',
|
|
566
|
+
title: currentItem.title,
|
|
567
|
+
isChecked: newCheckedState,
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
await graphRequest(`/planner/tasks/${taskId}/details`, {
|
|
572
|
+
method: 'PATCH',
|
|
573
|
+
body,
|
|
574
|
+
headers: { 'If-Match': etag },
|
|
575
|
+
});
|
|
576
|
+
return {
|
|
577
|
+
success: true,
|
|
578
|
+
message: `Checklist item ${newCheckedState ? 'checked' : 'unchecked'}`,
|
|
579
|
+
itemId,
|
|
580
|
+
title: currentItem.title,
|
|
581
|
+
isChecked: newCheckedState,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
// ============================================================================
|
|
585
|
+
// References (Attachments)
|
|
586
|
+
// ============================================================================
|
|
587
|
+
export async function addPlannerTaskReference(params) {
|
|
588
|
+
const { taskId, url, alias, type } = params;
|
|
589
|
+
// Fetch ETag first
|
|
590
|
+
const etag = await getETag(`/planner/tasks/${taskId}/details`);
|
|
591
|
+
// Encode URL for use as object key
|
|
592
|
+
const encodedUrl = encodeReferenceUrl(url);
|
|
593
|
+
// Auto-detect type if not provided
|
|
594
|
+
const referenceType = type || detectReferenceType(url);
|
|
595
|
+
// Build reference object
|
|
596
|
+
const reference = {
|
|
597
|
+
'@odata.type': 'microsoft.graph.plannerExternalReference',
|
|
598
|
+
type: referenceType,
|
|
599
|
+
previewPriority: ' !',
|
|
600
|
+
};
|
|
601
|
+
if (alias) {
|
|
602
|
+
reference.alias = alias;
|
|
603
|
+
}
|
|
604
|
+
const body = {
|
|
605
|
+
references: {
|
|
606
|
+
[encodedUrl]: reference,
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
await graphRequest(`/planner/tasks/${taskId}/details`, {
|
|
610
|
+
method: 'PATCH',
|
|
611
|
+
body,
|
|
612
|
+
headers: { 'If-Match': etag },
|
|
613
|
+
});
|
|
614
|
+
return {
|
|
615
|
+
success: true,
|
|
616
|
+
message: 'Reference added to task',
|
|
617
|
+
url,
|
|
618
|
+
alias: alias || url,
|
|
619
|
+
type: referenceType,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
export async function removePlannerTaskReference(params) {
|
|
623
|
+
const { taskId, url } = params;
|
|
624
|
+
// Fetch ETag first
|
|
625
|
+
const etag = await getETag(`/planner/tasks/${taskId}/details`);
|
|
626
|
+
// Encode URL for use as object key
|
|
627
|
+
const encodedUrl = encodeReferenceUrl(url);
|
|
628
|
+
// Set to null to remove
|
|
629
|
+
const body = {
|
|
630
|
+
references: {
|
|
631
|
+
[encodedUrl]: null,
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
await graphRequest(`/planner/tasks/${taskId}/details`, {
|
|
635
|
+
method: 'PATCH',
|
|
636
|
+
body,
|
|
637
|
+
headers: { 'If-Match': etag },
|
|
638
|
+
});
|
|
639
|
+
return {
|
|
640
|
+
success: true,
|
|
641
|
+
message: 'Reference removed from task',
|
|
642
|
+
url,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
export async function uploadAndAttach(params) {
|
|
646
|
+
const { taskId, localPath, remotePath, alias } = params;
|
|
647
|
+
const filename = basename(localPath);
|
|
648
|
+
// Step 1: Get task to find planId
|
|
649
|
+
const task = await graphRequest(`/planner/tasks/${taskId}`);
|
|
650
|
+
// Step 2: Get plan to find group (owner) and plan title
|
|
651
|
+
const plan = await graphRequest(`/planner/plans/${task.planId}`);
|
|
652
|
+
const groupId = plan.owner;
|
|
653
|
+
const planTitle = plan.title.replace(/[/\\?%*:|"<>]/g, '-'); // Sanitize for folder name
|
|
654
|
+
// Step 3: Get the group's default drive (SharePoint document library)
|
|
655
|
+
const drive = await graphRequest(`/groups/${groupId}/drive`);
|
|
656
|
+
// Step 4: Read local file
|
|
657
|
+
const content = await readFile(localPath);
|
|
658
|
+
// Step 5: Determine destination path in SharePoint
|
|
659
|
+
const destPath = remotePath || `Planner Attachments/${planTitle}/${filename}`;
|
|
660
|
+
// Step 6: Upload to group's SharePoint
|
|
661
|
+
const MAX_SIMPLE_UPLOAD = 4 * 1024 * 1024;
|
|
662
|
+
let uploaded;
|
|
663
|
+
if (content.length <= MAX_SIMPLE_UPLOAD) {
|
|
664
|
+
uploaded = await graphUpload(`/drives/${drive.id}/root:/${destPath}:/content`, content);
|
|
665
|
+
}
|
|
666
|
+
else {
|
|
667
|
+
uploaded = await graphUploadLarge(`/drives/${drive.id}/root:/${destPath}`, content);
|
|
668
|
+
}
|
|
669
|
+
// Step 7: Attach the uploaded file's URL to the task
|
|
670
|
+
const attachResult = await addPlannerTaskReference({
|
|
671
|
+
taskId,
|
|
672
|
+
url: uploaded.webUrl,
|
|
673
|
+
alias: alias || filename,
|
|
674
|
+
});
|
|
675
|
+
return {
|
|
676
|
+
success: true,
|
|
677
|
+
message: 'File uploaded to SharePoint and attached to task',
|
|
678
|
+
file: {
|
|
679
|
+
name: uploaded.name,
|
|
680
|
+
size: uploaded.size,
|
|
681
|
+
webUrl: uploaded.webUrl,
|
|
682
|
+
},
|
|
683
|
+
attachment: {
|
|
684
|
+
alias: alias || filename,
|
|
685
|
+
type: attachResult.type,
|
|
686
|
+
},
|
|
687
|
+
location: {
|
|
688
|
+
sharePointSite: drive.webUrl,
|
|
689
|
+
path: destPath,
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=planner.js.map
|