@dashflow/ms365-mcp-server 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/.env.example +54 -0
- package/.releaserc.json +9 -0
- package/Dockerfile +22 -0
- package/LICENSE +21 -0
- package/README.md +590 -0
- package/bin/generate-graph-client.mjs +59 -0
- package/bin/modules/download-openapi.mjs +40 -0
- package/bin/modules/extract-descriptions.mjs +48 -0
- package/bin/modules/generate-mcp-tools.mjs +46 -0
- package/bin/modules/simplified-openapi.mjs +694 -0
- package/dist/auth-tools.js +202 -0
- package/dist/auth.js +422 -0
- package/dist/cli.js +78 -0
- package/dist/cloud-config.js +49 -0
- package/dist/endpoints.json +596 -0
- package/dist/generated/endpoint-types.js +0 -0
- package/dist/generated/hack.js +42 -0
- package/dist/graph-client.js +208 -0
- package/dist/graph-tools.js +401 -0
- package/dist/index.js +76 -0
- package/dist/lib/microsoft-auth.js +73 -0
- package/dist/logger.js +42 -0
- package/dist/oauth-provider.js +51 -0
- package/dist/request-context.js +9 -0
- package/dist/secrets.js +68 -0
- package/dist/server.js +387 -0
- package/dist/tool-categories.js +93 -0
- package/dist/version.js +10 -0
- package/eslint.config.js +43 -0
- package/glama.json +4 -0
- package/package.json +79 -0
- package/remove-recursive-refs.js +294 -0
- package/src/endpoints.json +596 -0
- package/src/generated/README.md +56 -0
- package/src/generated/endpoint-types.ts +27 -0
- package/src/generated/hack.ts +49 -0
- package/test-calendar-fix.js +62 -0
- package/test-real-calendar.js +96 -0
- package/tsup.config.ts +30 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
|
|
4
|
+
export function createAndSaveSimplifiedOpenAPI(endpointsFile, openapiFile, openapiTrimmedFile) {
|
|
5
|
+
const allEndpoints = JSON.parse(fs.readFileSync(endpointsFile, 'utf8'));
|
|
6
|
+
const endpoints = allEndpoints.filter((endpoint) => !endpoint.disabled);
|
|
7
|
+
|
|
8
|
+
const spec = fs.readFileSync(openapiFile, 'utf8');
|
|
9
|
+
const openApiSpec = yaml.load(spec);
|
|
10
|
+
|
|
11
|
+
for (const endpoint of endpoints) {
|
|
12
|
+
if (!openApiSpec.paths[endpoint.pathPattern]) {
|
|
13
|
+
throw new Error(`Path "${endpoint.pathPattern}" not found in OpenAPI spec.`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const [key, value] of Object.entries(openApiSpec.paths)) {
|
|
18
|
+
const e = endpoints.filter((ep) => ep.pathPattern === key);
|
|
19
|
+
if (e.length === 0) {
|
|
20
|
+
delete openApiSpec.paths[key];
|
|
21
|
+
} else {
|
|
22
|
+
for (const [method, operation] of Object.entries(value)) {
|
|
23
|
+
const eo = e.find((ep) => ep.method.toLowerCase() === method);
|
|
24
|
+
if (eo) {
|
|
25
|
+
operation.operationId = eo.toolName;
|
|
26
|
+
if (!operation.description && operation.summary) {
|
|
27
|
+
operation.description = operation.summary;
|
|
28
|
+
}
|
|
29
|
+
if (operation.parameters) {
|
|
30
|
+
operation.parameters = operation.parameters.map((param) => {
|
|
31
|
+
if (param.$ref && param.$ref.startsWith('#/components/parameters/')) {
|
|
32
|
+
const paramName = param.$ref.replace('#/components/parameters/', '');
|
|
33
|
+
const resolvedParam = openApiSpec.components?.parameters?.[paramName];
|
|
34
|
+
if (resolvedParam) {
|
|
35
|
+
return { ...resolvedParam };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return param;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
delete value[method];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (openApiSpec.components && openApiSpec.components.schemas) {
|
|
49
|
+
removeODataTypeRecursively(openApiSpec.components.schemas);
|
|
50
|
+
flattenComplexSchemasRecursively(openApiSpec.components.schemas);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (openApiSpec.paths) {
|
|
54
|
+
removeODataTypeRecursively(openApiSpec.paths);
|
|
55
|
+
simplifyAnyOfInPaths(openApiSpec.paths);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('🧹 Pruning unused schemas...');
|
|
59
|
+
const usedSchemas = findUsedSchemas(openApiSpec);
|
|
60
|
+
pruneUnusedSchemas(openApiSpec, usedSchemas);
|
|
61
|
+
|
|
62
|
+
fs.writeFileSync(openapiTrimmedFile, yaml.dump(openApiSpec));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function removeODataTypeRecursively(obj) {
|
|
66
|
+
if (!obj || typeof obj !== 'object') return;
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(obj)) {
|
|
69
|
+
obj.forEach((item) => removeODataTypeRecursively(item));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Object.keys(obj).forEach((key) => {
|
|
74
|
+
if (key === '@odata.type') {
|
|
75
|
+
delete obj[key];
|
|
76
|
+
} else {
|
|
77
|
+
removeODataTypeRecursively(obj[key]);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function simplifyAnyOfInPaths(paths) {
|
|
83
|
+
Object.entries(paths).forEach(([pathKey, pathItem]) => {
|
|
84
|
+
if (!pathItem || typeof pathItem !== 'object') return;
|
|
85
|
+
|
|
86
|
+
Object.entries(pathItem).forEach(([method, operation]) => {
|
|
87
|
+
if (!operation || typeof operation !== 'object') return;
|
|
88
|
+
|
|
89
|
+
if (operation.parameters && Array.isArray(operation.parameters)) {
|
|
90
|
+
operation.parameters.forEach((param) => {
|
|
91
|
+
if (param.schema && param.schema.anyOf) {
|
|
92
|
+
simplifyAnyOfSchema(param.schema, `Path ${pathKey} ${method} parameter`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (operation.requestBody && operation.requestBody.content) {
|
|
98
|
+
Object.entries(operation.requestBody.content).forEach(([mediaType, mediaTypeObj]) => {
|
|
99
|
+
if (mediaTypeObj.schema && mediaTypeObj.schema.anyOf) {
|
|
100
|
+
simplifyAnyOfSchema(
|
|
101
|
+
mediaTypeObj.schema,
|
|
102
|
+
`Path ${pathKey} ${method} requestBody ${mediaType}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (operation.responses) {
|
|
109
|
+
Object.entries(operation.responses).forEach(([statusCode, response]) => {
|
|
110
|
+
if (response.content) {
|
|
111
|
+
Object.entries(response.content).forEach(([mediaType, mediaTypeObj]) => {
|
|
112
|
+
if (mediaTypeObj.schema && mediaTypeObj.schema.anyOf) {
|
|
113
|
+
simplifyAnyOfSchema(
|
|
114
|
+
mediaTypeObj.schema,
|
|
115
|
+
`Path ${pathKey} ${method} response ${statusCode} ${mediaType}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function simplifyAnyOfSchema(schema, context) {
|
|
127
|
+
if (!schema.anyOf || !Array.isArray(schema.anyOf)) return;
|
|
128
|
+
|
|
129
|
+
const anyOfItems = schema.anyOf;
|
|
130
|
+
|
|
131
|
+
if (anyOfItems.length === 2) {
|
|
132
|
+
const hasRef = anyOfItems.some((item) => item.$ref);
|
|
133
|
+
const hasNullableObject = anyOfItems.some(
|
|
134
|
+
(item) => item.type === 'object' && item.nullable === true && Object.keys(item).length <= 2
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (hasRef && hasNullableObject) {
|
|
138
|
+
console.log(`Simplifying anyOf in ${context} (ref + nullable object pattern)`);
|
|
139
|
+
const refItem = anyOfItems.find((item) => item.$ref);
|
|
140
|
+
delete schema.anyOf;
|
|
141
|
+
schema.$ref = refItem.$ref;
|
|
142
|
+
schema.nullable = true;
|
|
143
|
+
}
|
|
144
|
+
} else if (anyOfItems.length > 2) {
|
|
145
|
+
console.log(`Simplifying anyOf in ${context} (multiple options)`);
|
|
146
|
+
schema.type = anyOfItems[0].type || 'object';
|
|
147
|
+
schema.nullable = true;
|
|
148
|
+
schema.description = `${schema.description || ''} [Simplified from ${
|
|
149
|
+
anyOfItems.length
|
|
150
|
+
} options]`.trim();
|
|
151
|
+
delete schema.anyOf;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function flattenComplexSchemasRecursively(schemas) {
|
|
156
|
+
Object.entries(schemas).forEach(([schemaName, schema]) => {
|
|
157
|
+
if (!schema || typeof schema !== 'object') return;
|
|
158
|
+
|
|
159
|
+
flattenComplexSchema(schema, schemaName);
|
|
160
|
+
|
|
161
|
+
if (schema.allOf) {
|
|
162
|
+
const flattenedSchema = mergeAllOfSchemas(schema.allOf, schemas);
|
|
163
|
+
Object.assign(schema, flattenedSchema);
|
|
164
|
+
delete schema.allOf;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (schema.properties && shouldReduceProperties(schema)) {
|
|
168
|
+
reduceProperties(schema, schemaName);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (schema.properties) {
|
|
172
|
+
simplifyNestedPropertiesRecursively(schema.properties);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function flattenComplexSchema(schema, schemaName) {
|
|
178
|
+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
179
|
+
if (schema.anyOf.length === 2) {
|
|
180
|
+
const hasRef = schema.anyOf.some((item) => item.$ref);
|
|
181
|
+
const hasNullableObject = schema.anyOf.some(
|
|
182
|
+
(item) => item.type === 'object' && item.nullable === true && Object.keys(item).length <= 2
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (hasRef && hasNullableObject) {
|
|
186
|
+
console.log(`Simplifying anyOf in ${schemaName} (ref + nullable object pattern)`);
|
|
187
|
+
const refItem = schema.anyOf.find((item) => item.$ref);
|
|
188
|
+
delete schema.anyOf;
|
|
189
|
+
schema.$ref = refItem.$ref;
|
|
190
|
+
schema.nullable = true;
|
|
191
|
+
}
|
|
192
|
+
} else if (schema.anyOf.length > 2) {
|
|
193
|
+
console.log(`Simplifying anyOf in ${schemaName} (${schema.anyOf.length} options)`);
|
|
194
|
+
const firstOption = schema.anyOf[0];
|
|
195
|
+
schema.type = firstOption.type || 'object';
|
|
196
|
+
schema.nullable = true;
|
|
197
|
+
schema.description = `${schema.description || ''} [Simplified from ${
|
|
198
|
+
schema.anyOf.length
|
|
199
|
+
} options]`.trim();
|
|
200
|
+
delete schema.anyOf;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 2) {
|
|
205
|
+
console.log(`Simplifying oneOf in ${schemaName} (${schema.oneOf.length} options)`);
|
|
206
|
+
const firstOption = schema.oneOf[0];
|
|
207
|
+
schema.type = firstOption.type || 'object';
|
|
208
|
+
schema.nullable = true;
|
|
209
|
+
schema.description = `${schema.description || ''} [Simplified from ${
|
|
210
|
+
schema.oneOf.length
|
|
211
|
+
} options]`.trim();
|
|
212
|
+
delete schema.oneOf;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function shouldReduceProperties(schema) {
|
|
217
|
+
if (!schema.properties) return false;
|
|
218
|
+
const propertyCount = Object.keys(schema.properties).length;
|
|
219
|
+
return propertyCount > 25;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function reduceProperties(schema, schemaName) {
|
|
223
|
+
const properties = schema.properties;
|
|
224
|
+
const propertyCount = Object.keys(properties).length;
|
|
225
|
+
|
|
226
|
+
if (propertyCount > 25) {
|
|
227
|
+
console.log(`Reducing properties in ${schemaName} (${propertyCount} -> 25)`);
|
|
228
|
+
|
|
229
|
+
const priorityProperties = [
|
|
230
|
+
'id',
|
|
231
|
+
'name',
|
|
232
|
+
'displayName',
|
|
233
|
+
'description',
|
|
234
|
+
'createdDateTime',
|
|
235
|
+
'lastModifiedDateTime',
|
|
236
|
+
'status',
|
|
237
|
+
'state',
|
|
238
|
+
'type',
|
|
239
|
+
'value',
|
|
240
|
+
'email',
|
|
241
|
+
'userPrincipalName',
|
|
242
|
+
'title',
|
|
243
|
+
'content',
|
|
244
|
+
'body',
|
|
245
|
+
'subject',
|
|
246
|
+
'message',
|
|
247
|
+
'attachments',
|
|
248
|
+
'error',
|
|
249
|
+
'code',
|
|
250
|
+
'details',
|
|
251
|
+
'url',
|
|
252
|
+
'href',
|
|
253
|
+
'path',
|
|
254
|
+
'method',
|
|
255
|
+
'enabled',
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const keptProperties = {};
|
|
259
|
+
const propertyKeys = Object.keys(properties);
|
|
260
|
+
|
|
261
|
+
priorityProperties.forEach((key) => {
|
|
262
|
+
if (properties[key]) {
|
|
263
|
+
keptProperties[key] = properties[key];
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const remainingSlots = 25 - Object.keys(keptProperties).length;
|
|
268
|
+
const otherKeys = propertyKeys.filter((key) => !keptProperties[key]);
|
|
269
|
+
|
|
270
|
+
otherKeys.slice(0, remainingSlots).forEach((key) => {
|
|
271
|
+
keptProperties[key] = properties[key];
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
schema.properties = keptProperties;
|
|
275
|
+
schema.additionalProperties = true;
|
|
276
|
+
schema.description = `${
|
|
277
|
+
schema.description || ''
|
|
278
|
+
} [Note: Simplified from ${propertyCount} properties to 25 most common ones]`.trim();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function mergeAllOfSchemas(allOfArray, allSchemas, visited = new Set()) {
|
|
283
|
+
const merged = {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
allOfArray.forEach((item) => {
|
|
289
|
+
if (item.$ref) {
|
|
290
|
+
const refSchemaName = item.$ref.replace('#/components/schemas/', '');
|
|
291
|
+
|
|
292
|
+
if (visited.has(refSchemaName)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
visited.add(refSchemaName);
|
|
296
|
+
|
|
297
|
+
const refSchema = allSchemas[refSchemaName];
|
|
298
|
+
if (refSchema) {
|
|
299
|
+
console.log(
|
|
300
|
+
`Processing ref ${refSchemaName} for ${item.title}, exists: true, has properties: ${!!refSchema.properties}, has allOf: ${!!refSchema.allOf}`
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
if (refSchema.allOf) {
|
|
304
|
+
const nestedMerged = mergeAllOfSchemas(refSchema.allOf, allSchemas, new Set(visited));
|
|
305
|
+
Object.assign(merged.properties, nestedMerged.properties);
|
|
306
|
+
if (nestedMerged.required) {
|
|
307
|
+
merged.required = [...(merged.required || []), ...nestedMerged.required];
|
|
308
|
+
}
|
|
309
|
+
if (nestedMerged.description && !merged.description) {
|
|
310
|
+
merged.description = nestedMerged.description;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (refSchema.properties) {
|
|
315
|
+
console.log(`Ensuring ${item.title} has all required properties from ${refSchemaName}`);
|
|
316
|
+
Object.assign(merged.properties, refSchema.properties);
|
|
317
|
+
}
|
|
318
|
+
if (refSchema.required) {
|
|
319
|
+
merged.required = [...(merged.required || []), ...refSchema.required];
|
|
320
|
+
}
|
|
321
|
+
if (refSchema.description && !merged.description) {
|
|
322
|
+
merged.description = refSchema.description;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} else if (item.properties) {
|
|
326
|
+
Object.assign(merged.properties, item.properties);
|
|
327
|
+
if (item.required) {
|
|
328
|
+
merged.required = [...(merged.required || []), ...item.required];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (merged.required) {
|
|
334
|
+
merged.required = [...new Set(merged.required)];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return merged;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function simplifyNestedPropertiesRecursively(properties, currentDepth = 0, maxDepth = 3) {
|
|
341
|
+
if (!properties || typeof properties !== 'object' || currentDepth >= maxDepth) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
Object.keys(properties).forEach((key) => {
|
|
346
|
+
const prop = properties[key];
|
|
347
|
+
|
|
348
|
+
if (prop && typeof prop === 'object') {
|
|
349
|
+
if (currentDepth === maxDepth - 1 && prop.properties) {
|
|
350
|
+
console.log(`Flattening nested property at depth ${currentDepth}: ${key}`);
|
|
351
|
+
prop.type = 'object';
|
|
352
|
+
prop.description = `${prop.description || ''} [Simplified: nested object]`.trim();
|
|
353
|
+
delete prop.properties;
|
|
354
|
+
delete prop.additionalProperties;
|
|
355
|
+
} else if (prop.properties) {
|
|
356
|
+
simplifyNestedPropertiesRecursively(prop.properties, currentDepth + 1, maxDepth);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (prop.anyOf && Array.isArray(prop.anyOf)) {
|
|
360
|
+
if (prop.anyOf.length === 2) {
|
|
361
|
+
const hasRef = prop.anyOf.some((item) => item.$ref);
|
|
362
|
+
const hasNullableObject = prop.anyOf.some(
|
|
363
|
+
(item) =>
|
|
364
|
+
item.type === 'object' && item.nullable === true && Object.keys(item).length <= 2
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
if (hasRef && hasNullableObject) {
|
|
368
|
+
console.log(`Simplifying anyOf in property ${key} (ref + nullable object pattern)`);
|
|
369
|
+
const refItem = prop.anyOf.find((item) => item.$ref);
|
|
370
|
+
delete prop.anyOf;
|
|
371
|
+
prop.$ref = refItem.$ref;
|
|
372
|
+
prop.nullable = true;
|
|
373
|
+
}
|
|
374
|
+
} else if (prop.anyOf.length > 2) {
|
|
375
|
+
prop.type = prop.anyOf[0].type || 'object';
|
|
376
|
+
prop.nullable = true;
|
|
377
|
+
prop.description =
|
|
378
|
+
`${prop.description || ''} [Simplified from ${prop.anyOf.length} options]`.trim();
|
|
379
|
+
delete prop.anyOf;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (prop.oneOf && Array.isArray(prop.oneOf) && prop.oneOf.length > 2) {
|
|
384
|
+
prop.type = prop.oneOf[0].type || 'object';
|
|
385
|
+
prop.nullable = true;
|
|
386
|
+
prop.description =
|
|
387
|
+
`${prop.description || ''} [Simplified from ${prop.oneOf.length} options]`.trim();
|
|
388
|
+
delete prop.oneOf;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function findUsedSchemas(openApiSpec) {
|
|
395
|
+
const usedSchemas = new Set();
|
|
396
|
+
const schemasToProcess = [];
|
|
397
|
+
const schemas = openApiSpec.components?.schemas || {};
|
|
398
|
+
const responses = openApiSpec.components?.responses || {};
|
|
399
|
+
const requestBodies = openApiSpec.components?.requestBodies || {};
|
|
400
|
+
const paths = openApiSpec.paths || {};
|
|
401
|
+
|
|
402
|
+
Object.entries(paths).forEach(([, pathItem]) => {
|
|
403
|
+
Object.entries(pathItem).forEach(([, operation]) => {
|
|
404
|
+
if (typeof operation !== 'object') return;
|
|
405
|
+
|
|
406
|
+
if (operation.requestBody?.$ref) {
|
|
407
|
+
const requestBodyName = operation.requestBody.$ref.replace(
|
|
408
|
+
'#/components/requestBodies/',
|
|
409
|
+
''
|
|
410
|
+
);
|
|
411
|
+
const requestBodyDefinition = requestBodies[requestBodyName];
|
|
412
|
+
if (requestBodyDefinition?.content) {
|
|
413
|
+
Object.values(requestBodyDefinition.content).forEach((content) => {
|
|
414
|
+
if (content.schema?.$ref) {
|
|
415
|
+
const schemaName = content.schema.$ref.replace('#/components/schemas/', '');
|
|
416
|
+
schemasToProcess.push(schemaName);
|
|
417
|
+
}
|
|
418
|
+
if (content.schema?.properties) {
|
|
419
|
+
findRefsInObject(content.schema.properties, (ref) => {
|
|
420
|
+
const schemaName = ref.replace('#/components/schemas/', '');
|
|
421
|
+
schemasToProcess.push(schemaName);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (operation.requestBody?.content) {
|
|
429
|
+
Object.values(operation.requestBody.content).forEach((content) => {
|
|
430
|
+
if (content.schema?.$ref) {
|
|
431
|
+
const schemaName = content.schema.$ref.replace('#/components/schemas/', '');
|
|
432
|
+
schemasToProcess.push(schemaName);
|
|
433
|
+
}
|
|
434
|
+
if (content.schema?.properties?.requests?.items?.$ref) {
|
|
435
|
+
const schemaName = content.schema.properties.requests.items.$ref.replace(
|
|
436
|
+
'#/components/schemas/',
|
|
437
|
+
''
|
|
438
|
+
);
|
|
439
|
+
schemasToProcess.push(schemaName);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (operation.responses) {
|
|
445
|
+
Object.entries(operation.responses).forEach(([, response]) => {
|
|
446
|
+
if (response.$ref) {
|
|
447
|
+
const responseName = response.$ref.replace('#/components/responses/', '');
|
|
448
|
+
const responseDefinition = responses[responseName];
|
|
449
|
+
if (responseDefinition?.content) {
|
|
450
|
+
Object.values(responseDefinition.content).forEach((content) => {
|
|
451
|
+
if (content.schema?.$ref) {
|
|
452
|
+
const schemaName = content.schema.$ref.replace('#/components/schemas/', '');
|
|
453
|
+
schemasToProcess.push(schemaName);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (response.content) {
|
|
460
|
+
Object.values(response.content).forEach((content) => {
|
|
461
|
+
if (content.schema?.$ref) {
|
|
462
|
+
const schemaName = content.schema.$ref.replace('#/components/schemas/', '');
|
|
463
|
+
schemasToProcess.push(schemaName);
|
|
464
|
+
}
|
|
465
|
+
if (content.schema?.allOf) {
|
|
466
|
+
content.schema.allOf.forEach((allOfItem) => {
|
|
467
|
+
if (allOfItem.$ref) {
|
|
468
|
+
const schemaName = allOfItem.$ref.replace('#/components/schemas/', '');
|
|
469
|
+
schemasToProcess.push(schemaName);
|
|
470
|
+
}
|
|
471
|
+
if (allOfItem.properties?.value?.items?.$ref) {
|
|
472
|
+
const schemaName = allOfItem.properties.value.items.$ref.replace(
|
|
473
|
+
'#/components/schemas/',
|
|
474
|
+
''
|
|
475
|
+
);
|
|
476
|
+
schemasToProcess.push(schemaName);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (operation.parameters) {
|
|
486
|
+
operation.parameters.forEach((param) => {
|
|
487
|
+
if (param.schema?.$ref) {
|
|
488
|
+
const schemaName = param.schema.$ref.replace('#/components/schemas/', '');
|
|
489
|
+
schemasToProcess.push(schemaName);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const visited = new Set();
|
|
497
|
+
|
|
498
|
+
function processSchema(schemaName) {
|
|
499
|
+
if (visited.has(schemaName)) return;
|
|
500
|
+
visited.add(schemaName);
|
|
501
|
+
|
|
502
|
+
const schema = schemas[schemaName];
|
|
503
|
+
if (!schema) {
|
|
504
|
+
console.log(`⚠️ Warning: Schema ${schemaName} not found`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
usedSchemas.add(schemaName);
|
|
509
|
+
|
|
510
|
+
findRefsInObject(schema, (ref) => {
|
|
511
|
+
const refSchemaName = ref.replace('#/components/schemas/', '');
|
|
512
|
+
if (schemas[refSchemaName]) {
|
|
513
|
+
processSchema(refSchemaName);
|
|
514
|
+
} else {
|
|
515
|
+
console.log(`⚠️ Schema ${schemaName} references missing schema: ${refSchemaName}`);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
schemasToProcess.forEach((schemaName) => processSchema(schemaName));
|
|
521
|
+
|
|
522
|
+
[
|
|
523
|
+
'microsoft.graph.ODataErrors.ODataError',
|
|
524
|
+
'microsoft.graph.ODataErrors.MainError',
|
|
525
|
+
'microsoft.graph.ODataErrors.ErrorDetails',
|
|
526
|
+
'microsoft.graph.ODataErrors.InnerError',
|
|
527
|
+
].forEach((errorSchema) => {
|
|
528
|
+
if (schemas[errorSchema]) {
|
|
529
|
+
processSchema(errorSchema);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
console.log(
|
|
534
|
+
` Found ${usedSchemas.size} used schemas out of ${Object.keys(schemas).length} total schemas`
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
return usedSchemas;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function findRefsInObject(obj, callback, visited = new Set()) {
|
|
541
|
+
if (!obj || typeof obj !== 'object' || visited.has(obj)) return;
|
|
542
|
+
visited.add(obj);
|
|
543
|
+
|
|
544
|
+
if (Array.isArray(obj)) {
|
|
545
|
+
obj.forEach((item) => findRefsInObject(item, callback, visited));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
Object.entries(obj).forEach(([key, value]) => {
|
|
550
|
+
if (key === '$ref' && typeof value === 'string' && value.startsWith('#/components/schemas/')) {
|
|
551
|
+
callback(value);
|
|
552
|
+
} else if (typeof value === 'object') {
|
|
553
|
+
findRefsInObject(value, callback, visited);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function cleanBrokenRefs(obj, availableSchemas, visited = new Set()) {
|
|
559
|
+
if (!obj || typeof obj !== 'object' || visited.has(obj)) return;
|
|
560
|
+
visited.add(obj);
|
|
561
|
+
|
|
562
|
+
if (Array.isArray(obj)) {
|
|
563
|
+
for (let i = obj.length - 1; i >= 0; i--) {
|
|
564
|
+
const item = obj[i];
|
|
565
|
+
if (item && typeof item === 'object' && item.$ref) {
|
|
566
|
+
const refSchemaName = item.$ref.replace('#/components/schemas/', '');
|
|
567
|
+
if (!availableSchemas[refSchemaName]) {
|
|
568
|
+
console.log(` Removing broken reference: ${refSchemaName}`);
|
|
569
|
+
obj.splice(i, 1);
|
|
570
|
+
}
|
|
571
|
+
} else if (typeof item === 'object') {
|
|
572
|
+
cleanBrokenRefs(item, availableSchemas, visited);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
Object.entries(obj).forEach(([key, value]) => {
|
|
579
|
+
if (key === '$ref' && typeof value === 'string' && value.startsWith('#/components/schemas/')) {
|
|
580
|
+
const refSchemaName = value.replace('#/components/schemas/', '');
|
|
581
|
+
if (!availableSchemas[refSchemaName]) {
|
|
582
|
+
console.log(` Removing broken $ref: ${refSchemaName}`);
|
|
583
|
+
delete obj[key];
|
|
584
|
+
if (Object.keys(obj).length === 0) {
|
|
585
|
+
obj.type = 'object';
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
} else if (typeof value === 'object') {
|
|
589
|
+
cleanBrokenRefs(value, availableSchemas, visited);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function pruneUnusedSchemas(openApiSpec, usedSchemas) {
|
|
595
|
+
const schemas = openApiSpec.components?.schemas || {};
|
|
596
|
+
const originalCount = Object.keys(schemas).length;
|
|
597
|
+
|
|
598
|
+
Object.keys(schemas).forEach((schemaName) => {
|
|
599
|
+
if (!usedSchemas.has(schemaName)) {
|
|
600
|
+
delete schemas[schemaName];
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
Object.values(schemas).forEach((schema) => {
|
|
605
|
+
if (schema) {
|
|
606
|
+
cleanBrokenRefs(schema, schemas);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
if (openApiSpec.components?.responses) {
|
|
611
|
+
Object.values(openApiSpec.components.responses).forEach((response) => {
|
|
612
|
+
if (response) {
|
|
613
|
+
cleanBrokenRefs(response, schemas);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (openApiSpec.paths) {
|
|
619
|
+
Object.values(openApiSpec.paths).forEach((pathItem) => {
|
|
620
|
+
if (pathItem) {
|
|
621
|
+
cleanBrokenRefs(pathItem, schemas);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const newCount = Object.keys(schemas).length;
|
|
627
|
+
const reduction = (((originalCount - newCount) / originalCount) * 100).toFixed(1);
|
|
628
|
+
|
|
629
|
+
console.log(` Removed ${originalCount - newCount} unused schemas (${reduction}% reduction)`);
|
|
630
|
+
console.log(` Final schema count: ${newCount} (from ${originalCount})`);
|
|
631
|
+
|
|
632
|
+
if (openApiSpec.components?.responses) {
|
|
633
|
+
const usedResponses = new Set();
|
|
634
|
+
|
|
635
|
+
Object.values(openApiSpec.paths || {}).forEach((pathItem) => {
|
|
636
|
+
Object.values(pathItem).forEach((operation) => {
|
|
637
|
+
if (operation.responses) {
|
|
638
|
+
Object.values(operation.responses).forEach((response) => {
|
|
639
|
+
if (response.$ref) {
|
|
640
|
+
const responseName = response.$ref.replace('#/components/responses/', '');
|
|
641
|
+
usedResponses.add(responseName);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
usedResponses.add('error');
|
|
649
|
+
|
|
650
|
+
const responses = openApiSpec.components.responses;
|
|
651
|
+
const originalResponseCount = Object.keys(responses).length;
|
|
652
|
+
|
|
653
|
+
Object.keys(responses).forEach((responseName) => {
|
|
654
|
+
if (!usedResponses.has(responseName)) {
|
|
655
|
+
delete responses[responseName];
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const newResponseCount = Object.keys(responses).length;
|
|
660
|
+
console.log(
|
|
661
|
+
` Removed ${originalResponseCount - newResponseCount} unused responses (from ${originalResponseCount} to ${newResponseCount})`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (openApiSpec.components?.requestBodies) {
|
|
666
|
+
const usedRequestBodies = new Set();
|
|
667
|
+
|
|
668
|
+
Object.values(openApiSpec.paths || {}).forEach((pathItem) => {
|
|
669
|
+
Object.values(pathItem).forEach((operation) => {
|
|
670
|
+
if (operation.requestBody?.$ref) {
|
|
671
|
+
const requestBodyName = operation.requestBody.$ref.replace(
|
|
672
|
+
'#/components/requestBodies/',
|
|
673
|
+
''
|
|
674
|
+
);
|
|
675
|
+
usedRequestBodies.add(requestBodyName);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const requestBodies = openApiSpec.components.requestBodies;
|
|
681
|
+
const originalRequestBodyCount = Object.keys(requestBodies).length;
|
|
682
|
+
|
|
683
|
+
Object.keys(requestBodies).forEach((requestBodyName) => {
|
|
684
|
+
if (!usedRequestBodies.has(requestBodyName)) {
|
|
685
|
+
delete requestBodies[requestBodyName];
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const newRequestBodyCount = Object.keys(requestBodies).length;
|
|
690
|
+
console.log(
|
|
691
|
+
` Removed ${originalRequestBodyCount - newRequestBodyCount} unused request bodies (from ${originalRequestBodyCount} to ${newRequestBodyCount})`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
}
|