| @@ -337,6 +337,121 @@ const microsoftCatalogByMonolithPresetId: ReadonlyMap<string, OpenApiPreset> = n · 336 unmodified lines |
| 337 | microsoftCatalog.map((preset) => [preset.id.replace(/^microsoft-/, ""), preset]), | 337 | microsoftCatalog.map((preset) => [preset.id.replace(/^microsoft-/, ""), preset]), |
| 338 | ); | 338 | ); |
| 339 | | 339 | |
| | 340 | const MICROSOFT_UNMIGRATABLE_UMBRELLA_PRESET_IDS = new Set(["me-surface", "users", "groups"]); |
| | 341 | const MICROSOFT_TOOL_IMPLIED_PRESET_IDS = new Set(MICROSOFT_GRAPH_DEFAULT_PRESET_IDS); |
| | 342 | const microsoftCatalogBackedPresetIds = new Set( |
| | 343 | microsoftCatalog |
| | 344 | .filter((preset) => preset.defaultSlug && preset.defaultSlug.length > 0) |
| | 345 | .map((preset) => preset.id.replace(/^microsoft-/, "")), |
| | 346 | ); |
| | 347 | |
| | 348 | export const microsoftToolFirstSegmentPresetIds: ReadonlyMap<string, readonly string[]> = new Map([ |
| | 349 | ["chats", ["teams-chat"]], |
| | 350 | ["chatsChat", ["teams-chat"]], |
| | 351 | ["communications", ["meetings-calls"]], |
| | 352 | ["communicationsCall", ["meetings-calls"]], |
| | 353 | ["communicationsOnlineMeeting", ["meetings-calls"]], |
| | 354 | ["communicationsPresence", ["meetings-calls"]], |
| | 355 | ["drives", ["files"]], |
| | 356 | ["drivesDrive", ["files"]], |
| | 357 | ["drivesDriveItem", ["files"]], |
| | 358 | ["groupsDrive", ["files"]], |
| | 359 | ["groupsDrives", ["files"]], |
| | 360 | ["groupsOnenote", ["onenote"]], |
| | 361 | ["groupsTeam", ["teams-channels"]], |
| | 362 | ["meCalendar", ["calendar"]], |
| | 363 | ["meCalendarGroups", ["calendar"]], |
| | 364 | ["meCalendars", ["calendar"]], |
| | 365 | ["meCalendarView", ["calendar"]], |
| | 366 | ["meChat", ["teams-chat"]], |
| | 367 | ["meChats", ["teams-chat"]], |
| | 368 | ["meContact", ["contacts"]], |
| | 369 | ["meContactFolder", ["contacts"]], |
| | 370 | ["meContactFolders", ["contacts"]], |
| | 371 | ["meContacts", ["contacts"]], |
| | 372 | ["meDrive", ["files"]], |
| | 373 | ["meDrives", ["files"]], |
| | 374 | ["meEvent", ["calendar"]], |
| | 375 | ["meEvents", ["calendar"]], |
| | 376 | ["meFindMeetingTimes", ["calendar"]], |
| | 377 | ["meFollowedSites", ["files"]], |
| | 378 | ["meGetMailTips", ["mail"]], |
| | 379 | ["meInferenceClassification", ["mail"]], |
| | 380 | ["meJoinedTeams", ["teams-channels"]], |
| | 381 | ["meMailFolders", ["mail"]], |
| | 382 | ["meMailboxSettings", ["mail"]], |
| | 383 | ["meMessage", ["mail"]], |
| | 384 | ["meMessages", ["mail"]], |
| | 385 | ["meOnlineMeeting", ["meetings-calls"]], |
| | 386 | ["meOnlineMeetings", ["meetings-calls"]], |
| | 387 | ["meOnenote", ["onenote"]], |
| | 388 | ["meOutlook", ["mail"]], |
| | 389 | ["meOutlookUser", ["mail"]], |
| | 390 | ["mePeople", ["contacts"]], |
| | 391 | ["mePerson", ["contacts"]], |
| | 392 | ["mePhoto", ["profile"]], |
| | 393 | ["meProfilePhoto", ["profile"]], |
| | 394 | ["meReminderView", ["calendar"]], |
| | 395 | ["meSendMail", ["mail"]], |
| | 396 | ["meSite", ["files"]], |
| | 397 | ["meTeam", ["teams-channels"]], |
| | 398 | ["meTodo", ["tasks"]], |
| | 399 | ["meUser", ["profile"]], |
| | 400 | ["meUserActions", ["profile"]], |
| | 401 | ["meUserFunctions", ["profile"]], |
| | 402 | ["shares", ["files"]], |
| | 403 | ["sites", ["sites"]], |
| | 404 | ["sitesOnenote", ["onenote"]], |
| | 405 | ["sitesSite", ["sites"]], |
| | 406 | ["teams", ["teams-channels"]], |
| | 407 | ["teamsChannel", ["teams-channels"]], |
| | 408 | ["teamsTeam", ["teams-channels"]], |
| | 409 | ["teamsTemplates", ["teams-channels"]], |
| | 410 | ["teamwork", ["teams-channels"]], |
| | 411 | ["usersCalendar", ["calendar"]], |
| | 412 | ["usersCalendarGroups", ["calendar"]], |
| | 413 | ["usersCalendars", ["calendar"]], |
| | 414 | ["usersCalendarView", ["calendar"]], |
| | 415 | ["usersContact", ["contacts"]], |
| | 416 | ["usersContactFolder", ["contacts"]], |
| | 417 | ["usersContactFolders", ["contacts"]], |
| | 418 | ["usersContacts", ["contacts"]], |
| | 419 | ["usersDrive", ["files"]], |
| | 420 | ["usersDrives", ["files"]], |
| | 421 | ["usersEvent", ["calendar"]], |
| | 422 | ["usersEvents", ["calendar"]], |
| | 423 | ["usersFindMeetingTimes", ["calendar"]], |
| | 424 | ["usersMailFolders", ["mail"]], |
| | 425 | ["usersMessage", ["mail"]], |
| | 426 | ["usersMessages", ["mail"]], |
| | 427 | ["usersOnlineMeeting", ["meetings-calls"]], |
| | 428 | ["usersOnlineMeetings", ["meetings-calls"]], |
| | 429 | ["usersOnenote", ["onenote"]], |
| | 430 | ["usersOutlook", ["mail"]], |
| | 431 | ["usersPeople", ["contacts"]], |
| | 432 | ["usersPerson", ["contacts"]], |
| | 433 | ["usersReminderView", ["calendar"]], |
| | 434 | ["usersSendMail", ["mail"]], |
| | 435 | ["usersTodo", ["tasks"]], |
| | 436 | ]); |
| | 437 | |
| | 438 | const MICROSOFT_WORKBOOK_FIRST_SEGMENTS = new Set([ |
| | 439 | "drivesDriveItem", |
| | 440 | "groupsDrive", |
| | 441 | "meDrive", |
| | 442 | "usersDrive", |
| | 443 | ]); |
| | 444 | |
| | 445 | const microsoftFirstSegmentOwnershipKey = (firstSegment: string): string | undefined => |
| | 446 | [...microsoftToolFirstSegmentPresetIds.keys()] |
| | 447 | .filter( |
| | 448 | (key) => |
| | 449 | firstSegment === key || |
| | 450 | (firstSegment.startsWith(key) && |
| | 451 | firstSegment[key.length]?.toUpperCase() === firstSegment[key.length]), |
| | 452 | ) |
| | 453 | .sort((left, right) => right.length - left.length)[0]; |
| | 454 | |
| 340 | const serviceSlugForPreset = (family: MonolithPluginId, presetId: string): string | undefined => { | 455 | const serviceSlugForPreset = (family: MonolithPluginId, presetId: string): string | undefined => { |
| 341 | const preset = | 456 | const preset = |
| 342 | family === "google" | 457 | family === "google" |
| @@ -405,6 +520,13 @@ const microsoftPresetIdsFromConfig = (integration: IntegrationRow): readonly str · 62 unmodified lines |
| 405 | ); | 520 | ); |
| 406 | }; | 521 | }; |
| 407 | | 522 | |
| | 523 | const microsoftUnmigratablePresetIds = (presetIds: readonly string[]): readonly string[] => |
| | 524 | presetIds.filter( |
| | 525 | (presetId) => |
| | 526 | MICROSOFT_UNMIGRATABLE_UMBRELLA_PRESET_IDS.has(presetId) || |
| | 527 | !microsoftCatalogBackedPresetIds.has(presetId), |
| | 528 | ); |
| | 529 | |
| 408 | export const googlePresetIdForTool = (toolName: string): string | undefined => { | 530 | export const googlePresetIdForTool = (toolName: string): string | undefined => { |
| 409 | const [first, second] = toolName.split("."); | 531 | const [first, second] = toolName.split("."); |
| 410 | if (first === "admin") { | 532 | if (first === "admin") { |
| @@ -413,38 +535,20 @@ export const googlePresetIdForTool = (toolName: string): string | undefined => { · 2 unmodified lines |
| 413 | return GOOGLE_TOOL_PREFIX_TO_PRESET_ID.get(first ?? ""); | 535 | return GOOGLE_TOOL_PREFIX_TO_PRESET_ID.get(first ?? ""); |
| 414 | }; | 536 | }; |
| 415 | | 537 | |
| 416 | const microsoftPresetIdsForTool = (toolName: string): readonly string[] => { | 538 | export const microsoftPresetIdsForTool = (toolName: string): readonly string[] => { |
| 417 | const segments = toolName.split("."); | 539 | const segments = toolName.split("."); |
| 418 | const first = segments[0] ?? ""; | 540 | const first = segments[0] ?? ""; |
| 419 | const presetIds: string[] = []; | 541 | const ownershipKey = microsoftFirstSegmentOwnershipKey(first); |
| 420 | const add = (presetId: string) => { | 542 | const fromFirstSegment = ownershipKey |
| 421 | if (!presetIds.includes(presetId)) presetIds.push(presetId); | 543 | ? (microsoftToolFirstSegmentPresetIds.get(ownershipKey) ?? []) |
| 422 | }; | 544 | : []; |
| 423 | | 545 | const workbookOwned = |
| 424 | if (/^me/.test(first)) add("me-surface"); | 546 | ownershipKey !== undefined && |
| 425 | if (/^users/.test(first)) add("users"); | 547 | MICROSOFT_WORKBOOK_FIRST_SEGMENTS.has(ownershipKey) && |
| 426 | if (/^groups/.test(first)) add("groups"); | 548 | segments.some((segment) => segment.toLowerCase().includes("workbook")); |
| 427 | if (/^(me|users).*Message|^meMail|^usersMail|^meMailbox|^meOutlook|^usersOutlook/.test(first)) { | 549 | return unique([...fromFirstSegment, ...(workbookOwned ? ["excel"] : [])]).filter((presetId) => |
| 428 | add("mail"); | 550 | MICROSOFT_TOOL_IMPLIED_PRESET_IDS.has(presetId), |
| 429 | } | 551 | ); |
| 430 | if (/Calendar|Event|Reminder|findMeetingTimes/.test(first)) add("calendar"); | | |
| 431 | if (/Contact|Person/.test(first)) add("contacts"); | | |
| 432 | if (/Todo/.test(first)) add("tasks"); | | |
| 433 | if (/Planner/.test(first)) add("planner"); | | |
| 434 | if (/Drive|drives|shares/.test(first)) add("files"); | | |
| 435 | if (segments.some((segment) => /workbook/i.test(segment)) || /Workbook|Excel/.test(first)) { | | |
| 436 | if (/^(me|users|groups)/.test(first) || /Drive|drives/.test(first)) add("files"); | | |
| 437 | add("excel"); | | |
| 438 | } | | |
| 439 | if (/sites|Site|List|SharePoint/.test(first)) add("sites"); | | |
| 440 | if (/onenote/i.test(first) || segments.some((segment) => /onenote/i.test(segment))) { | | |
| 441 | add("onenote"); | | |
| 442 | } | | |
| 443 | if (/chats|Chat/.test(first)) add("teams-chat"); | | |
| 444 | if (/teams|Team|teamwork|Channel/.test(first)) add("teams-channels"); | | |
| 445 | if (/communications|OnlineMeeting|Call|Presence/.test(first)) add("meetings-calls"); | | |
| 446 | if (/^meUser|^usersUser|ProfilePhoto/.test(first)) add("profile"); | | |
| 447 | return presetIds; | | |
| 448 | }; | 552 | }; |
| 449 | | 553 | |
| 450 | const presetIdsForTool = (pluginId: MonolithPluginId, toolName: string): readonly string[] => { | 554 | const presetIdsForTool = (pluginId: MonolithPluginId, toolName: string): readonly string[] => { |
| @@ -462,6 +566,14 @@ const deriveServices = ( · 11 unmodified lines |
| 462 | pluginId === "google" | 566 | pluginId === "google" |
| 463 | ? googlePresetIdsFromConfig(integration) | 567 | ? googlePresetIdsFromConfig(integration) |
| 464 | : microsoftPresetIdsFromConfig(integration); | 568 | : microsoftPresetIdsFromConfig(integration); |
| | 569 | if (pluginId === "microsoft") { |
| | 570 | const unmigratablePresetIds = microsoftUnmigratablePresetIds(fromConfig); |
| | 571 | if (unmigratablePresetIds.length > 0) { |
| | 572 | throw new Error( |
| | 573 | `Monolith ${tenantHash(integration.tenant)}/${integration.plugin_id}/${integration.slug} references unmigratable-umbrella Microsoft Graph preset(s): ${unmigratablePresetIds.join(", ")}; org needs manual handling`, |
| | 574 | ); |
| | 575 | } |
| | 576 | } |
| 465 | const fromTools = unique( | 577 | const fromTools = unique( |
| 466 | tools.flatMap((tool) => { | 578 | tools.flatMap((tool) => { |
| 467 | const presetIds = presetIdsForTool(pluginId, tool.name); | 579 | const presetIds = presetIdsForTool(pluginId, tool.name); |
| @@ -578,7 +690,7 @@ const operationRowsForService = ( · 110 unmodified lines |
| 578 | if (operationIntegration(row) !== monolith.slug) return false; | 690 | if (operationIntegration(row) !== monolith.slug) return false; |
| 579 | const toolName = operationToolName(row); | 691 | const toolName = operationToolName(row); |
| 580 | if (!toolName) return false; | 692 | if (!toolName) return false; |
| 581 | if (monolith.plugin_id === "google" && toolName.startsWith("oauth2.")) return false; | 693 | if (isIntentionallyDroppedTool(monolith.plugin_id as MonolithPluginId, toolName)) return false; |
| 582 | return serviceForMatchedTool(monolith.plugin_id as MonolithPluginId, toolName, [ | 694 | return serviceForMatchedTool(monolith.plugin_id as MonolithPluginId, toolName, [ |
| 583 | target, | 695 | target, |
| 584 | ]).includes(target.slug); | 696 | ]).includes(target.slug); |
| @@ -602,6 +714,9 @@ const isConfigOnlyMonolith = (input: { · 17 unmodified lines |
| 602 | }): boolean => | 714 | }): boolean => |
| 603 | input.tools.length === 0 && input.connections.length === 0 && input.operations.length === 0; | 715 | input.tools.length === 0 && input.connections.length === 0 && input.operations.length === 0; |
| 604 | | 716 | |
| | 717 | const isIntentionallyDroppedTool = (pluginId: MonolithPluginId, toolName: string): boolean => |
| | 718 | pluginId === "google" && toolName.startsWith("oauth2."); |
| | 719 | |
| 605 | const serviceForMatchedTool = ( | 720 | const serviceForMatchedTool = ( |
| 606 | pluginId: MonolithPluginId, | 721 | pluginId: MonolithPluginId, |
| 607 | toolName: string, | 722 | toolName: string, |
| @@ -617,6 +732,43 @@ const serviceForMatchedTool = ( · 9 unmodified lines |
| 617 | const allServiceSlugs = (services: readonly ServiceTarget[]): readonly string[] => | 732 | const allServiceSlugs = (services: readonly ServiceTarget[]): readonly string[] => |
| 618 | services.map((service) => service.slug); | 733 | services.map((service) => service.slug); |
| 619 | | 734 | |
| | 735 | const unassignedToolNames = ( |
| | 736 | pluginId: MonolithPluginId, |
| | 737 | tools: readonly { readonly name: string }[], |
| | 738 | services: readonly ServiceTarget[], |
| | 739 | ): readonly string[] => |
| | 740 | unique( |
| | 741 | tools.flatMap((tool) => { |
| | 742 | if (isIntentionallyDroppedTool(pluginId, tool.name)) return []; |
| | 743 | return serviceForMatchedTool(pluginId, tool.name, services).length === 0 ? [tool.name] : []; |
| | 744 | }), |
| | 745 | ); |
| | 746 | |
| | 747 | const assertNoUnassignedRows = ( |
| | 748 | monolith: IntegrationRow, |
| | 749 | tools: readonly ToolRow[], |
| | 750 | operations: readonly PluginStorageRow[], |
| | 751 | services: readonly ServiceTarget[], |
| | 752 | ): void => { |
| | 753 | const pluginId = monolith.plugin_id as MonolithPluginId; |
| | 754 | const unassignedTools = unassignedToolNames(pluginId, tools, services); |
| | 755 | if (unassignedTools.length > 0) { |
| | 756 | throw new Error( |
| | 757 | `Monolith ${tenantHash(monolith.tenant)}/${monolith.plugin_id}/${monolith.slug} has tool row(s) with no target service: ${unassignedTools.slice(0, 20).join(", ")}`, |
| | 758 | ); |
| | 759 | } |
| | 760 | const operationTools = operations.flatMap((row) => { |
| | 761 | const toolName = operationToolName(row); |
| | 762 | return toolName ? [{ name: toolName }] : []; |
| | 763 | }); |
| | 764 | const unassignedOperations = unassignedToolNames(pluginId, operationTools, services); |
| | 765 | if (unassignedOperations.length > 0) { |
| | 766 | throw new Error( |
| | 767 | `Monolith ${tenantHash(monolith.tenant)}/${monolith.plugin_id}/${monolith.slug} has operation row(s) with no target service: ${unassignedOperations.slice(0, 20).join(", ")}`, |
| | 768 | ); |
| | 769 | } |
| | 770 | }; |
| | 771 | |
| 620 | const serviceSlugsForToolPattern = ( | 772 | const serviceSlugsForToolPattern = ( |
| 621 | pluginId: MonolithPluginId, | 773 | pluginId: MonolithPluginId, |
| 622 | toolSegments: readonly string[], | 774 | toolSegments: readonly string[], |
| @@ -880,6 +1032,13 @@ export const planMigration = (input: MigrationInput): MigrationPlan => { · 257 unmodified lines |
| 880 | ) { | 1032 | ) { |
| 881 | continue; | 1033 | continue; |
| 882 | } | 1034 | } |
| | 1035 | try { |
| | 1036 | assertNoUnassignedRows(monolith, monolithTools, monolithOperations, services); |
| | 1037 | } catch (error) { |
| | 1038 | if (!input.collectPolicyErrors) throw error; |
| | 1039 | hardErrors.push(error instanceof Error ? error.message : String(error)); |
| | 1040 | continue; |
| | 1041 | } |
| 883 | const specHash = specHashFor(monolith); | 1042 | const specHash = specHashFor(monolith); |
| 884 | const namespace = pluginBlobNamespace(tenant, monolith.plugin_id); | 1043 | const namespace = pluginBlobNamespace(tenant, monolith.plugin_id); |
| 885 | const targetNamespace = pluginBlobNamespace(tenant, "openapi"); | 1044 | const targetNamespace = pluginBlobNamespace(tenant, "openapi"); |