Showing the changes to scripts (1 of 2 files). Show all
Unified · Split
| @@ -337,6 +337,121 @@ const microsoftCatalogByMonolithPresetId: ReadonlyMap<string, OpenApiPreset> = n · 336 unmodified lines | ||
| 337 | 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 | 455 | const serviceSlugForPreset = (family: MonolithPluginId, presetId: string): string | undefined => { |
| 341 | 456 | const preset = |
| 342 | 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 | 530 | export const googlePresetIdForTool = (toolName: string): string | undefined => { |
| 409 | 531 | const [first, second] = toolName.split("."); |
| 410 | 532 | if (first === "admin") { |
| @@ -413,38 +535,20 @@ export const googlePresetIdForTool = (toolName: string): string | undefined => { · 2 unmodified lines | ||
| 413 | 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 | 539 | const segments = toolName.split("."); |
| 418 | 540 | const first = segments[0] ?? ""; |
| 419 | − const presetIds: string[] = []; | |
| 420 | − const add = (presetId: string) => { | |
| 421 | − if (!presetIds.includes(presetId)) presetIds.push(presetId); | |
| 422 | − }; | |
| 423 | − | |
| 424 | − if (/^me/.test(first)) add("me-surface"); | |
| 425 | − if (/^users/.test(first)) add("users"); | |
| 426 | − if (/^groups/.test(first)) add("groups"); | |
| 427 | − if (/^(me|users).*Message|^meMail|^usersMail|^meMailbox|^meOutlook|^usersOutlook/.test(first)) { | |
| 428 | − add("mail"); | |
| 429 | − } | |
| 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; | |
| 541 | + const ownershipKey = microsoftFirstSegmentOwnershipKey(first); | |
| 542 | + const fromFirstSegment = ownershipKey | |
| 543 | + ? (microsoftToolFirstSegmentPresetIds.get(ownershipKey) ?? []) | |
| 544 | + : []; | |
| 545 | + const workbookOwned = | |
| 546 | + ownershipKey !== undefined && | |
| 547 | + MICROSOFT_WORKBOOK_FIRST_SEGMENTS.has(ownershipKey) && | |
| 548 | + segments.some((segment) => segment.toLowerCase().includes("workbook")); | |
| 549 | + return unique([...fromFirstSegment, ...(workbookOwned ? ["excel"] : [])]).filter((presetId) => | |
| 550 | + MICROSOFT_TOOL_IMPLIED_PRESET_IDS.has(presetId), | |
| 551 | + ); | |
| 448 | 552 | }; |
| 449 | 553 | |
| 450 | 554 | const presetIdsForTool = (pluginId: MonolithPluginId, toolName: string): readonly string[] => { |
| @@ -462,6 +566,14 @@ const deriveServices = ( · 11 unmodified lines | ||
| 462 | 566 | pluginId === "google" |
| 463 | 567 | ? googlePresetIdsFromConfig(integration) |
| 464 | 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 | 577 | const fromTools = unique( |
| 466 | 578 | tools.flatMap((tool) => { |
| 467 | 579 | const presetIds = presetIdsForTool(pluginId, tool.name); |
| @@ -578,7 +690,7 @@ const operationRowsForService = ( · 110 unmodified lines | ||
| 578 | 690 | if (operationIntegration(row) !== monolith.slug) return false; |
| 579 | 691 | const toolName = operationToolName(row); |
| 580 | 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 | 694 | return serviceForMatchedTool(monolith.plugin_id as MonolithPluginId, toolName, [ |
| 583 | 695 | target, |
| 584 | 696 | ]).includes(target.slug); |
| @@ -602,6 +714,9 @@ const isConfigOnlyMonolith = (input: { · 17 unmodified lines | ||
| 602 | 714 | }): boolean => |
| 603 | 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 | 720 | const serviceForMatchedTool = ( |
| 606 | 721 | pluginId: MonolithPluginId, |
| 607 | 722 | toolName: string, |
| @@ -617,6 +732,43 @@ const serviceForMatchedTool = ( · 9 unmodified lines | ||
| 617 | 732 | const allServiceSlugs = (services: readonly ServiceTarget[]): readonly string[] => |
| 618 | 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 | 772 | const serviceSlugsForToolPattern = ( |
| 621 | 773 | pluginId: MonolithPluginId, |
| 622 | 774 | toolSegments: readonly string[], |
| @@ -880,6 +1032,13 @@ export const planMigration = (input: MigrationInput): MigrationPlan => { · 257 unmodified lines | ||
| 880 | 1032 | ) { |
| 881 | 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 | 1042 | const specHash = specHashFor(monolith); |
| 884 | 1043 | const namespace = pluginBlobNamespace(tenant, monolith.plugin_id); |
| 885 | 1044 | const targetNamespace = pluginBlobNamespace(tenant, "openapi"); |