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 | 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"); |