From b150ee48b049b42edf97cf6bb09f326092834a2e Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Wed, 14 Aug 2024 22:55:43 +0200 Subject: [PATCH 01/43] Add new sample to find obsolete m365 groups in the tenant. Closes #2475 --- .../assets/sample.json | 59 ++++ .../entra/find-obsolete-m365-groups/index.mdx | 288 ++++++++++++++++++ 2 files changed, 347 insertions(+) create mode 100644 docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json create mode 100644 docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json new file mode 100644 index 00000000000..4155599794d --- /dev/null +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json @@ -0,0 +1,59 @@ +[ + { + "name": "pnp-find-obsolete-m365-groups", + "source": "pnp", + "title": "Finding Obsolete Microsoft 365 Groups with PowerShell", + "url": "https://pnp.github.io/cli-microsoft365/sample-scripts/entra/find-obsolete-m365-groups", + "creationDateTime": "2024-08-14", + "updateDateTime": "2024-08-14", + "shortDescription": "Understand to what extent the Microsoft 365 groups in your tenant are being used or even not.", + "longDescription": [ + "Like any resource within your Microsoft 365 tenant, M365 Groups can become unused over time. This routine uses PowerShell with `m365 cli` commands to create a report of all M365 groups that are possibly obsolete." + ], + "products": ["SharePoint", "M365 Groups", "Teams", "Exchange Online"], + "categories": [], + "tags": [ + "provisioning", + "libraries", + "group mailbox", + "governance", + "m365 groups", + "teams", + "usage", + "insights" + ], + "metadata": [ + { + "key": "CLI-FOR-MICROSOFT365", + "value": "v8.0.0" + } + ], + "thumbnails": [ + { + "type": "image", + "order": 100, + "url": "https://raw.githubusercontent.com/pnp/cli-microsoft365/main/docs/docs/sample-scripts/find-obsolete-m365-groups/assets/preview.png", + "alt": "preview image for the sample" + } + ], + "authors": [ + { + "gitHubAccount": "tmaestrini", + "pictureUrl": "https://avatars.githubusercontent.com/u/69770609?v=4", + "name": "Tobias Maestrini" + } + ], + "references": [ + { + "name": "Want to learn more about CLI for Microsoft 365 and the commands", + "description": "Check out the CLI for Microsoft 365 site to get started and for the reference to the commands.", + "url": "https://aka.ms/cli-m365" + }, + { + "name": "Original article by Tony Redmond", + "description": "Check out the original article on which this script is based.", + "url": "https://petri.com/identifying-obsolete-office-365-groups-powershell" + } + ] + } +] diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx new file mode 100644 index 00000000000..f356e147119 --- /dev/null +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -0,0 +1,288 @@ +--- +tags: + - provisioning + - libraries + - group mailbox + - governance + - teams + - m365 groups +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Finding Obsolete Microsoft 365 Groups with PowerShell + +Author: [Tobias Maestrini](https://github.com/tmaestrini) + +This script is based on the [original article](https://petri.com/identifying-obsolete-office-365-groups-powershell) written by [Tony Redmond](https://twitter.com/12Knocksinna). + +Like any resource within your Microsoft 365 tenant, M365 Groups can become unused over time. + +This routine uses PowerShell with `m365 cli` commands +- to gather insights about SharePoint file activity within the related SharePoint site, +- to do a check against conversation items in the group mailbox, +- to denote the amount of active people (group owners, members and guests) in the group. + +These metrics can help us understand the extent to which the resource is being used from a governance perspective – or even not. +Use this script to create a report of all M365 groups that are possibly obsolete. + + + + + ```powershell + $ErrorActionPreference = "Stop" + + class GroupInfo { + [PSCustomObject] $Reference + [PSCustomObject] $Membership + [PSCustomObject] $SharePointStatus + [PSCustomObject] $MailboxStatus + [PSCustomObject] $ChatStatus + [string] $TestStatus + [string[]] $Reasons + } + + function Start-Routine { + # START ROUTINE + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] [Switch] $KeepConnectionsAlive, + [Parameter(Mandatory = $false)] [Switch] $KeepOutputPath + ) + + try { + Initialize-Params + if ($KeepOutputPath.IsPresent) { Initialize-ExportPath -KeepOutputPath } + else { Initialize-ExportPath } + Get-AllM365Groups + Start-GroupInsightsTests + + Write-Host "`n✔︎ Routine terminated" -ForegroundColor Green + if (!$KeepConnectionsAlive.IsPresent) { + m365 logout + } + } + catch { + Write-Error $_.Exception.Message + } + } + + function Initialize-Params { + Write-Host "🚀 Generating report of obsolete M365 groups within your organization" + + # define globals + $Global:Path + $Script:ReportPath = $null + $Script:Groups = @() + $Global:ObsoleteGroups = [System.Collections.Generic.Dictionary[string, GroupInfo]]::new() + + Write-Output "Connecting to M365 tenant: please follow the instructions." + Write-output "IMPORTANT: You'll need to have global admin permissions!`n" + if ((m365 status --output text) -eq "Logged out") { + m365 login + } + } + + function Initialize-ExportPath { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] [Switch] $KeepOutputPath + ) + + if (!$KeepOutputPath.IsPresent -or $null -eq $Global:Path) { + $Script:Path = Read-Host "Set the path to the folder where you want to export the report data as csv file" + } + + $TestPath = Test-Path -Path $Script:Path + $tStamp = (Get-Date).ToString("yyyyMMddHHmmss") + if ($TestPath -ne $true) { + New-Item -ItemType directory -Path $Script:Path | Out-Null + Write-Host "Will create file in $($Script:Path): M365GroupsReport-{current date}.csv" -ForegroundColor Yellow + } + else { + Write-Host "Following report file will be created in $($Script:Path): 'M365GroupsReport-$($tStamp).csv'." + Write-Host "`nAll data will be exported to $($Script:Path): M365GroupsReport-$($tStamp).csv." -ForegroundColor Blue + Write-Host "Do not edit this file during the scan." -ForegroundColor Blue + } + $Script:ReportPath = "$($Script:Path)/M365GroupsReport-$($tStamp).csv" + } + + function Get-AllM365Groups { + $groups = m365 entra m365group list --includeSiteUrl | ConvertFrom-Json + $Script:Groups = $groups | Where-Object { $null -ne $_.siteUrl } + } + + function Start-GroupInsightsTests { + Write-Host "Checking your $($Script:Groups.Count) groups for activity" + + $Script:Groups | ForEach-Object { + $groupInfo = [GroupInfo]::new() + $groupInfo.Reference = $_ + $groupInfo.Membership = @{Owners = 0; Members = 0; Guests = 0 } + $groupInfo.TestStatus = "🟢 OK" + + Write-Host "☀︎ $($groupInfo.Reference.displayName)" + + # Tests + Test-GroupMembership -Group $groupInfo + Test-SharePointActivity -Group $groupInfo + Test-ConversationActivity -Group $groupInfo + + # Report + New-Report -Group $groupInfo + } + + #Give feedback to user + Write-Host "`n-------------------------------------------------------------------" + Write-Host "`SUMMARY" -ForegroundColor DarkGreen + Write-Host "`-------------------------------------------------------------------" + Write-Host "`n👉 Found $($Global:ObsoleteGroups.Count) group$($Global:ObsoleteGroups.Count -gt 1 ? 's' : '') with possibly low activity." + Write-Host "` Please review the report: " -NoNewline + Write-Host "$($Script:ReportPath)" -ForegroundColor DarkBlue + } + + function Test-GroupMembership { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] [GroupInfo] $Group + ) + + $users = m365 entra m365group user list --groupId $Group.Reference.id | ConvertFrom-Json + $owners = $users | Where-Object { $_.userType -eq "Owner" } + $members = $users | Where-Object { $_.userType -eq "Member" } + $guests = $users | Where-Object { $_.userType -eq "Guest" } + + $Group.Membership = [ordered] @{ + Owners = $owners + Members = $members + Guests = $guests + } + + if ($owners.Count -eq 1 -and ($members.Count + $guests.Count) -eq 0) { + Write-Host " → potentially obsolete (abandoned group: only 1 owner left)" -ForegroundColor Yellow + $reason = "Low user count" + + $Group.Membership.Status = "Abandoned ($reason)" + $Group.TestStatus = "🟡 Warning" + $Group.Reasons += $reason + + try { + $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) + } + catch {} + } + } + + function Test-SharePointActivity { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] [GroupInfo] $Group + ) + + function Get-ParsedDate { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] [String] $JavascriptDateString + ) + + $dateParts = [regex]::Matches($JavascriptDateString, '\d+') | ForEach-Object { $_.Value } + + # Convert the parts to integers + $year = [int]$dateParts[0] + $month = [int]$dateParts[1] + 1 + $day = [int]$dateParts[2] + $hour = [int]$dateParts[3] + $minute = [int]$dateParts[4] + $second = [int]$dateParts[5] + # $millisecond = [int]$dateParts[6] + + # return a DateTime object + $dateObject = New-Object -TypeName DateTime -ArgumentList $year, $month, $day, $hour, $minute, $second + $dateObject + } + + $WarningDate = (Get-Date).AddDays(-90) + + # Not possible to retrieve property 'LastContentModifiedDate' via command 'm365 spo site get --url $group.siteUrl', so we need to use filtering: + # $spoSite = m365 spo site get --url $group.siteUrl | ConvertFrom-Json + $spoSite = m365 spo site list --filter "Url -eq '$($Group.Reference.siteUrl)'" | ConvertFrom-Json + $spoSite.LastContentModifiedDate = Get-ParsedDate -JavascriptDateString $spoSite.LastContentModifiedDate + if ($spoSite.LastContentModifiedDate -lt $WarningDate) { + Write-Host " → potentially obsolete (SPO last content modified: $($spoSite.LastContentModifiedDate))" -ForegroundColor Yellow + $reason = "Low SharePoint activity ($($spoSite.LastContentModifiedDate))" + + $Group.SharePointStatus = @{ + Reason = $reason + } + $Group.TestStatus = "🟡 Warning" + $Group.Reasons += $reason + + try { + $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) + } + catch {} + } + } + + function Test-ConversationActivity { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] [GroupInfo] $Group + ) + + $WarningDate = (Get-Date).AddDays(-365) + + $a = m365 entra m365group conversation list --groupId $Group.Reference.id | ConvertFrom-Json | Where-Object { + [datetime]$_.lastDeliveredDateTime -lt $WarningDate + } + if (!$a -or $a.Length -eq 0) { return } + + Write-Host " → potentially obsolete ($($a.Length) conversation item$($a.Length -gt 1 ? 's' : '') created more than 1 year ago)" -ForegroundColor Yellow + $reason = "$($a.Length) conversation item$($a.Length -gt 1 ? 's' : '') created more than 1 year ago)" + + $Group.MailboxStatus = @{ + OutdatedConversations = $a | Sort-Object -Property lastDeliveredDateTime + Reason = $reason + } + $Group.TestStatus = "🟡 Warning" + $Group.Reasons += $reason + + try { + $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) + } + catch { } + } + + function New-Report { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] [GroupInfo] $Group + ) + + $exportObject = [ordered] @{ + "Group Name" = $Group.Reference.displayName + "Managed by" = $Group.Membership.Owners ? $Group.Membership.Owners.displayName -join ", " : "n/a" + Owners = $Group.Membership.Owners.Count + Members = $Group.Membership.Members.Count + Guests = $Group.Membership.Guests.Count + "Group Status" = $Group.Membership.Status ?? "Normal" + Description = $Group.Reference.description + "Conversation Status" = $Group.MailboxStatus.Reason ?? "Normal" + "Number of Conversations" = $Group.MailboxStatus.OutdatedConversations ? $Group.MailboxStatus.OutdatedConversations.Length : "n/a" + "Last Conversation" = $Group.MailboxStatus.OutdatedConversations ? $Group.MailboxStatus.OutdatedConversations[0].lastDeliveredDateTime : "" + "Team enabled" = $Group.Reference.resourceProvisioningOptions -contains 'Team' ? "True" : "False" + "SPO Status" = $Group.SharePointStatus.Reason ?? "Normal" + "SPO Activity" = $Group.SharePointStatus ? "Low / No document library usage" : "Document library in use" + "Number of warnings" = $Group.Reasons.Count + Status = $Group.TestStatus + } + + $exportObject | Export-Csv -Path $Script:ReportPath -Append -NoTypeInformation + } + + # START the report generation + Start-Routine #-KeepConnectionsAlive -KeepOutputPath + ``` + + From c3c96bb93585bc9d6f92757d299aaeb2172f45d6 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Wed, 14 Aug 2024 23:20:12 +0200 Subject: [PATCH 02/43] add preview image to assets --- .../assets/preview.png | Bin 0 -> 68490 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/preview.png diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/preview.png b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..d85816f034a180502b0757457c54bfe37c6bedca GIT binary patch literal 68490 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 literal 0 HcmV?d00001 From 9643937ac3e5401229c3713126c6970af127f02d Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Wed, 28 Aug 2024 00:41:39 +0200 Subject: [PATCH 03/43] fix typos and change several minor implementations due to readabiltiy --- .../entra/find-obsolete-m365-groups/index.mdx | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index f356e147119..0b7da414e1f 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -11,7 +11,7 @@ tags: import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# Finding Obsolete Microsoft 365 Groups with PowerShell +# Finding obsolete Microsoft 365 groups with PowerShell Author: [Tobias Maestrini](https://github.com/tmaestrini) @@ -19,10 +19,10 @@ This script is based on the [original article](https://petri.com/identifying-obs Like any resource within your Microsoft 365 tenant, M365 Groups can become unused over time. -This routine uses PowerShell with `m365 cli` commands -- to gather insights about SharePoint file activity within the related SharePoint site, -- to do a check against conversation items in the group mailbox, -- to denote the amount of active people (group owners, members and guests) in the group. +This routine uses PowerShell with CLI for Microsoft 365 +- To gather insights about SharePoint file activity within the related SharePoint site. +- To do a check against conversation items in the group mailbox. +- To denote the amount of active people (group owners, members and guests) in the group. These metrics can help us understand the extent to which the resource is being used from a governance perspective – or even not. Use this script to create a report of all M365 groups that are possibly obsolete. @@ -78,7 +78,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete $Global:ObsoleteGroups = [System.Collections.Generic.Dictionary[string, GroupInfo]]::new() Write-Output "Connecting to M365 tenant: please follow the instructions." - Write-output "IMPORTANT: You'll need to have global admin permissions!`n" + Write-output "IMPORTANT: You'll need to have at least global reader permissions!`n" if ((m365 status --output text) -eq "Logged out") { m365 login } @@ -95,7 +95,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete } $TestPath = Test-Path -Path $Script:Path - $tStamp = (Get-Date).ToString("yyyyMMddHHmmss") + $tStamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") if ($TestPath -ne $true) { New-Item -ItemType directory -Path $Script:Path | Out-Null Write-Host "Will create file in $($Script:Path): M365GroupsReport-{current date}.csv" -ForegroundColor Yellow @@ -114,7 +114,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete } function Start-GroupInsightsTests { - Write-Host "Checking your $($Script:Groups.Count) groups for activity" + Write-Host "Checking $($Script:Groups.Count) groups for activity" $Script:Groups | ForEach-Object { $groupInfo = [GroupInfo]::new() @@ -148,10 +148,16 @@ Use this script to create a report of all M365 groups that are possibly obsolete [Parameter(Mandatory = $true)] [GroupInfo] $Group ) + # Original lists $users = m365 entra m365group user list --groupId $Group.Reference.id | ConvertFrom-Json - $owners = $users | Where-Object { $_.userType -eq "Owner" } - $members = $users | Where-Object { $_.userType -eq "Member" } - $guests = $users | Where-Object { $_.userType -eq "Guest" } + $owners = m365 entra m365group user list --groupId $Group.Reference.id --role Owner | ConvertFrom-Json + $members = m365 entra m365group user list --groupId $Group.Reference.id --role Member | ConvertFrom-Json + + # Consider guests as users that are not in the $members list + $guests = Compare-Object -ReferenceObject $users $members -PassThru + + # Modify the $members list to only contain users that are not in the $owners list + $members = Compare-Object $members $owners -PassThru $Group.Membership = [ordered] @{ Owners = $owners @@ -205,7 +211,6 @@ Use this script to create a report of all M365 groups that are possibly obsolete $WarningDate = (Get-Date).AddDays(-90) # Not possible to retrieve property 'LastContentModifiedDate' via command 'm365 spo site get --url $group.siteUrl', so we need to use filtering: - # $spoSite = m365 spo site get --url $group.siteUrl | ConvertFrom-Json $spoSite = m365 spo site list --filter "Url -eq '$($Group.Reference.siteUrl)'" | ConvertFrom-Json $spoSite.LastContentModifiedDate = Get-ParsedDate -JavascriptDateString $spoSite.LastContentModifiedDate if ($spoSite.LastContentModifiedDate -lt $WarningDate) { From 793b05b5942a9695473c71dbe8eeb5800cb97d56 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Mon, 9 Sep 2024 14:12:45 +0200 Subject: [PATCH 04/43] change implementation after rewiew --- .../assets/sample.json | 2 +- .../entra/find-obsolete-m365-groups/index.mdx | 69 ++++++++----------- 2 files changed, 30 insertions(+), 41 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json index 4155599794d..25ffb0489a0 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/sample.json @@ -8,7 +8,7 @@ "updateDateTime": "2024-08-14", "shortDescription": "Understand to what extent the Microsoft 365 groups in your tenant are being used or even not.", "longDescription": [ - "Like any resource within your Microsoft 365 tenant, M365 Groups can become unused over time. This routine uses PowerShell with `m365 cli` commands to create a report of all M365 groups that are possibly obsolete." + "Like any resource within your Microsoft 365 tenant, M365 Groups can become unused over time. This routine uses PowerShell with CLI for Microsoft 365 to create a report of all M365 groups that are possibly obsolete." ], "products": ["SharePoint", "M365 Groups", "Teams", "Exchange Online"], "categories": [], diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index 0b7da414e1f..35d6c668973 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -56,6 +56,8 @@ Use this script to create a report of all M365 groups that are possibly obsolete if ($KeepOutputPath.IsPresent) { Initialize-ExportPath -KeepOutputPath } else { Initialize-ExportPath } Get-AllM365Groups + Get-AllGuestUsers + Get-AllTeamSites Start-GroupInsightsTests Write-Host "`n✔︎ Routine terminated" -ForegroundColor Green @@ -75,6 +77,8 @@ Use this script to create a report of all M365 groups that are possibly obsolete $Global:Path $Script:ReportPath = $null $Script:Groups = @() + $Script:Guests = @() + $Script:TeamSites = @() $Global:ObsoleteGroups = [System.Collections.Generic.Dictionary[string, GroupInfo]]::new() Write-Output "Connecting to M365 tenant: please follow the instructions." @@ -113,6 +117,14 @@ Use this script to create a report of all M365 groups that are possibly obsolete $Script:Groups = $groups | Where-Object { $null -ne $_.siteUrl } } + function Get-AllGuestUsers { + $Script:Guests = m365 entra user list --type Guest | ConvertFrom-Json + } + + function Get-AllTeamSites { + $Script:TeamSites = m365 spo site list --type TeamSite | ConvertFrom-Json + } + function Start-GroupInsightsTests { Write-Host "Checking $($Script:Groups.Count) groups for activity" @@ -153,8 +165,8 @@ Use this script to create a report of all M365 groups that are possibly obsolete $owners = m365 entra m365group user list --groupId $Group.Reference.id --role Owner | ConvertFrom-Json $members = m365 entra m365group user list --groupId $Group.Reference.id --role Member | ConvertFrom-Json - # Consider guests as users that are not in the $members list - $guests = Compare-Object -ReferenceObject $users $members -PassThru + # Consider guests as users that also match the $Script:Guests array + $guests = $users | Where-Object { $_.id -in $Script:Guests.id } # Modify the $members list to only contain users that are not in the $owners list $members = Compare-Object $members $owners -PassThru @@ -176,7 +188,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete try { $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) } - catch {} + catch { Write-Information "Group was already added to the list of potentially obsolete groups" } } } @@ -185,37 +197,14 @@ Use this script to create a report of all M365 groups that are possibly obsolete param ( [Parameter(Mandatory = $true)] [GroupInfo] $Group ) - - function Get-ParsedDate { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] [String] $JavascriptDateString - ) - - $dateParts = [regex]::Matches($JavascriptDateString, '\d+') | ForEach-Object { $_.Value } - - # Convert the parts to integers - $year = [int]$dateParts[0] - $month = [int]$dateParts[1] + 1 - $day = [int]$dateParts[2] - $hour = [int]$dateParts[3] - $minute = [int]$dateParts[4] - $second = [int]$dateParts[5] - # $millisecond = [int]$dateParts[6] - - # return a DateTime object - $dateObject = New-Object -TypeName DateTime -ArgumentList $year, $month, $day, $hour, $minute, $second - $dateObject - } $WarningDate = (Get-Date).AddDays(-90) - # Not possible to retrieve property 'LastContentModifiedDate' via command 'm365 spo site get --url $group.siteUrl', so we need to use filtering: - $spoSite = m365 spo site list --filter "Url -eq '$($Group.Reference.siteUrl)'" | ConvertFrom-Json - $spoSite.LastContentModifiedDate = Get-ParsedDate -JavascriptDateString $spoSite.LastContentModifiedDate - if ($spoSite.LastContentModifiedDate -lt $WarningDate) { - Write-Host " → potentially obsolete (SPO last content modified: $($spoSite.LastContentModifiedDate))" -ForegroundColor Yellow - $reason = "Low SharePoint activity ($($spoSite.LastContentModifiedDate))" + $spoSite = $Script:TeamSites | Where-Object { $_.GroupId -eq "/Guid($($Group.Reference.id))/" } + $spoWeb = m365 spo web get --url $spoSite.Url | ConvertFrom-Json + if ($spoWeb.LastItemUserModifiedDate -lt $WarningDate) { + Write-Host " → potentially obsolete (SPO last content modified: $($spoWeb.LastItemUserModifiedDate))" -ForegroundColor Yellow + $reason = "Low SharePoint activity ($($spoWeb.LastItemUserModifiedDate))" $Group.SharePointStatus = @{ Reason = $reason @@ -226,7 +215,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete try { $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) } - catch {} + catch { Write-Information "Group was already added to the list of potentially obsolete groups" } } } @@ -238,16 +227,16 @@ Use this script to create a report of all M365 groups that are possibly obsolete $WarningDate = (Get-Date).AddDays(-365) - $a = m365 entra m365group conversation list --groupId $Group.Reference.id | ConvertFrom-Json | Where-Object { + $conversations = m365 entra m365group conversation list --groupId $Group.Reference.id | ConvertFrom-Json | Where-Object { [datetime]$_.lastDeliveredDateTime -lt $WarningDate } - if (!$a -or $a.Length -eq 0) { return } + if (!$conversations -or $conversations.Length -eq 0) { return } - Write-Host " → potentially obsolete ($($a.Length) conversation item$($a.Length -gt 1 ? 's' : '') created more than 1 year ago)" -ForegroundColor Yellow - $reason = "$($a.Length) conversation item$($a.Length -gt 1 ? 's' : '') created more than 1 year ago)" + Write-Host " → potentially obsolete ($($conversations.Length) conversation item$($conversations.Length -gt 1 ? 's' : '') created more than 1 year ago)" -ForegroundColor Yellow + $reason = "$($conversations.Length) conversation item$($conversations.Length -gt 1 ? 's' : '') created more than 1 year ago" $Group.MailboxStatus = @{ - OutdatedConversations = $a | Sort-Object -Property lastDeliveredDateTime + OutdatedConversations = $conversations | Sort-Object -Property lastDeliveredDateTime Reason = $reason } $Group.TestStatus = "🟡 Warning" @@ -256,7 +245,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete try { $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) } - catch { } + catch { Write-Information "Group was already added to the list of potentially obsolete groups" } } function New-Report { @@ -267,15 +256,15 @@ Use this script to create a report of all M365 groups that are possibly obsolete $exportObject = [ordered] @{ "Group Name" = $Group.Reference.displayName + Description = $Group.Reference.description "Managed by" = $Group.Membership.Owners ? $Group.Membership.Owners.displayName -join ", " : "n/a" Owners = $Group.Membership.Owners.Count Members = $Group.Membership.Members.Count Guests = $Group.Membership.Guests.Count "Group Status" = $Group.Membership.Status ?? "Normal" - Description = $Group.Reference.description - "Conversation Status" = $Group.MailboxStatus.Reason ?? "Normal" "Number of Conversations" = $Group.MailboxStatus.OutdatedConversations ? $Group.MailboxStatus.OutdatedConversations.Length : "n/a" "Last Conversation" = $Group.MailboxStatus.OutdatedConversations ? $Group.MailboxStatus.OutdatedConversations[0].lastDeliveredDateTime : "" + "Conversation Status" = $Group.MailboxStatus.Reason ?? "Normal" "Team enabled" = $Group.Reference.resourceProvisioningOptions -contains 'Team' ? "True" : "False" "SPO Status" = $Group.SharePointStatus.Reason ?? "Normal" "SPO Activity" = $Group.SharePointStatus ? "Low / No document library usage" : "Document library in use" From 34bdeaa6053d77d7f2b06176aac5004bb27cf5c4 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Sun, 22 Sep 2024 22:53:57 +0200 Subject: [PATCH 05/43] change console output to make the output path more precise for the user --- .../sample-scripts/entra/find-obsolete-m365-groups/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index 35d6c668973..4e307ad834b 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -102,7 +102,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete $tStamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") if ($TestPath -ne $true) { New-Item -ItemType directory -Path $Script:Path | Out-Null - Write-Host "Will create file in $($Script:Path): M365GroupsReport-{current date}.csv" -ForegroundColor Yellow + Write-Host "Will create file in $($Script:Path): M365GroupsReport-$tStamp.csv" -ForegroundColor Yellow } else { Write-Host "Following report file will be created in $($Script:Path): 'M365GroupsReport-$($tStamp).csv'." From bf41310974c77a448e739d351bdad2c69982a7cb Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Sun, 22 Sep 2024 22:54:43 +0200 Subject: [PATCH 06/43] change retrieval of owners and members to increase script performance --- .../sample-scripts/entra/find-obsolete-m365-groups/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index 4e307ad834b..cf76800fcb8 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -162,8 +162,8 @@ Use this script to create a report of all M365 groups that are possibly obsolete # Original lists $users = m365 entra m365group user list --groupId $Group.Reference.id | ConvertFrom-Json - $owners = m365 entra m365group user list --groupId $Group.Reference.id --role Owner | ConvertFrom-Json - $members = m365 entra m365group user list --groupId $Group.Reference.id --role Member | ConvertFrom-Json + $owners = $users | Where-Object { $_.roles -contains "Owner" } + $members = $users | Where-Object { $_.roles -contains "Member" } # Consider guests as users that also match the $Script:Guests array $guests = $users | Where-Object { $_.id -in $Script:Guests.id } From c8ca5af06806b98bd91bd2c2c5cbc83d9106f997 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Wed, 25 Sep 2024 23:35:46 +0200 Subject: [PATCH 07/43] fix error related to comparison in case that group is empty --- .../sample-scripts/entra/find-obsolete-m365-groups/index.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index cf76800fcb8..8e0bb7a5df7 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -169,7 +169,9 @@ Use this script to create a report of all M365 groups that are possibly obsolete $guests = $users | Where-Object { $_.id -in $Script:Guests.id } # Modify the $members list to only contain users that are not in the $owners list - $members = Compare-Object $members $owners -PassThru + if($null -ne $owners -and $null -ne $members) { + $members = Compare-Object $members $owners -PassThru + } $Group.Membership = [ordered] @{ Owners = $owners From 2b723ffb198860ad137ec680d18cc5b375fed145 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Wed, 25 Sep 2024 23:47:28 +0200 Subject: [PATCH 08/43] fix handling of groups with 0 or only 1 owner --- .../entra/find-obsolete-m365-groups/index.mdx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index 8e0bb7a5df7..1a61afc3741 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -179,8 +179,23 @@ Use this script to create a report of all M365 groups that are possibly obsolete Guests = $guests } - if ($owners.Count -eq 1 -and ($members.Count + $guests.Count) -eq 0) { - Write-Host " → potentially obsolete (abandoned group: only 1 owner left)" -ForegroundColor Yellow + if ($owners.Count -eq 0) { + Write-Host " → potentially obsolete (abandoned group: no owner)" -ForegroundColor Yellow + $reason = "Low user count" + + $Group.Membership.Status = "Abandoned ($reason)" + $Group.TestStatus = "🟡 Warning" + $Group.Reasons += $reason + + try { + $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) + } + catch { Write-Information "Group was already added to the list of potentially obsolete groups" } + return + } + + if ($owners.Count -le 1 -and ($members.Count + $guests.Count) -eq 0) { + Write-Host " → potentially obsolete (abandoned group: only $($owners.Count) owner left)" -ForegroundColor Yellow $reason = "Low user count" $Group.Membership.Status = "Abandoned ($reason)" From bc31a5bfa0566c311eb01f3a7e80b2790bc725e8 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Wed, 25 Sep 2024 23:49:22 +0200 Subject: [PATCH 09/43] change logging behavior (reverted) in order to avoid overflooding the console with unnecessary output --- .../entra/find-obsolete-m365-groups/index.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index 1a61afc3741..6875eb1ba25 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -190,7 +190,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete try { $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) } - catch { Write-Information "Group was already added to the list of potentially obsolete groups" } + catch { } return } @@ -205,7 +205,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete try { $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) } - catch { Write-Information "Group was already added to the list of potentially obsolete groups" } + catch { } } } @@ -232,7 +232,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete try { $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) } - catch { Write-Information "Group was already added to the list of potentially obsolete groups" } + catch { } } } @@ -262,7 +262,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete try { $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) } - catch { Write-Information "Group was already added to the list of potentially obsolete groups" } + catch { } } function New-Report { From 0c3779d944e9f667f702df6ca3bc6671e46709c1 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Wed, 25 Sep 2024 23:56:18 +0200 Subject: [PATCH 10/43] change member retrieval by excluding guests from the members' list --- .../sample-scripts/entra/find-obsolete-m365-groups/index.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index 6875eb1ba25..543b9c23923 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -163,9 +163,7 @@ Use this script to create a report of all M365 groups that are possibly obsolete # Original lists $users = m365 entra m365group user list --groupId $Group.Reference.id | ConvertFrom-Json $owners = $users | Where-Object { $_.roles -contains "Owner" } - $members = $users | Where-Object { $_.roles -contains "Member" } - - # Consider guests as users that also match the $Script:Guests array + $members = $users | Where-Object { $_.roles -contains "Member" -and $_.id -notin $Script:Guests.id } $guests = $users | Where-Object { $_.id -in $Script:Guests.id } # Modify the $members list to only contain users that are not in the $owners list From 1aadf5751baf7a61fd00127a93f221b31ff71b24 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Thu, 26 Sep 2024 01:13:46 +0200 Subject: [PATCH 11/43] change retrieval of groups and procedure to check whether the group is obsolete --- .../entra/find-obsolete-m365-groups/index.mdx | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index 543b9c23923..50b35f00384 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -234,35 +234,46 @@ Use this script to create a report of all M365 groups that are possibly obsolete } } - function Test-ConversationActivity { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] [GroupInfo] $Group - ) +function Test-ConversationActivity { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] [GroupInfo] $Group + ) - $WarningDate = (Get-Date).AddDays(-365) + $WarningDate = (Get-Date).AddDays(-365) - $conversations = m365 entra m365group conversation list --groupId $Group.Reference.id | ConvertFrom-Json | Where-Object { - [datetime]$_.lastDeliveredDateTime -lt $WarningDate - } - if (!$conversations -or $conversations.Length -eq 0) { return } + $conversations = m365 entra m365group conversation list --groupId $Group.Reference.id | ConvertFrom-Json | Sort-Object -Property lastDeliveredDateTime -Descending + $latestConversation = $conversations | Where-Object { + [datetime]$_.lastDeliveredDateTime -gt $WarningDate.Date + } | Select-Object -First 1 + + $Group.MailboxStatus = @{ + NumberOfConversations = $conversations.Length + LastConversation = $conversations ? $conversations[0].lastDeliveredDateTime : "n/a" + OutdatedConversations = 0 + Reason = "" + } - Write-Host " → potentially obsolete ($($conversations.Length) conversation item$($conversations.Length -gt 1 ? 's' : '') created more than 1 year ago)" -ForegroundColor Yellow - $reason = "$($conversations.Length) conversation item$($conversations.Length -gt 1 ? 's' : '') created more than 1 year ago" + # Return if there are no conversations or the latest conversation is not outdated + if (!$conversations -or $latestConversation.Count -eq 1) { return } + + $outdatedConversations = $conversations | Where-Object { + [datetime]$_.lastDeliveredDateTime -lt $WarningDate + } - $Group.MailboxStatus = @{ - OutdatedConversations = $conversations | Sort-Object -Property lastDeliveredDateTime - Reason = $reason - } - $Group.TestStatus = "🟡 Warning" - $Group.Reasons += $reason + Write-Host " → potentially obsolete ($($outdatedConversations.Length) conversation item$($outdatedConversations.Length -gt 1 ? 's' : '') created more than 1 year ago)" -ForegroundColor Yellow + $reason = "$($outdatedConversations.Length) conversation item$($outdatedConversations.Length -gt 1 ? 's' : '') created more than 1 year ago" - try { - $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) - } - catch { } - } + $Group.MailboxStatus.OutdatedConversations = $outdatedConversations | Sort-Object -Property lastDeliveredDateTime + $Group.MailboxStatus.Reason = $reason + $Group.TestStatus = "🟡 Warning" + $Group.Reasons += $reason + try { + $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) + } + catch { Write-Information "Group was already added to the list of potentially obsolete groups" } +} function New-Report { [CmdletBinding()] param ( From e43a560d1208849fca38445ae3606620e8f0fcfb Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Thu, 26 Sep 2024 01:14:29 +0200 Subject: [PATCH 12/43] add additional information to exported object (group conversation infos) --- .../sample-scripts/entra/find-obsolete-m365-groups/index.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index 50b35f00384..a2aa6a9d214 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -288,8 +288,8 @@ function Test-ConversationActivity { Members = $Group.Membership.Members.Count Guests = $Group.Membership.Guests.Count "Group Status" = $Group.Membership.Status ?? "Normal" - "Number of Conversations" = $Group.MailboxStatus.OutdatedConversations ? $Group.MailboxStatus.OutdatedConversations.Length : "n/a" - "Last Conversation" = $Group.MailboxStatus.OutdatedConversations ? $Group.MailboxStatus.OutdatedConversations[0].lastDeliveredDateTime : "" + "Number of Conversations" = $Group.MailboxStatus.NumberOfConversations ? $Group.MailboxStatus.NumberOfConversations : "n/a" + "Last Conversation" = $Group.MailboxStatus.LastConversation "Conversation Status" = $Group.MailboxStatus.Reason ?? "Normal" "Team enabled" = $Group.Reference.resourceProvisioningOptions -contains 'Team' ? "True" : "False" "SPO Status" = $Group.SharePointStatus.Reason ?? "Normal" From 310bd82e9aad882e35b6cf58b93ec8538bffb3e3 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Thu, 26 Sep 2024 01:21:05 +0200 Subject: [PATCH 13/43] change sample preview image --- .../assets/preview.png | Bin 68490 -> 68777 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/preview.png b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/assets/preview.png index d85816f034a180502b0757457c54bfe37c6bedca..9098eba2c65024baa09ef5d18bebbbece386de96 100644 GIT binary patch literal 68777 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 From 692a8adb51f4f694f39a44848c3dc01c410031ef Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Thu, 26 Sep 2024 01:22:04 +0200 Subject: [PATCH 14/43] fix formatting --- .../entra/find-obsolete-m365-groups/index.mdx | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx index a2aa6a9d214..8ed4cb4cab5 100644 --- a/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx +++ b/docs/docs/sample-scripts/entra/find-obsolete-m365-groups/index.mdx @@ -234,46 +234,47 @@ Use this script to create a report of all M365 groups that are possibly obsolete } } -function Test-ConversationActivity { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] [GroupInfo] $Group - ) + function Test-ConversationActivity { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] [GroupInfo] $Group + ) - $WarningDate = (Get-Date).AddDays(-365) - - $conversations = m365 entra m365group conversation list --groupId $Group.Reference.id | ConvertFrom-Json | Sort-Object -Property lastDeliveredDateTime -Descending - $latestConversation = $conversations | Where-Object { - [datetime]$_.lastDeliveredDateTime -gt $WarningDate.Date - } | Select-Object -First 1 - - $Group.MailboxStatus = @{ - NumberOfConversations = $conversations.Length - LastConversation = $conversations ? $conversations[0].lastDeliveredDateTime : "n/a" - OutdatedConversations = 0 - Reason = "" - } + $WarningDate = (Get-Date).AddDays(-365) + + $conversations = m365 entra m365group conversation list --groupId $Group.Reference.id | ConvertFrom-Json | Sort-Object -Property lastDeliveredDateTime -Descending + $latestConversation = $conversations | Where-Object { + [datetime]$_.lastDeliveredDateTime -gt $WarningDate.Date + } | Select-Object -First 1 + + $Group.MailboxStatus = @{ + NumberOfConversations = $conversations.Length + LastConversation = $conversations ? $conversations[0].lastDeliveredDateTime : "n/a" + OutdatedConversations = 0 + Reason = "" + } - # Return if there are no conversations or the latest conversation is not outdated - if (!$conversations -or $latestConversation.Count -eq 1) { return } - - $outdatedConversations = $conversations | Where-Object { - [datetime]$_.lastDeliveredDateTime -lt $WarningDate - } + # Return if there are no conversations or the latest conversation is not outdated + if (!$conversations -or $latestConversation.Count -eq 1) { return } + + $outdatedConversations = $conversations | Where-Object { + [datetime]$_.lastDeliveredDateTime -lt $WarningDate + } - Write-Host " → potentially obsolete ($($outdatedConversations.Length) conversation item$($outdatedConversations.Length -gt 1 ? 's' : '') created more than 1 year ago)" -ForegroundColor Yellow - $reason = "$($outdatedConversations.Length) conversation item$($outdatedConversations.Length -gt 1 ? 's' : '') created more than 1 year ago" + Write-Host " → potentially obsolete ($($outdatedConversations.Length) conversation item$($outdatedConversations.Length -gt 1 ? 's' : '') created more than 1 year ago)" -ForegroundColor Yellow + $reason = "$($outdatedConversations.Length) conversation item$($outdatedConversations.Length -gt 1 ? 's' : '') created more than 1 year ago" - $Group.MailboxStatus.OutdatedConversations = $outdatedConversations | Sort-Object -Property lastDeliveredDateTime - $Group.MailboxStatus.Reason = $reason - $Group.TestStatus = "🟡 Warning" - $Group.Reasons += $reason + $Group.MailboxStatus.OutdatedConversations = $outdatedConversations | Sort-Object -Property lastDeliveredDateTime + $Group.MailboxStatus.Reason = $reason + $Group.TestStatus = "🟡 Warning" + $Group.Reasons += $reason - try { - $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) + try { + $Global:ObsoleteGroups.Add($Group.Reference.id, $Group) + } + catch { Write-Information "Group was already added to the list of potentially obsolete groups" } } - catch { Write-Information "Group was already added to the list of potentially obsolete groups" } -} + function New-Report { [CmdletBinding()] param ( From 9bbfee14ef4695c4d98f348c68c960b80fcb3d7b Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Sun, 20 Oct 2024 23:54:02 +0200 Subject: [PATCH 15/43] remove option set dependency to reduce mandatory options --- src/m365/spo/commands/list/list-get.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/m365/spo/commands/list/list-get.ts b/src/m365/spo/commands/list/list-get.ts index 53476a01f56..c6f3412edba 100644 --- a/src/m365/spo/commands/list/list-get.ts +++ b/src/m365/spo/commands/list/list-get.ts @@ -44,7 +44,6 @@ class SpoListGetCommand extends SpoCommand { this.#initTelemetry(); this.#initOptions(); this.#initValidators(); - this.#initOptionSets(); } #initTelemetry(): void { @@ -101,10 +100,6 @@ class SpoListGetCommand extends SpoCommand { ); } - #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'title', 'url'] }); - } - public async commandAction(logger: Logger, args: CommandArgs): Promise { if (this.verbose) { await logger.logToStderr(`Retrieving information for list in site at ${args.options.webUrl}...`); From 6a5ec8697408935c29d14dbd845228b4b5038b56 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Sun, 20 Oct 2024 23:54:18 +0200 Subject: [PATCH 16/43] change description --- src/m365/spo/commands/list/list-get.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m365/spo/commands/list/list-get.ts b/src/m365/spo/commands/list/list-get.ts index c6f3412edba..288c14febcc 100644 --- a/src/m365/spo/commands/list/list-get.ts +++ b/src/m365/spo/commands/list/list-get.ts @@ -35,7 +35,7 @@ class SpoListGetCommand extends SpoCommand { } public get description(): string { - return 'Gets information about the specific list'; + return 'Gets information about the specific list or returns information about the default list in a site'; } constructor() { From e7d8518ccaadc07c5fa1c4262465ffeb5db1f81d Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Mon, 21 Oct 2024 00:07:02 +0200 Subject: [PATCH 17/43] change option selectors to retrieve default document library --- src/m365/spo/commands/list/list-get.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/m365/spo/commands/list/list-get.ts b/src/m365/spo/commands/list/list-get.ts index 288c14febcc..5165480ede9 100644 --- a/src/m365/spo/commands/list/list-get.ts +++ b/src/m365/spo/commands/list/list-get.ts @@ -107,6 +107,9 @@ class SpoListGetCommand extends SpoCommand { let requestUrl: string = `${args.options.webUrl}/_api/web/`; + if (!args.options.id && !args.options.title && !args.options.url) { + requestUrl += `DefaultDocumentLibrary`; + } if (args.options.id) { requestUrl += `lists(guid'${formatting.encodeQueryParameter(args.options.id)}')`; } From 01e32f24f08ab81cf20f74cb0db0657d540819c0 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Mon, 21 Oct 2024 01:23:25 +0200 Subject: [PATCH 18/43] add test --- src/m365/spo/commands/list/list-get.spec.ts | 23 ++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/m365/spo/commands/list/list-get.spec.ts b/src/m365/spo/commands/list/list-get.spec.ts index ad2786adfd0..bc87a8fa190 100644 --- a/src/m365/spo/commands/list/list-get.spec.ts +++ b/src/m365/spo/commands/list/list-get.spec.ts @@ -1281,4 +1281,25 @@ describe(commands.LIST_GET, () => { const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', id: '0CD891EF-AFCE-4E55-B836-FCE03286CCCF' } }, commandInfo); assert(actual); }); -}); + + it('retrieves the default list in the specified site by providing a webUrl', async () => { + const defaultSiteList = { ...listResponse, BaseTemplate: 101, ParentWebUrl: "/", ListItemEntityTypeFullName: "SP.Data.Shared_x0020_DocumentsItem" }; + + sinon.stub(request, 'get').resolves({ + ...defaultSiteList + }); + + await command.action(logger, { + options: { + title: 'Documents', + webUrl: 'https://contoso.sharepoint.com' + } + }); + + assert(loggerLogSpy.calledWithMatch({ + BaseTemplate: 101, + ParentWebUrl: "/", + ListItemEntityTypeFullName: "SP.Data.Shared_x0020_DocumentsItem" + })); + }); +}); \ No newline at end of file From 143c299009a909046765072d4498f585f7eed417 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Tue, 22 Oct 2024 08:41:27 +0200 Subject: [PATCH 19/43] change email address --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7133ed14288..ee9cbcfef27 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "Levert, Sebastien ", "Lingstuyl, Martin ", "Macháček, Martin ", - "Maestrini Tobias ", + "Maestrini, Tobias ", "Maillot, Michaël ", "Mastykarz, Waldek ", "McDonnell, Kevin ", From a59350d47adffbb4924e49e4db4a6085265bf0c2 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Tue, 22 Oct 2024 08:46:45 +0200 Subject: [PATCH 20/43] fix test definition to cover new implementation to retrieve the site's standard list --- src/m365/spo/commands/list/list-get.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/m365/spo/commands/list/list-get.spec.ts b/src/m365/spo/commands/list/list-get.spec.ts index bc87a8fa190..4dec616c95c 100644 --- a/src/m365/spo/commands/list/list-get.spec.ts +++ b/src/m365/spo/commands/list/list-get.spec.ts @@ -1291,7 +1291,6 @@ describe(commands.LIST_GET, () => { await command.action(logger, { options: { - title: 'Documents', webUrl: 'https://contoso.sharepoint.com' } }); From 2f640fbf1d4ee9fe502c226854f126f222cd7fb5 Mon Sep 17 00:00:00 2001 From: waldekmastykarz Date: Sat, 21 Sep 2024 11:16:36 +0200 Subject: [PATCH 21/43] Fixes casing and autocomplete for enums based on zod --- src/m365/commands/login.ts | 4 ++-- src/utils/zod.ts | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/m365/commands/login.ts b/src/m365/commands/login.ts index f57bf3a3855..e07168eafd8 100644 --- a/src/m365/commands/login.ts +++ b/src/m365/commands/login.ts @@ -12,8 +12,8 @@ import commands from './commands.js'; const options = globalOptionsZod .extend({ - authType: zod.alias('t', z.nativeEnum(AuthType).optional()), - cloud: z.nativeEnum(CloudType).optional().default(CloudType.Public), + authType: zod.alias('t', zod.coercedEnum(AuthType).optional()), + cloud: zod.coercedEnum(CloudType).optional().default(CloudType.Public), userName: zod.alias('u', z.string().optional()), password: zod.alias('p', z.string().optional()), certificateFile: zod.alias('c', z.string().optional() diff --git a/src/utils/zod.ts b/src/utils/zod.ts index fb55d1513e9..38838f8a906 100644 --- a/src/utils/zod.ts +++ b/src/utils/zod.ts @@ -89,7 +89,7 @@ function parseEnum(def: z.ZodEnumDef, _options: CommandOptionInfo[], currentOpti function parseNativeEnum(def: z.ZodNativeEnumDef, _options: CommandOptionInfo[], currentOption?: CommandOptionInfo): z.ZodTypeDef | undefined { if (currentOption) { currentOption.type = 'string'; - currentOption.autocomplete = Object.getOwnPropertyNames(def.values); + currentOption.autocomplete = Object.values(def.values).map(v => String(v)); } return; @@ -139,6 +139,11 @@ function parseDef(def: z.ZodTypeDef, options: CommandOptionInfo[], currentOption } while (parsedDef); } +type EnumLike = { + [k: string]: string | number + [nu: number]: string +}; + export const zod = { alias(alias: string, type: T): T { type._def.alias = alias; @@ -149,5 +154,17 @@ export const zod = { const options: CommandOptionInfo[] = []; parseDef(schema._def, options); return options; - } + }, + + coercedEnum: (e: T): z.ZodEffects, T[keyof T], unknown> => + z.preprocess(val => { + const target = String(val)?.toLowerCase(); + for (const k of Object.values(e)) { + if (String(k)?.toLowerCase() === target) { + return k; + } + } + + return null; + }, z.nativeEnum(e)) }; \ No newline at end of file From b06b28368f6198931ab101c9aafabe0ae5896bac Mon Sep 17 00:00:00 2001 From: waldekmastykarz Date: Sun, 6 Oct 2024 15:41:30 +0200 Subject: [PATCH 22/43] Updates docs contribution guide with Zod. Closes #6322 --- .../new-command/build-command-logic.mdx | 259 ++++++++++-------- .../contribute/new-command/unit-tests.mdx | 21 +- 2 files changed, 161 insertions(+), 119 deletions(-) diff --git a/docs/docs/contribute/new-command/build-command-logic.mdx b/docs/docs/contribute/new-command/build-command-logic.mdx index 0976b06df89..1bae95c42ce 100644 --- a/docs/docs/contribute/new-command/build-command-logic.mdx +++ b/docs/docs/contribute/new-command/build-command-logic.mdx @@ -64,58 +64,71 @@ export default { Next, to enhance our command with options, validators, telemetry, there are a bunch of methods already available for you. When you make use of a method we order them alphabetically. -## Defining the options +## Defining command options -When the command requires options to be passed along, we will define them in the interface `Options`. This interface extends from our `GlobalOptions`, where the common options `query`, `output`, `debug`, and `verbose` are defined. When an option is optional, let's make sure that it's also optional in the interface. +Some commands require options. For example, when you want to get a group, you need to specify the site URL and the group ID. In CLI for Microsoft 365 we define options in a [Zod](https://zod.dev/) schema. Using Zod allows us to define the options in a type-safe way so that we can easily convert arguments specified by the user in the command line to objects that can be used in the command logic. -We will also define the options in the method `#initOptions`. Here we pass along the option name, as a possible abbreviation for the option, to the `this.options` object. In some occasions, the option will always require a predefined input (an option with a fixed amount of choices). When this is the case, we can define them under the property `autocomplete`. +Global CLI options, such as `query`, `output`, `debug` or `verbose` are defined in the `globalOptionsZod` property. To define options specific to your command, extend the global schema with your command-specific options. -:::info +```ts title="src/m365/spo/commands/group/group-get.ts" +import { globalOptionsZod } from '../../Command.js'; + +export const options = globalOptionsZod + .extend({ + webUrl: z.string(), + id: z.number().optional(), + name: z.string().optional(), + associatedGroup: z.string().optional() + }) + .strict(); +``` -Required options are denoted as `--required `, optional options are denoted as `--optional [optional]`, and flag options are denoted as `--flag`. +In this example, we're adding 4 command options: `webUrl`, `id`, `name`, and `associatedGroup`. The `webUrl` option is required and must be a string. The `id`, `name`, and `associatedGroup` options are optional. Since our command doesn't support unknown options, we set the schema to strict. -::: +Next, we define a TypeScript type for options and command args, which allows us to benefit from type-safety when working with options in the command logic. ```ts title="src/m365/spo/commands/group/group-get.ts" -// ... -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../Command.js'; + +export const options = globalOptionsZod + .extend({ + webUrl: z.string(), + id: z.number().optional(), + name: z.string().optional(), + associatedGroup: z.string().optional() + }) + .strict(); +declare type Options = z.infer; interface CommandArgs { options: Options; } +``` -interface Options extends GlobalOptions { - webUrl: string; - id?: number; - name?: string; - associatedGroup?: string; +Finally, we expose the options schema in the command class. + +```ts title="src/m365/spo/commands/group/group-get.ts" +import { globalOptionsZod } from '../../Command.js'; + +export const options = globalOptionsZod + .extend({ + webUrl: z.string(), + id: z.number().optional(), + name: z.string().optional(), + associatedGroup: z.string().optional() + }) + .strict(); +declare type Options = z.infer; + +interface CommandArgs { + options: Options; } class SpoGroupGetCommand extends SpoCommand { // ... - constructor() { - super(); - - this.#initOptions(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-u, --webUrl ' - }, - { - option: '-i, --id [id]' - }, - { - option: '--name [name]' - }, - { - option: '--associatedGroup [associatedGroup]', - autocomplete: ['Owner', 'Member', 'Visitor'] - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -128,111 +141,135 @@ class SpoGroupGetCommand extends SpoCommand { } ``` -## Option validation +## Defining aliases -The options that are passed along won't always be correct from the first attempt. So instead of passing faulty values to the API, we can write option validation that runs before `commandAction` is executed. This can be done in the method `#initValidators`. Conditions can be written here to validate the option values and return an error when they're faulty. Once again, there are already several validation methods you can make use of to check some common options, e.g., `validation.isValidSharePointUrl(...)`. +To simplify using the CLI, we often use aliases for options. For example, the `--webUrl` option can be shortened to `-u`. To define an alias for an option, we wrap the property in the `alias` function in the schema. ```ts title="src/m365/spo/commands/group/group-get.ts" -// ... -import { validation } from '../../../../utils/validation.js'; - -class SpoExampleListCommand extends Command { - constructor() { - super(); +import { globalOptionsZod } from '../../Command.js'; +import { zod } from '../../utils/zod.js'; + +export const options = globalOptionsZod + .extend({ + webUrl: zod.alias('u', z.string()), + id: zod.alias('i', z.number().optional()), + name: z.string().optional(), + associatedGroup: z.string().optional() + }) + .strict(); +``` - // ... - this.#initValidators(); - } +## Defining option autocomplete - // ... +Some options require predefined values. For example, the `associatedGroup` option can only have one of the following values: `Owner`, `Member`, or `Visitor`. To define the allowed values for an option, we use enums with the `coercedEnum` helper function. This function allows users to specify the values in a case-insensitive way. In the code, the value of the `associatedGroup` option will be expressed as an enum which allows us to benefit from type-safety and support refactoring. - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.id && isNaN(args.options.id)) { - return `Specified id ${args.options.id} is not a number`; - } +```ts title="src/m365/spo/commands/group/group-get.ts" +import { globalOptionsZod } from '../../Command.js'; +import { zod } from '../../utils/zod.js'; - if (args.options.associatedGroup && ['owner', 'member', 'visitor'].indexOf(args.options.associatedGroup.toLowerCase()) === -1) { - return `${args.options.associatedGroup} is not a valid associatedGroup value. Allowed values are Owner|Member|Visitor.`; - } +enum AssociatedGroup { + Owner = 'owner', + Member = 'member', + Visitor = 'visitor' +}; - return validation.isValidSharePointUrl(args.options.webUrl); - } - ); - } -} +export const options = globalOptionsZod + .extend({ + webUrl: zod.alias('u', z.string()), + id: zod.alias('i', z.number().optional()), + name: z.string().optional(), + associatedGroup: zod.coercedEnum(AssociatedGroup).optional() + }) + .strict(); ``` -## Option sets +## Option validation -Option sets are used to ensure that only one option has a value from a set of options. When no option is used, the command will return an error, and the same goes when multiple of these options are used. To make use of the option sets, you can use the method `#initOptionSets`. +The options that users specify won't always be correct. So instead of passing faulty values to the CLI, we can add logic to validate the input first. This allows us to fail fast and offer users the ability to correct their input before continuing. -```ts title="src/m365/spo/commands/group/group-get.ts" -class SpoExampleListCommand extends Command { - constructor() { - super(); - - // ... - this.#initOptionSets(); - } +Zod automatically validates primitive values, which means that you only need to add 'business' validation logic. Validation is implemented using Zod refinements on the specific property. For example, to validate that the specified URL is a SharePoint URL, use: - // ... +```ts title="src/m365/spo/commands/group/group-get.ts" +import { globalOptionsZod } from '../../Command.js'; +import { validation } from '../../utils/validation.js'; +import { zod } from '../../utils/zod.js'; + +enum AssociatedGroup { + Owner = 'owner', + Member = 'member', + Visitor = 'visitor' +}; - #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'name', 'associatedGroup'] }); - } -} +export const options = globalOptionsZod + .extend({ + webUrl: zod.alias('u', z.string().refine(url => validation.isValidSharePointUrl(url) === true, url => ({ + message: `Specified URL ${url} is not a valid SharePoint URL`, + }))), + id: zod.alias('i', z.number().optional()), + name: z.string().optional(), + associatedGroup: zod.coercedEnum(AssociatedGroup).optional() + }) + .strict(); ``` -Option sets also allow you to define `runsWhen` for a set of options. This allows you to define a condition when the option set should be executed. E.g., when the option `title` is defined, either the option set `ownerGroupId` and `ownerGroupName` should be required. - -```ts title="src/m365/planner/commands/plan/plan-set.ts" - #initOptionSets(): void { - this.optionSets.push( - { - options: ['id', 'title', 'rosterId'] - }, - { - options: ['ownerGroupId', 'ownerGroupName'], - runsWhen: (args) => args.options.title !== undefined - } - ); - } -``` +A refinement takes two arguments: a predicate function and an error object. The predicate function is used to validate the input, and the error object is used to define the error message that will be displayed when the input is invalid. -## Telemetry +## Option sets -The CLI for Microsoft 365 tracks the usage of the different commands using Azure Application Insights. By default, for each command, the CLI tracks its name and whether it's been executed in debug/verbose mode or not. If your command has additional properties that should be included in the telemetry, you can define them by implementing the `#initTelemetry` method and adding your properties to the `this.telemetryProperties` object. +Option sets are used to ensure that only one option has a value from a set of options. When no option is used, the command will return an error, and the same goes when multiple of these options are used. To use option sets, add a Zod refinement on the whole schema, which gives you access to all schema properties. ```ts title="src/m365/spo/commands/group/group-get.ts" -class SpoExampleListCommand extends Command { - constructor() { - super(); +import { globalOptionsZod } from '../../Command.js'; +import { validation } from '../../utils/validation.js'; +import { zod } from '../../utils/zod.js'; + +enum AssociatedGroup { + Owner = 'owner', + Member = 'member', + Visitor = 'visitor' +}; - // ... - this.#initTelemetry(); - } +export const options = globalOptionsZod + .extend({ + webUrl: zod.alias('u', z.string().refine(url => validation.isValidSharePointUrl(url) === true, url => ({ + message: `Specified URL ${url} is not a valid SharePoint URL`, + }))), + id: zod.alias('i', z.number().optional()), + name: z.string().optional(), + associatedGroup: zod.coercedEnum(AssociatedGroup).optional() + }) + .strict(); +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} +class SpoGroupGetCommand extends SpoCommand { // ... - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: typeof args.options.id !== 'undefined', - name: typeof args.options.name !== 'undefined', - associatedGroup: !!args.options.associatedGroup + public get schema(): z.ZodTypeAny | undefined { + return options; + } + + public getRefinedSchema(schema: typeof options): z.ZodEffects | undefined { + return schema + .refine(options => options.id !== undefined || options.name !== undefined && !(options.id !== undefined && options.name !== undefined), { + message: `Either id or name is required, but not both.` }); - }); } -} -``` -:::note + public async commandAction(logger: Logger, args: CommandArgs): Promise { + if (this.verbose) { + await logger.logToStderr(`Retrieving information for group in site at ${args.options.webUrl}...`); + } -We are only listing the option options in `#initTelemetry`. Since the required options are always used, we don't include them in the telemetry. + // Command implementation goes here + } +} +``` -::: +In the `refine` function, you define how the option set should be validated. In this case, we check if either `id` or `name` is defined, but not both. If both are defined, the command returns an error. You can use different logic, depending on the requirements of your command. ## Command action diff --git a/docs/docs/contribute/new-command/unit-tests.mdx b/docs/docs/contribute/new-command/unit-tests.mdx index b431a5222ee..fbe9727b6da 100644 --- a/docs/docs/contribute/new-command/unit-tests.mdx +++ b/docs/docs/contribute/new-command/unit-tests.mdx @@ -6,7 +6,7 @@ Once you have correctly set up your local environment, you can run `npm run watc ::: -Each command must be accompanied by a set of unit tests. We aim for 100% code and branch coverage in every command file. To accomplish this, the CLI for Microsoft 365 makes use of `Sinon` and `Mocha`. `Mocha` is the JavaScript test framework that’s often used for testing Node.js applications. It’s a widely used testing tool for both JavaScript and TypeScript projects. `Sinon`, on the other hand, will be used to test a method that is required to interact with or call other external methods. Therefore, you need a utility to spy, stub, or mock those external methods. +Each command must be accompanied by a set of unit tests. We aim for 100% code and branch coverage in every command file. To accomplish this, the CLI for Microsoft 365 makes use of `Sinon` and `Mocha`. `Mocha` is the JavaScript test framework that's often used for testing Node.js applications. It's a widely used testing tool for both JavaScript and TypeScript projects. `Sinon`, on the other hand, will be used to test a method that is required to interact with or call other external methods. Therefore, you need a utility to spy, stub, or mock those external methods. To get started, we will need a file to work in. Each command will have its own test file that you can work in. These are the common files found in the `src` directory and end with `*.spec.ts`. For our example, we'll create a new one based on our sample issue, which is mentioned [here](./step-by-step-guide.mdx#new-command-get-site-group). We'll name it `group-get.spec.ts`. @@ -18,7 +18,6 @@ Each `Mocha` file that ends with `*.spec.ts` will contain several common interfa import commands from '../../commands.js'; describe(commands.GROUP_GET, () => { - before(() => { // Execute before running tests }); @@ -174,9 +173,9 @@ describe(commands.GROUP_GET, () => { }); ``` -### Testing `#initValidators` conditions +### Testing validation conditions -`#initValidators` will validate if the option input is correct with the input we require. For our scenario, we have a condition that will check whether or not the option `id` input is a number. Because this condition can pass and fail, we will need to write two tests. One for failure and one where the condition passes. +Zod validates if the option input is correct with the input we require. For our scenario, we have a condition that will check whether or not the option `id` input is a number. Because this condition can pass and fail, we will need to write two tests. One for failure and one where the condition passes. ```ts title="src/m365/spo/commands/group/group-get.spec.ts" // ... @@ -185,13 +184,19 @@ describe(commands.GROUP_GET, () => { // ... it('fails validation if the specified ID is not a number', async () => { - const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', id: 'a' } }, commandInfo); - assert.notStrictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + webUrl: 'https://contoso.sharepoint.com', + id: 'a' + }); + assert.strictEqual(actual.success, false); }); it('passes validation when the URL is valid and the ID is passed', async () => { - const actual = await command.validate({ options: { webUrl: 'https://contoso.sharepoint.com', id: 7 } }, commandInfo); - assert.strictEqual(actual, true); + const actual = commandOptionsSchema.safeParse({ + webUrl: 'https://contoso.sharepoint.com', + id: 7 + }); + assert.strictEqual(actual.success, true); }); }); ``` From b8e931a3222df7e90f342ac6576900b040443153 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Sun, 6 Oct 2024 23:10:57 +0200 Subject: [PATCH 23/43] Removes obsolete example. Closes #6272 --- docs/docs/cmd/entra/m365group/m365group-user-list.mdx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx index 48f3b0d13f1..e55e629639a 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx @@ -63,12 +63,6 @@ List specific properties for all group users from a group specified by ID. m365 entra m365group user list --groupId 03cba9da-3974-46c1-afaf-79caa2e45bbe --properties "id,jobTitle,companyName,accountEnabled" ``` -List all group members that are guest users. - -```sh -m365 entra m365group user list --groupDisplayName Developers --filter "userType eq 'Guest'" -``` - ## Response From d6ab19790787a792e73b756192252d26d173fcea Mon Sep 17 00:00:00 2001 From: Jwaegebaert <38426621+Jwaegebaert@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:06:17 +0200 Subject: [PATCH 24/43] Fixes 'm365 setup' app registration to use 'CLI for M365'. Closes #6367 --- src/m365/commands/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m365/commands/setup.ts b/src/m365/commands/setup.ts index 5844b95fe6d..16bd6a797a6 100644 --- a/src/m365/commands/setup.ts +++ b/src/m365/commands/setup.ts @@ -299,7 +299,7 @@ class SetupCommand extends AnonymousCommand { apisDelegated: (preferences.newEntraAppScopes === NewEntraAppScopes.All ? config.allScopes : config.minimalScopes).join(','), implicitFlow: false, multitenant: false, - name: 'CLI for Microsoft 365', + name: 'CLI for M365', platform: 'publicClient', redirectUris: 'http://localhost,https://localhost,https://login.microsoftonline.com/common/oauth2/nativeclient' }; From 46e40b63b8f890f365ff2330afd82edf815f8366 Mon Sep 17 00:00:00 2001 From: Nanddeep Nachan Date: Fri, 12 Jul 2024 10:01:07 +0000 Subject: [PATCH 25/43] Removes aad options and aliasses. Closes #5823, #5676 --- .../administrativeunit-add.mdx | 6 - .../administrativeunit-get.mdx | 6 - .../administrativeunit-list.mdx | 6 - .../administrativeunit-member-add.mdx | 6 - .../administrativeunit-member-get.mdx | 6 - .../administrativeunit-member-list.mdx | 6 - .../administrativeunit-remove.mdx | 6 - docs/docs/cmd/entra/app/app-add.mdx | 1 - docs/docs/cmd/entra/app/app-get.mdx | 1 - docs/docs/cmd/entra/app/app-list.mdx | 1 - .../docs/cmd/entra/app/app-permission-add.mdx | 1 - .../cmd/entra/app/app-permission-list.mdx | 6 - docs/docs/cmd/entra/app/app-remove.mdx | 1 - docs/docs/cmd/entra/app/app-role-add.mdx | 1 - docs/docs/cmd/entra/app/app-role-list.mdx | 1 - docs/docs/cmd/entra/app/app-role-remove.mdx | 1 - docs/docs/cmd/entra/app/app-set.mdx | 1 - .../approleassignment-add.mdx | 6 - .../approleassignment-list.mdx | 6 - .../approleassignment-remove.mdx | 6 - .../entra/enterpriseapp/enterpriseapp-add.mdx | 1 - .../entra/enterpriseapp/enterpriseapp-get.mdx | 1 - .../enterpriseapp/enterpriseapp-list.mdx | 1 - docs/docs/cmd/entra/group/group-add.mdx | 6 - docs/docs/cmd/entra/group/group-get.mdx | 6 - docs/docs/cmd/entra/group/group-list.mdx | 6 - .../cmd/entra/group/group-member-list.mdx | 6 - docs/docs/cmd/entra/group/group-remove.mdx | 6 - .../entra/groupsetting/groupsetting-add.mdx | 6 - .../entra/groupsetting/groupsetting-get.mdx | 6 - .../entra/groupsetting/groupsetting-list.mdx | 6 - .../groupsetting/groupsetting-remove.mdx | 6 - .../entra/groupsetting/groupsetting-set.mdx | 6 - .../groupsettingtemplate-get.mdx | 6 - .../groupsettingtemplate-list.mdx | 6 - docs/docs/cmd/entra/license/license-list.mdx | 6 - .../cmd/entra/m365group/m365group-add.mdx | 6 - .../m365group/m365group-conversation-list.mdx | 6 - .../m365group-conversation-post-list.mdx | 6 - .../cmd/entra/m365group/m365group-get.mdx | 6 - .../cmd/entra/m365group/m365group-list.mdx | 6 - .../m365group-recyclebinitem-clear.mdx | 6 - .../m365group-recyclebinitem-list.mdx | 6 - .../m365group-recyclebinitem-remove.mdx | 6 - .../m365group-recyclebinitem-restore.mdx | 6 - .../cmd/entra/m365group/m365group-remove.mdx | 6 - .../cmd/entra/m365group/m365group-renew.mdx | 6 - .../m365group-report-activitycounts.mdx | 6 - .../m365group-report-activitydetail.mdx | 6 - .../m365group-report-activityfilecounts.mdx | 6 - .../m365group-report-activitygroupcounts.mdx | 10 +- .../m365group-report-activitystorage.mdx | 10 +- .../cmd/entra/m365group/m365group-set.mdx | 8 +- .../cmd/entra/m365group/m365group-teamify.mdx | 16 +-- .../entra/m365group/m365group-user-add.mdx | 16 +-- .../entra/m365group/m365group-user-list.mdx | 6 - .../entra/m365group/m365group-user-remove.mdx | 24 ++-- .../entra/m365group/m365group-user-set.mdx | 10 -- .../cmd/entra/oauth2grant/oauth2grant-add.mdx | 12 +- .../entra/oauth2grant/oauth2grant-list.mdx | 8 +- .../entra/oauth2grant/oauth2grant-remove.mdx | 14 +-- .../cmd/entra/oauth2grant/oauth2grant-set.mdx | 12 +- docs/docs/cmd/entra/policy/policy-list.mdx | 12 +- .../siteclassification-disable.mdx | 12 +- .../siteclassification-enable.mdx | 20 ++-- .../siteclassification-get.mdx | 8 +- .../siteclassification-set.mdx | 22 ++-- docs/docs/cmd/entra/user/user-add.mdx | 14 +-- docs/docs/cmd/entra/user/user-get.mdx | 20 ++-- docs/docs/cmd/entra/user/user-guest-add.mdx | 12 +- docs/docs/cmd/entra/user/user-hibp.mdx | 10 +- docs/docs/cmd/entra/user/user-license-add.mdx | 6 - .../docs/cmd/entra/user/user-license-list.mdx | 6 - .../cmd/entra/user/user-license-remove.mdx | 10 +- docs/docs/cmd/entra/user/user-list.mdx | 8 +- .../cmd/entra/user/user-password-validate.mdx | 8 +- .../entra/user/user-recyclebinitem-clear.mdx | 10 +- .../entra/user/user-recyclebinitem-list.mdx | 6 - .../entra/user/user-recyclebinitem-remove.mdx | 10 +- .../user/user-recyclebinitem-restore.mdx | 6 - .../user/user-registrationdetails-list.mdx | 20 ++-- docs/docs/cmd/entra/user/user-remove.mdx | 10 +- docs/docs/cmd/entra/user/user-set.mdx | 6 - docs/docs/cmd/entra/user/user-signin-list.mdx | 24 ++-- docs/docs/cmd/spo/group/group-member-add.mdx | 22 ++-- .../cmd/spo/group/group-member-remove.mdx | 16 +-- docs/docs/cmd/spo/user/user-ensure.mdx | 7 +- src/m365/entra/aadCommands.ts | 89 --------------- .../administrativeunit-add.spec.ts | 11 -- .../administrativeunit-add.ts | 7 -- .../administrativeunit-get.spec.ts | 11 -- .../administrativeunit-get.ts | 7 -- .../administrativeunit-list.spec.ts | 11 -- .../administrativeunit-list.ts | 7 -- .../administrativeunit-member-add.spec.ts | 11 -- .../administrativeunit-member-add.ts | 7 -- .../administrativeunit-member-get.spec.ts | 11 -- .../administrativeunit-member-get.ts | 7 -- .../administrativeunit-member-list.spec.ts | 11 -- .../administrativeunit-member-list.ts | 7 -- .../administrativeunit-remove.spec.ts | 11 -- .../administrativeunit-remove.ts | 8 -- src/m365/entra/commands/app/app-add.spec.ts | 11 -- src/m365/entra/commands/app/app-add.ts | 7 -- src/m365/entra/commands/app/app-get.spec.ts | 11 -- src/m365/entra/commands/app/app-get.ts | 7 -- src/m365/entra/commands/app/app-list.spec.ts | 11 -- src/m365/entra/commands/app/app-list.ts | 7 -- .../commands/app/app-permission-add.spec.ts | 11 -- .../entra/commands/app/app-permission-add.ts | 7 -- .../entra/commands/app/app-remove.spec.ts | 11 -- src/m365/entra/commands/app/app-remove.ts | 7 -- .../entra/commands/app/app-role-add.spec.ts | 11 -- src/m365/entra/commands/app/app-role-add.ts | 7 -- .../entra/commands/app/app-role-list.spec.ts | 11 -- src/m365/entra/commands/app/app-role-list.ts | 7 -- .../commands/app/app-role-remove.spec.ts | 11 -- .../entra/commands/app/app-role-remove.ts | 7 -- src/m365/entra/commands/app/app-set.spec.ts | 11 -- src/m365/entra/commands/app/app-set.ts | 7 -- .../approleassignment-add.spec.ts | 11 -- .../approleassignment-add.ts | 7 -- .../approleassignment-list.spec.ts | 11 -- .../approleassignment-list.ts | 7 -- .../approleassignment-remove.spec.ts | 11 -- .../approleassignment-remove.ts | 7 -- .../enterpriseapp/enterpriseapp-add.spec.ts | 11 -- .../enterpriseapp/enterpriseapp-add.ts | 7 -- .../enterpriseapp/enterpriseapp-get.spec.ts | 11 -- .../enterpriseapp/enterpriseapp-get.ts | 7 -- .../enterpriseapp/enterpriseapp-list.spec.ts | 11 -- .../enterpriseapp/enterpriseapp-list.ts | 7 -- .../entra/commands/group/group-add.spec.ts | 11 -- src/m365/entra/commands/group/group-add.ts | 5 - .../entra/commands/group/group-get.spec.ts | 11 -- src/m365/entra/commands/group/group-get.ts | 7 -- .../entra/commands/group/group-list.spec.ts | 11 -- src/m365/entra/commands/group/group-list.ts | 7 -- .../commands/group/group-member-list.spec.ts | 11 -- .../entra/commands/group/group-member-list.ts | 7 -- .../entra/commands/group/group-remove.spec.ts | 11 -- src/m365/entra/commands/group/group-remove.ts | 7 -- .../groupsetting/groupsetting-add.spec.ts | 11 -- .../commands/groupsetting/groupsetting-add.ts | 7 -- .../groupsetting/groupsetting-get.spec.ts | 11 -- .../commands/groupsetting/groupsetting-get.ts | 7 -- .../groupsetting/groupsetting-list.spec.ts | 11 -- .../groupsetting/groupsetting-list.ts | 7 -- .../groupsetting/groupsetting-remove.spec.ts | 11 -- .../groupsetting/groupsetting-remove.ts | 7 -- .../groupsetting/groupsetting-set.spec.ts | 11 -- .../commands/groupsetting/groupsetting-set.ts | 7 -- .../groupsettingtemplate-get.spec.ts | 11 -- .../groupsettingtemplate-get.ts | 7 -- .../groupsettingtemplate-list.spec.ts | 11 -- .../groupsettingtemplate-list.ts | 7 -- .../commands/license/license-list.spec.ts | 11 -- .../entra/commands/license/license-list.ts | 7 -- .../commands/m365group/m365group-add.spec.ts | 11 -- .../entra/commands/m365group/m365group-add.ts | 7 -- .../m365group-conversation-list.spec.ts | 11 -- .../m365group/m365group-conversation-list.ts | 7 -- .../m365group-conversation-post-list.spec.ts | 18 +-- .../m365group-conversation-post-list.ts | 7 -- .../commands/m365group/m365group-get.spec.ts | 11 -- .../entra/commands/m365group/m365group-get.ts | 7 -- .../commands/m365group/m365group-list.spec.ts | 11 -- .../commands/m365group/m365group-list.ts | 7 -- .../m365group-recyclebinitem-clear.spec.ts | 11 -- .../m365group-recyclebinitem-clear.ts | 7 -- .../m365group-recyclebinitem-list.spec.ts | 11 -- .../m365group-recyclebinitem-list.ts | 7 -- .../m365group-recyclebinitem-remove.spec.ts | 11 -- .../m365group-recyclebinitem-remove.ts | 7 -- .../m365group-recyclebinitem-restore.spec.ts | 11 -- .../m365group-recyclebinitem-restore.ts | 7 -- .../m365group/m365group-remove.spec.ts | 11 -- .../commands/m365group/m365group-remove.ts | 7 -- .../m365group/m365group-renew.spec.ts | 11 -- .../commands/m365group/m365group-renew.ts | 7 -- .../m365group-report-activitycounts.spec.ts | 11 -- .../m365group-report-activitycounts.ts | 5 - .../m365group-report-activitydetail.spec.ts | 11 -- .../m365group-report-activitydetail.ts | 5 - ...365group-report-activityfilecounts.spec.ts | 13 +-- .../m365group-report-activityfilecounts.ts | 5 - ...65group-report-activitygroupcounts.spec.ts | 13 +-- .../m365group-report-activitygroupcounts.ts | 5 - .../m365group-report-activitystorage.spec.ts | 13 +-- .../m365group-report-activitystorage.ts | 5 - .../commands/m365group/m365group-set.spec.ts | 11 -- .../entra/commands/m365group/m365group-set.ts | 7 -- .../m365group/m365group-teamify.spec.ts | 11 -- .../commands/m365group/m365group-teamify.ts | 7 -- .../m365group/m365group-user-add.spec.ts | 12 -- .../commands/m365group/m365group-user-add.ts | 8 -- .../m365group/m365group-user-list.spec.ts | 11 -- .../commands/m365group/m365group-user-list.ts | 7 -- .../m365group/m365group-user-remove.spec.ts | 12 -- .../m365group/m365group-user-remove.ts | 8 -- .../m365group/m365group-user-set.spec.ts | 12 -- .../commands/m365group/m365group-user-set.ts | 9 -- .../oauth2grant/oauth2grant-add.spec.ts | 11 -- .../commands/oauth2grant/oauth2grant-add.ts | 7 -- .../oauth2grant/oauth2grant-list.spec.ts | 11 -- .../commands/oauth2grant/oauth2grant-list.ts | 7 -- .../oauth2grant/oauth2grant-remove.spec.ts | 11 -- .../oauth2grant/oauth2grant-remove.ts | 7 -- .../oauth2grant/oauth2grant-set.spec.ts | 11 -- .../commands/oauth2grant/oauth2grant-set.ts | 7 -- .../entra/commands/policy/policy-list.spec.ts | 11 -- src/m365/entra/commands/policy/policy-list.ts | 7 -- .../siteclassification-disable.spec.ts | 11 -- .../siteclassification-disable.ts | 7 -- .../siteclassification-enable.spec.ts | 11 -- .../siteclassification-enable.ts | 7 -- .../siteclassification-get.spec.ts | 11 -- .../siteclassification-get.ts | 7 -- .../siteclassification-set.spec.ts | 11 -- .../siteclassification-set.ts | 7 -- src/m365/entra/commands/user/user-add.spec.ts | 11 -- src/m365/entra/commands/user/user-add.ts | 7 -- src/m365/entra/commands/user/user-get.spec.ts | 21 +--- src/m365/entra/commands/user/user-get.ts | 7 -- .../commands/user/user-guest-add.spec.ts | 11 -- .../entra/commands/user/user-guest-add.ts | 7 -- .../entra/commands/user/user-hibp.spec.ts | 11 -- src/m365/entra/commands/user/user-hibp.ts | 7 -- .../commands/user/user-license-add.spec.ts | 11 -- .../entra/commands/user/user-license-add.ts | 7 -- .../commands/user/user-license-list.spec.ts | 11 -- .../entra/commands/user/user-license-list.ts | 7 -- .../commands/user/user-license-remove.spec.ts | 11 -- .../commands/user/user-license-remove.ts | 7 -- .../entra/commands/user/user-list.spec.ts | 11 -- src/m365/entra/commands/user/user-list.ts | 7 -- .../user/user-password-validate.spec.ts | 11 -- .../commands/user/user-password-validate.ts | 7 -- .../user/user-recyclebinitem-clear.spec.ts | 11 -- .../user/user-recyclebinitem-clear.ts | 7 -- .../user/user-recyclebinitem-list.spec.ts | 11 -- .../commands/user/user-recyclebinitem-list.ts | 7 -- .../user/user-recyclebinitem-remove.spec.ts | 11 -- .../user/user-recyclebinitem-remove.ts | 7 -- .../user/user-recyclebinitem-restore.spec.ts | 11 -- .../user/user-recyclebinitem-restore.ts | 7 -- .../user-registrationdetails-list.spec.ts | 11 -- .../user/user-registrationdetails-list.ts | 5 - .../entra/commands/user/user-remove.spec.ts | 11 -- src/m365/entra/commands/user/user-remove.ts | 7 -- src/m365/entra/commands/user/user-set.spec.ts | 11 -- src/m365/entra/commands/user/user-set.ts | 7 -- .../commands/user/user-signin-list.spec.ts | 11 -- .../entra/commands/user/user-signin-list.ts | 7 -- .../commands/group/group-member-add.spec.ts | 103 +++++------------- .../spo/commands/group/group-member-add.ts | 35 +----- .../group/group-member-remove.spec.ts | 15 +-- .../spo/commands/group/group-member-remove.ts | 28 +---- .../spo/commands/user/user-ensure.spec.ts | 27 ----- src/m365/spo/commands/user/user-ensure.ts | 17 +-- 260 files changed, 160 insertions(+), 2356 deletions(-) delete mode 100644 src/m365/entra/aadCommands.ts diff --git a/docs/docs/cmd/entra/administrativeunit/administrativeunit-add.mdx b/docs/docs/cmd/entra/administrativeunit/administrativeunit-add.mdx index 9f447889c2b..8c029bdea03 100644 --- a/docs/docs/cmd/entra/administrativeunit/administrativeunit-add.mdx +++ b/docs/docs/cmd/entra/administrativeunit/administrativeunit-add.mdx @@ -12,12 +12,6 @@ Creates a new administrative unit m365 entra administrativeunit add [options] ``` -## Alias - -```sh -m365 aad administrativeunit add [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/administrativeunit/administrativeunit-get.mdx b/docs/docs/cmd/entra/administrativeunit/administrativeunit-get.mdx index 3e4748bf758..8b3e4d93224 100644 --- a/docs/docs/cmd/entra/administrativeunit/administrativeunit-get.mdx +++ b/docs/docs/cmd/entra/administrativeunit/administrativeunit-get.mdx @@ -12,12 +12,6 @@ Gets information about a specific administrative unit m365 entra administrativeunit get [options] ``` -## Alias - -```sh -m365 aad administrativeunit get [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/administrativeunit/administrativeunit-list.mdx b/docs/docs/cmd/entra/administrativeunit/administrativeunit-list.mdx index ac644383da6..7c0734f3715 100644 --- a/docs/docs/cmd/entra/administrativeunit/administrativeunit-list.mdx +++ b/docs/docs/cmd/entra/administrativeunit/administrativeunit-list.mdx @@ -12,12 +12,6 @@ Retrieves a list of administrative units m365 entra administrativeunit list [options] ``` -## Alias - -```sh -m365 aad administrativeunit list [options] -``` - ## Options diff --git a/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-add.mdx b/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-add.mdx index d0af68b7432..ea48be2de45 100644 --- a/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-add.mdx +++ b/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-add.mdx @@ -10,12 +10,6 @@ Add a member (user, group, or device) to an administrative unit m365 entra administrativeunit member add [options] ``` -## Alias - -```sh -m365 aad administrativeunit member add [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-get.mdx b/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-get.mdx index a234e2e2330..424f7271a2e 100644 --- a/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-get.mdx +++ b/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-get.mdx @@ -12,12 +12,6 @@ Retrieves info about a specific member of an administrative unit m365 entra administrativeunit member get [options] ``` -## Alias - -```sh -m365 aad administrativeunit member get [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-list.mdx b/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-list.mdx index fe0922cc4c0..8de9b4fdd73 100644 --- a/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-list.mdx +++ b/docs/docs/cmd/entra/administrativeunit/administrativeunit-member-list.mdx @@ -12,12 +12,6 @@ Retrieves members (users, groups, or devices) of an administrative unit m365 entra administrativeunit member list [options] ``` -## Alias - -```sh -m365 aad administrativeunit member list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/administrativeunit/administrativeunit-remove.mdx b/docs/docs/cmd/entra/administrativeunit/administrativeunit-remove.mdx index 445d10fc340..43d9abc6223 100644 --- a/docs/docs/cmd/entra/administrativeunit/administrativeunit-remove.mdx +++ b/docs/docs/cmd/entra/administrativeunit/administrativeunit-remove.mdx @@ -10,12 +10,6 @@ Removes an administrative unit m365 entra administrativeunit remove [options] ``` -## Alias - -```sh -m365 aad administrativeunit remove [options] -``` - ## options ```md definition-list diff --git a/docs/docs/cmd/entra/app/app-add.mdx b/docs/docs/cmd/entra/app/app-add.mdx index f204a6094c8..c71ce0f7f02 100644 --- a/docs/docs/cmd/entra/app/app-add.mdx +++ b/docs/docs/cmd/entra/app/app-add.mdx @@ -15,7 +15,6 @@ m365 entra app add [options] ## Alias ```sh -m365 aad app add [options] m365 entra appregistration add [options] ``` diff --git a/docs/docs/cmd/entra/app/app-get.mdx b/docs/docs/cmd/entra/app/app-get.mdx index d340157cd6a..87fc9aa69b5 100644 --- a/docs/docs/cmd/entra/app/app-get.mdx +++ b/docs/docs/cmd/entra/app/app-get.mdx @@ -15,7 +15,6 @@ m365 entra app get [options] ## Alias ```sh -m365 aad app get [options] m365 entra appregistration get [options] ``` diff --git a/docs/docs/cmd/entra/app/app-list.mdx b/docs/docs/cmd/entra/app/app-list.mdx index 62b6aa66b71..2461a55ddac 100644 --- a/docs/docs/cmd/entra/app/app-list.mdx +++ b/docs/docs/cmd/entra/app/app-list.mdx @@ -15,7 +15,6 @@ m365 entra app list [options] ## Alias ```sh -m365 aad app list [options] m365 entra appregistration list [options] ``` diff --git a/docs/docs/cmd/entra/app/app-permission-add.mdx b/docs/docs/cmd/entra/app/app-permission-add.mdx index 4be61409cd6..5d193c9e043 100644 --- a/docs/docs/cmd/entra/app/app-permission-add.mdx +++ b/docs/docs/cmd/entra/app/app-permission-add.mdx @@ -15,7 +15,6 @@ m365 entra app permission add [options] ## Alias ```sh -m365 aad app permission add [options] m365 entra appregistration permission add [options] ``` diff --git a/docs/docs/cmd/entra/app/app-permission-list.mdx b/docs/docs/cmd/entra/app/app-permission-list.mdx index c568ccf1ad1..07952de3699 100644 --- a/docs/docs/cmd/entra/app/app-permission-list.mdx +++ b/docs/docs/cmd/entra/app/app-permission-list.mdx @@ -12,12 +12,6 @@ Lists the application and delegated permissions for a specified Entra Applicatio m365 entra app permission list [options] ``` -## Alias - -```sh -m365 aad app permission list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/app/app-remove.mdx b/docs/docs/cmd/entra/app/app-remove.mdx index cf37323a9ca..288df6891ef 100644 --- a/docs/docs/cmd/entra/app/app-remove.mdx +++ b/docs/docs/cmd/entra/app/app-remove.mdx @@ -13,7 +13,6 @@ m365 entra app remove [options] ## Alias ```sh -m365 aad app remove [options] m365 entra appregistration remove [options] ``` diff --git a/docs/docs/cmd/entra/app/app-role-add.mdx b/docs/docs/cmd/entra/app/app-role-add.mdx index b3d000b0eb6..f5922737c48 100644 --- a/docs/docs/cmd/entra/app/app-role-add.mdx +++ b/docs/docs/cmd/entra/app/app-role-add.mdx @@ -13,7 +13,6 @@ m365 entra app role add [options] ## Alias ```sh -m365 aad app role add [options] m365 entra appregistration role add [options] ``` diff --git a/docs/docs/cmd/entra/app/app-role-list.mdx b/docs/docs/cmd/entra/app/app-role-list.mdx index e14ac64a4b7..90dacd955ce 100644 --- a/docs/docs/cmd/entra/app/app-role-list.mdx +++ b/docs/docs/cmd/entra/app/app-role-list.mdx @@ -15,7 +15,6 @@ m365 entra app role list [options] ## Alias ```sh -m365 aad app role list [options] m365 entra appregistration role list [options] ``` diff --git a/docs/docs/cmd/entra/app/app-role-remove.mdx b/docs/docs/cmd/entra/app/app-role-remove.mdx index c962626d13a..478c145596c 100644 --- a/docs/docs/cmd/entra/app/app-role-remove.mdx +++ b/docs/docs/cmd/entra/app/app-role-remove.mdx @@ -13,7 +13,6 @@ m365 entra app role remove [options] ## Alias ```sh -m365 aad app role remove [options] m365 entra appregistration role remove [options] ``` diff --git a/docs/docs/cmd/entra/app/app-set.mdx b/docs/docs/cmd/entra/app/app-set.mdx index 58fac2e47c7..cda8afe9644 100644 --- a/docs/docs/cmd/entra/app/app-set.mdx +++ b/docs/docs/cmd/entra/app/app-set.mdx @@ -13,7 +13,6 @@ m365 entra app set [options] ## Alias ```sh -m365 aad app set [options] m365 entra appregistration set [options] ``` diff --git a/docs/docs/cmd/entra/approleassignment/approleassignment-add.mdx b/docs/docs/cmd/entra/approleassignment/approleassignment-add.mdx index b22254634d7..11778890b72 100644 --- a/docs/docs/cmd/entra/approleassignment/approleassignment-add.mdx +++ b/docs/docs/cmd/entra/approleassignment/approleassignment-add.mdx @@ -12,12 +12,6 @@ Adds service principal permissions also known as scopes and app role assignments m365 entra approleassignment add [options] ``` -## Alias - -```sh -m365 aad approleassignment add [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/approleassignment/approleassignment-list.mdx b/docs/docs/cmd/entra/approleassignment/approleassignment-list.mdx index 8bb2b18ad7e..4f1e2b57731 100644 --- a/docs/docs/cmd/entra/approleassignment/approleassignment-list.mdx +++ b/docs/docs/cmd/entra/approleassignment/approleassignment-list.mdx @@ -12,12 +12,6 @@ Lists app role assignments for the specified application registration m365 entra approleassignment list [options] ``` -## Alias - -```sh -m365 aad approleassignment list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/approleassignment/approleassignment-remove.mdx b/docs/docs/cmd/entra/approleassignment/approleassignment-remove.mdx index 87ae22f3dda..33cd74337e7 100644 --- a/docs/docs/cmd/entra/approleassignment/approleassignment-remove.mdx +++ b/docs/docs/cmd/entra/approleassignment/approleassignment-remove.mdx @@ -10,12 +10,6 @@ Deletes an app role assignment for the specified Entra application registration m365 entra approleassignment remove [options] ``` -## Alias - -```sh -m365 aad approleassignment remove [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-add.mdx b/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-add.mdx index 511a85a0380..92934c74d01 100644 --- a/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-add.mdx +++ b/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-add.mdx @@ -15,7 +15,6 @@ m365 entra enterpriseapp add [options] ## Alias ```sh -m365 aad sp add [options] m365 entra sp add [options] ``` diff --git a/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-get.mdx b/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-get.mdx index ed83fcc0863..ec9269db708 100644 --- a/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-get.mdx +++ b/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-get.mdx @@ -15,7 +15,6 @@ m365 entra enterpriseapp get [options] ## Alias ```sh -m365 aad sp get [options] m365 entra sp get [options] ``` diff --git a/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-list.mdx b/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-list.mdx index c3785dfc4e2..9e23c259b46 100644 --- a/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-list.mdx +++ b/docs/docs/cmd/entra/enterpriseapp/enterpriseapp-list.mdx @@ -15,7 +15,6 @@ m365 entra enterpriseapp list [options] ## Alias ```sh -m365 aad sp list [options] m365 entra sp list [options] ``` diff --git a/docs/docs/cmd/entra/group/group-add.mdx b/docs/docs/cmd/entra/group/group-add.mdx index dfa9ef81cb3..9c51e3977b9 100644 --- a/docs/docs/cmd/entra/group/group-add.mdx +++ b/docs/docs/cmd/entra/group/group-add.mdx @@ -12,12 +12,6 @@ Creates a Microsoft Entra group m365 entra group add [options] ``` -## Alias - -```sh -m365 aad group add [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/group/group-get.mdx b/docs/docs/cmd/entra/group/group-get.mdx index 2f370ecf72d..490559b0b2b 100644 --- a/docs/docs/cmd/entra/group/group-get.mdx +++ b/docs/docs/cmd/entra/group/group-get.mdx @@ -12,12 +12,6 @@ Gets information about the specified Entra group m365 entra group get [options] ``` -## Alias - -```sh -m365 aad group get [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/group/group-list.mdx b/docs/docs/cmd/entra/group/group-list.mdx index be9a4c7506b..da9b300b3a6 100644 --- a/docs/docs/cmd/entra/group/group-list.mdx +++ b/docs/docs/cmd/entra/group/group-list.mdx @@ -12,12 +12,6 @@ Lists Entra groups m365 entra group list [options] ``` -## Alias - -```sh -m365 aad group list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/group/group-member-list.mdx b/docs/docs/cmd/entra/group/group-member-list.mdx index 65dd320dad0..5840bf2dbde 100644 --- a/docs/docs/cmd/entra/group/group-member-list.mdx +++ b/docs/docs/cmd/entra/group/group-member-list.mdx @@ -12,12 +12,6 @@ Lists members of a specific Entra group m365 entra group member list [options] ``` -## Alias - -```sh -m365 aad group user list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/group/group-remove.mdx b/docs/docs/cmd/entra/group/group-remove.mdx index f5ae06c50fa..b9181d92202 100644 --- a/docs/docs/cmd/entra/group/group-remove.mdx +++ b/docs/docs/cmd/entra/group/group-remove.mdx @@ -10,12 +10,6 @@ Removes an Entra group m365 entra group remove [options] ``` -## Alias - -```sh -m365 aad group remove [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/groupsetting/groupsetting-add.mdx b/docs/docs/cmd/entra/groupsetting/groupsetting-add.mdx index 2203d36c9d9..dc181d5108a 100644 --- a/docs/docs/cmd/entra/groupsetting/groupsetting-add.mdx +++ b/docs/docs/cmd/entra/groupsetting/groupsetting-add.mdx @@ -12,12 +12,6 @@ Creates a group setting m365 entra groupsetting add [options] ``` -## Alias - -```sh -m365 aad groupsetting add [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/groupsetting/groupsetting-get.mdx b/docs/docs/cmd/entra/groupsetting/groupsetting-get.mdx index b53a8203fea..054b7f614a3 100644 --- a/docs/docs/cmd/entra/groupsetting/groupsetting-get.mdx +++ b/docs/docs/cmd/entra/groupsetting/groupsetting-get.mdx @@ -12,12 +12,6 @@ Gets information about the particular group setting m365 entra groupsetting get [options] ``` -## Alias - -```sh -m365 aad groupsetting get [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/groupsetting/groupsetting-list.mdx b/docs/docs/cmd/entra/groupsetting/groupsetting-list.mdx index 575919d819f..48190c87e0b 100644 --- a/docs/docs/cmd/entra/groupsetting/groupsetting-list.mdx +++ b/docs/docs/cmd/entra/groupsetting/groupsetting-list.mdx @@ -12,12 +12,6 @@ Lists Entra group settings m365 entra groupsetting list [options] ``` -## Alias - -```sh -m365 aad groupsetting list [options] -``` - ## Options diff --git a/docs/docs/cmd/entra/groupsetting/groupsetting-remove.mdx b/docs/docs/cmd/entra/groupsetting/groupsetting-remove.mdx index 19fed3f2ae5..ca5809b5eb1 100644 --- a/docs/docs/cmd/entra/groupsetting/groupsetting-remove.mdx +++ b/docs/docs/cmd/entra/groupsetting/groupsetting-remove.mdx @@ -10,12 +10,6 @@ Removes the particular group setting m365 entra groupsetting remove [options] ``` -## Alias - -```sh -m365 aad groupsetting remove [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/groupsetting/groupsetting-set.mdx b/docs/docs/cmd/entra/groupsetting/groupsetting-set.mdx index a444b65e596..7c599831406 100644 --- a/docs/docs/cmd/entra/groupsetting/groupsetting-set.mdx +++ b/docs/docs/cmd/entra/groupsetting/groupsetting-set.mdx @@ -10,12 +10,6 @@ Updates the particular group setting m365 entra groupsetting set [options] ``` -## Alias - -```sh -m365 aad groupsetting set [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/groupsettingtemplate/groupsettingtemplate-get.mdx b/docs/docs/cmd/entra/groupsettingtemplate/groupsettingtemplate-get.mdx index ef0d13715a9..8e6f0d12366 100644 --- a/docs/docs/cmd/entra/groupsettingtemplate/groupsettingtemplate-get.mdx +++ b/docs/docs/cmd/entra/groupsettingtemplate/groupsettingtemplate-get.mdx @@ -12,12 +12,6 @@ Gets information about the specified Entra group settings template m365 entra groupsettingtemplate get [options] ``` -## Alias - -```sh -m365 aad groupsettingtemplate get [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/groupsettingtemplate/groupsettingtemplate-list.mdx b/docs/docs/cmd/entra/groupsettingtemplate/groupsettingtemplate-list.mdx index b13b2f20b08..ccf18b83d4e 100644 --- a/docs/docs/cmd/entra/groupsettingtemplate/groupsettingtemplate-list.mdx +++ b/docs/docs/cmd/entra/groupsettingtemplate/groupsettingtemplate-list.mdx @@ -12,12 +12,6 @@ Lists Entra group settings templates m365 entra groupsettingtemplate list [options] ``` -## Alias - -```sh -m365 aad groupsettingtemplate list [options] -``` - ## Options diff --git a/docs/docs/cmd/entra/license/license-list.mdx b/docs/docs/cmd/entra/license/license-list.mdx index d46280f5655..d25cfa395e0 100644 --- a/docs/docs/cmd/entra/license/license-list.mdx +++ b/docs/docs/cmd/entra/license/license-list.mdx @@ -12,12 +12,6 @@ Lists commercial subscriptions that an organization has acquired m365 entra license list [options] ``` -## Alias - -```sh -m365 aad license list [options] -``` - ## Options diff --git a/docs/docs/cmd/entra/m365group/m365group-add.mdx b/docs/docs/cmd/entra/m365group/m365group-add.mdx index b7fed8d9a7e..6c2f885b410 100644 --- a/docs/docs/cmd/entra/m365group/m365group-add.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-add.mdx @@ -12,12 +12,6 @@ Creates a Microsoft 365 Group m365 entra m365group add [options] ``` -## Alias - -```sh -m365 aad m365group add [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx b/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx index 03c194dd968..1cf4dbcdde7 100644 --- a/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx @@ -12,12 +12,6 @@ Lists conversations for the specified Microsoft 365 group m365 entra m365group conversation list [options] ``` -## Alias - -```sh -m365 aad m365group conversation list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-conversation-post-list.mdx b/docs/docs/cmd/entra/m365group/m365group-conversation-post-list.mdx index f24890ed5a8..69f2953b13c 100644 --- a/docs/docs/cmd/entra/m365group/m365group-conversation-post-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-conversation-post-list.mdx @@ -12,12 +12,6 @@ Lists conversation posts of a Microsoft 365 group m365 entra m365group conversation post list [options] ``` -## Alias - -```sh -m365 aad m365group conversation post list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-get.mdx b/docs/docs/cmd/entra/m365group/m365group-get.mdx index dcef5d64baf..f7663c58ca3 100644 --- a/docs/docs/cmd/entra/m365group/m365group-get.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-get.mdx @@ -12,12 +12,6 @@ Gets information about the specified Microsoft 365 Group or Microsoft Teams team m365 entra m365group get [options] ``` -## Alias - -```sh -m365 aad m365group get [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-list.mdx b/docs/docs/cmd/entra/m365group/m365group-list.mdx index ea96328f2cc..57a3cd1bb5c 100644 --- a/docs/docs/cmd/entra/m365group/m365group-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-list.mdx @@ -12,12 +12,6 @@ Lists Microsoft 365 Groups in the current tenant m365 entra m365group list [options] ``` -## Alias - -```sh -m365 aad m365group list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-clear.mdx b/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-clear.mdx index 14bd60724ac..e65b2305576 100644 --- a/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-clear.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-clear.mdx @@ -10,12 +10,6 @@ Clears Microsoft 365 Groups from the recycle bin in the current tenant m365 entra m365group recyclebinitem clear [options] ``` -## Alias - -```sh -m365 aad m365group recyclebinitem clear [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-list.mdx b/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-list.mdx index 1c7014d6371..52075b45ded 100644 --- a/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-list.mdx @@ -12,12 +12,6 @@ Lists Groups from the recycle bin in the current tenant m365 entra m365group recyclebinitem list [options] ``` -## Alias - -```sh -m365 aad m365group recyclebinitem list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-remove.mdx index 3bbc122e05e..ad98cbabdc4 100644 --- a/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-remove.mdx @@ -10,12 +10,6 @@ Permanently deletes a Microsoft 365 Group from the recycle bin in the current te m365 entra m365group recyclebinitem remove [options] ``` -## Alias - -```sh -m365 aad m365group recyclebinitem remove [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-restore.mdx b/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-restore.mdx index 63cfcad014b..b32c69d3831 100644 --- a/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-restore.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-recyclebinitem-restore.mdx @@ -10,12 +10,6 @@ Restores a deleted Microsoft 365 Group m365 entra m365group recyclebinitem restore [options] ``` -## Alias - -```sh -m365 aad m365group recyclebinitem restore [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-remove.mdx index 98d4b630fb6..3b65202a7a2 100644 --- a/docs/docs/cmd/entra/m365group/m365group-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-remove.mdx @@ -10,12 +10,6 @@ Removes an Microsoft 365 Group m365 entra m365group remove [options] ``` -## Alias - -```sh -m365 aad m365group remove [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-renew.mdx b/docs/docs/cmd/entra/m365group/m365group-renew.mdx index f1d85a9c0ec..bafec1f8263 100644 --- a/docs/docs/cmd/entra/m365group/m365group-renew.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-renew.mdx @@ -10,12 +10,6 @@ Renews Microsoft 365 group's expiration m365 entra m365group renew [options] ``` -## Alias - -```sh -m365 aad m365group renew [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-report-activitycounts.mdx b/docs/docs/cmd/entra/m365group/m365group-report-activitycounts.mdx index 12b929e1fa8..0c1d32ca15a 100644 --- a/docs/docs/cmd/entra/m365group/m365group-report-activitycounts.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-report-activitycounts.mdx @@ -12,12 +12,6 @@ Get the number of group activities across group workloads m365 entra m365group report activitycounts [options] ``` -## Alias - -```sh -m365 aad m365group report activitycounts [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-report-activitydetail.mdx b/docs/docs/cmd/entra/m365group/m365group-report-activitydetail.mdx index 621334b4cef..66b4ad6c177 100644 --- a/docs/docs/cmd/entra/m365group/m365group-report-activitydetail.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-report-activitydetail.mdx @@ -12,12 +12,6 @@ Get details about Microsoft 365 Groups activity by group. m365 entra m365group report activitydetail [options] ``` -## Alias - -```sh -m365 aad m365group report activitydetail [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-report-activityfilecounts.mdx b/docs/docs/cmd/entra/m365group/m365group-report-activityfilecounts.mdx index 19fe15412c5..9f120d21a7e 100644 --- a/docs/docs/cmd/entra/m365group/m365group-report-activityfilecounts.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-report-activityfilecounts.mdx @@ -12,12 +12,6 @@ Get the total number of files and how many of them were active across all group m365 entra m365group report activityfilecounts [options] ``` -## Alias - -```sh -m365 aad m365group report activityfilecounts [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-report-activitygroupcounts.mdx b/docs/docs/cmd/entra/m365group/m365group-report-activitygroupcounts.mdx index b5e915437ad..c95d4cbd32c 100644 --- a/docs/docs/cmd/entra/m365group/m365group-report-activitygroupcounts.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-report-activitygroupcounts.mdx @@ -12,20 +12,14 @@ Get the daily total number of groups and how many of them were active based on e m365 entra m365group report activitygroupcounts [options] ``` -## Alias - -```sh -m365 aad m365group report activitygroupcounts [options] -``` - ## Options ```md definition-list `-p, --period ` -: The length of time over which the report is aggregated. Supported values `D7`, `D30`, `D90`, `D180` +: The length of time over which the report is aggregated. Supported values `D7`, `D30`, `D90`, `D180`. `--outputFile [outputFile]` -: Path to the file where the Microsoft 365 Groups activities report should be stored in +: Path to the file where the Microsoft 365 Groups activities report should be stored in. ``` diff --git a/docs/docs/cmd/entra/m365group/m365group-report-activitystorage.mdx b/docs/docs/cmd/entra/m365group/m365group-report-activitystorage.mdx index cac96591875..9cd71abd568 100644 --- a/docs/docs/cmd/entra/m365group/m365group-report-activitystorage.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-report-activitystorage.mdx @@ -12,20 +12,14 @@ Get the total storage used across all group mailboxes and group sites m365 entra m365group report activitystorage [options] ``` -## Alias - -```sh -m365 aad m365group report activitystorage [options] -``` - ## Options ```md definition-list `-p, --period ` -: The length of time over which the report is aggregated. Supported values `D7`, `D30`, `D90`, `D180` +: The length of time over which the report is aggregated. Supported values `D7`, `D30`, `D90`, `D180`. `--outputFile [outputFile]` -: Path to the file where the Microsoft 365 Groups activities report should be stored in +: Path to the file where the Microsoft 365 Groups activities report should be stored in. ``` diff --git a/docs/docs/cmd/entra/m365group/m365group-set.mdx b/docs/docs/cmd/entra/m365group/m365group-set.mdx index d3e3db2cd9f..3690677b2ff 100644 --- a/docs/docs/cmd/entra/m365group/m365group-set.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-set.mdx @@ -10,12 +10,6 @@ Updates Microsoft 365 Group properties m365 entra m365group set [options] ``` -## Alias - -```sh -m365 aad m365group set [options] -``` - ## Options ```md definition-list @@ -44,7 +38,7 @@ m365 aad m365group set [options] : Comma-separated list of UPNs of Microsoft Entra ID users that will be group members. Specify either `memberIds` or `memberUserNames`, but not both. `--isPrivate [isPrivate]` -: Set to `true` if the Microsoft 365 Group should be private and `false` if it should be public. +: Set to `true` if the Microsoft 365 Group should be private and `false` if it should be public. `-l, --logoPath [logoPath]` : Local path to the image file to use as group logo. diff --git a/docs/docs/cmd/entra/m365group/m365group-teamify.mdx b/docs/docs/cmd/entra/m365group/m365group-teamify.mdx index ebc8b4859ad..4f31bb9f458 100644 --- a/docs/docs/cmd/entra/m365group/m365group-teamify.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-teamify.mdx @@ -10,33 +10,27 @@ Creates a new Microsoft Teams team under existing Microsoft 365 group m365 entra m365group teamify [options] ``` -## Alias - -```sh -m365 aad m365group teamify [options] -``` - ## Options ```md definition-list `-i, --id [id]` -: The ID of the Microsoft 365 Group to connect to Microsoft Teams. Specify either id or mailNickname but not both +: The ID of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id` or `mailNickname` but not both. `--mailNickname [mailNickname]` -: The mail alias of the Microsoft 365 Group to connect to Microsoft Teams. Specify either id or mailNickname but not both +: The mail alias of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id` or `mailNickname` but not both. ``` ## Examples -Creates a new Microsoft Teams team under existing Microsoft 365 group with id _e3f60f99-0bad-481f-9e9f-ff0f572fbd03_ +Creates a new Microsoft Teams team under existing Microsoft 365 group with the specified id. ```sh m365 entra m365group teamify --id e3f60f99-0bad-481f-9e9f-ff0f572fbd03 ``` -Creates a new Microsoft Teams team under existing Microsoft 365 group with mailNickname _GroupName_ +Creates a new Microsoft Teams team under existing Microsoft 365 group with the specified mailNickname. ```sh m365 entra m365group teamify --mailNickname GroupName @@ -44,4 +38,4 @@ m365 entra m365group teamify --mailNickname GroupName ## Response -The command won't return a response on success. +The command won't return a response on success. \ No newline at end of file diff --git a/docs/docs/cmd/entra/m365group/m365group-user-add.mdx b/docs/docs/cmd/entra/m365group/m365group-user-add.mdx index 3cfcb119345..8f708e970a7 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-add.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-add.mdx @@ -12,12 +12,6 @@ m365 entra m365group user add [options] ## Alias -```sh -m365 aad m365group user add [options] -``` - -## Alias - ```sh m365 teams user add ``` @@ -44,32 +38,32 @@ m365 teams user add : The user principal names of users. You can also pass a comma-separated list of UPNs. Specify either `ids` or `userNames` but not both. `-r, --role [role]` -: The role to be assigned to the new user: `Owner,Member`. Default `Member` +: The role to be assigned to the new user: `Owner,Member`. Default `Member`. ``` ## Examples -Add a new member with the userNames parameter to the specified Microsoft 365 Group +Add a new member with the userNames parameter to the specified Microsoft 365 Group. ```sh m365 entra m365group user add --groupId '00000000-0000-0000-0000-000000000000' --userNames 'anne.matthews@contoso.onmicrosoft.com' ``` -Add multiple new owners with the userNames parameter to the specified Microsoft 365 Group +Add multiple new owners with the userNames parameter to the specified Microsoft 365 Group. ```sh m365 entra m365group user add --groupName 'Contoso' --userNames 'anne.matthews@contoso.onmicrosoft.com, john.doe@contoso.onmicrosoft.com' --role Owner ``` -Add a new member with the userNames parameter to the specified Microsoft Teams team +Add a new member with the userNames parameter to the specified Microsoft Teams team. ```sh m365 entra m365group member add --teamId '00000000-0000-0000-0000-000000000000' --userNames 'anne.matthews@contoso.onmicrosoft.com' --role Member ``` -Add multiple new members with the ids parameter to the specified Microsoft Teams team +Add multiple new members with the ids parameter to the specified Microsoft Teams team. ```sh m365 entra m365group user add --teamName 'Engineering' --ids '74a3b772-3122-447b-b9da-10895e238219,dd3d21e4-a142-46b9-8482-bca8fe9596b3' --role Member diff --git a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx index e55e629639a..74524fe8469 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx @@ -12,12 +12,6 @@ Lists users for the specified Microsoft 365 group m365 entra m365group user list [options] ``` -## Alias - -```sh -m365 aad m365group user list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx index 57d8f1d6930..e560924f197 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx @@ -10,30 +10,20 @@ Removes the specified user from specified Microsoft 365 Group or Microsoft Teams m365 entra m365group user remove [options] ``` -## Alias - -```sh -m365 aad m365group user remove [options] -``` - -```sh -m365 aad teams user remove -``` - ## Options ```md definition-list `-i, --groupId [groupId]` -: The ID of the Microsoft 365 Group from which to remove the user +: The ID of the Microsoft 365 Group from which to remove the user. `--teamId [teamId]` -: The ID of the Microsoft Teams team from which to remove the user +: The ID of the Microsoft Teams team from which to remove the user. `-n, --userName ` -: User's UPN (user principal name), eg. `johndoe@example.com` +: User's UPN (user principal name), eg. `johndoe@example.com`. `-f, --force` -: Don't prompt for confirming removing the user from the specified Microsoft 365 Group or Microsoft Teams team +: Don't prompt for confirming removing the user from the specified Microsoft 365 Group or Microsoft Teams team. ``` @@ -44,19 +34,19 @@ You can remove users from a Microsoft 365 Group or Microsoft Teams team if you a ## Examples -Removes user from the specified Microsoft 365 Group +Removes user from the specified Microsoft 365 Group. ```sh m365 entra m365group user remove --groupId '00000000-0000-0000-0000-000000000000' --userName 'anne.matthews@contoso.onmicrosoft.com' ``` -Removes user from the specified Microsoft 365 Group without confirmation +Removes user from the specified Microsoft 365 Group without confirmation. ```sh m365 entra m365group user remove --groupId '00000000-0000-0000-0000-000000000000' --userName 'anne.matthews@contoso.onmicrosoft.com' --force ``` -Removes user from the specified Microsoft Teams team +Removes user from the specified Microsoft Teams team. ```sh m365 entra teams user remove --teamId '00000000-0000-0000-0000-000000000000' --userName 'anne.matthews@contoso.onmicrosoft.com' diff --git a/docs/docs/cmd/entra/m365group/m365group-user-set.mdx b/docs/docs/cmd/entra/m365group/m365group-user-set.mdx index fcebdb3a6fe..f2ac7cc1451 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-set.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-set.mdx @@ -10,16 +10,6 @@ Updates role of the specified users in the specified Microsoft 365 Group or Micr m365 entra m365group user set [options] ``` -## Alias - -```sh -m365 aad m365group user set [options] -``` - -```sh -m365 aad teams user set -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/oauth2grant/oauth2grant-add.mdx b/docs/docs/cmd/entra/oauth2grant/oauth2grant-add.mdx index db3b6116e44..416373f3002 100644 --- a/docs/docs/cmd/entra/oauth2grant/oauth2grant-add.mdx +++ b/docs/docs/cmd/entra/oauth2grant/oauth2grant-add.mdx @@ -10,23 +10,17 @@ Grant the specified service principal OAuth2 permissions to the specified resour m365 entra oauth2grant add [options] ``` -## Alias - -```sh -m365 aad oauth2grant add [options] -``` - ## Options ```md definition-list `-i, --clientId ` -: `objectId` of the service principal for which permissions should be granted +: `objectId` of the service principal for which permissions should be granted. `-r, --resourceId ` -: `objectId` of the Entra application to which permissions should be granted +: `objectId` of the Entra application to which permissions should be granted. `-s, --scope ` -: Permissions to grant +: Permissions to grant. ``` diff --git a/docs/docs/cmd/entra/oauth2grant/oauth2grant-list.mdx b/docs/docs/cmd/entra/oauth2grant/oauth2grant-list.mdx index 3e96b9624fb..0024fd3fef7 100644 --- a/docs/docs/cmd/entra/oauth2grant/oauth2grant-list.mdx +++ b/docs/docs/cmd/entra/oauth2grant/oauth2grant-list.mdx @@ -12,17 +12,11 @@ Lists OAuth2 permission grants for the specified service principal m365 entra oauth2grant list [options] ``` -## Alias - -```sh -m365 aad oauth2grant list [options] -``` - ## Options ```md definition-list `-i, --spObjectId ` -: objectId of the service principal for which the configured OAuth2 permission grants should be retrieved +: objectId of the service principal for which the configured OAuth2 permission grants should be retrieved. ``` diff --git a/docs/docs/cmd/entra/oauth2grant/oauth2grant-remove.mdx b/docs/docs/cmd/entra/oauth2grant/oauth2grant-remove.mdx index b9cd9efa91d..0d65ea3d6e5 100644 --- a/docs/docs/cmd/entra/oauth2grant/oauth2grant-remove.mdx +++ b/docs/docs/cmd/entra/oauth2grant/oauth2grant-remove.mdx @@ -10,20 +10,14 @@ Remove specified service principal OAuth2 permissions m365 entra oauth2grant remove [options] ``` -## Alias - -```sh -m365 aad oauth2grant remove [options] -``` - ## Options ```md definition-list `-i, --grantId ` -: `objectId` of OAuth2 permission grant to remove +: `objectId` of OAuth2 permission grant to remove. `-f, --force` -: Do not prompt for confirmation before removing OAuth2 permission grant +: Do not prompt for confirmation before removing OAuth2 permission grant. ``` @@ -40,13 +34,13 @@ m365 entra oauth2grant remove --grantId \\-Zc1JRY8REeLxmXz5KtixAYU3Q6noCBPlhwGiX ## Examples -Remove the OAuth2 permission grant with ID _YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek_ +Remove the OAuth2 permission grant with the specified ID. ```sh m365 entra oauth2grant remove --grantId YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek ``` -Remove the OAuth2 permission grant with ID _YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek_ without being asked for confirmation +Remove the OAuth2 permission grant with the specified ID without being asked for confirmation ```sh m365 entra oauth2grant remove --grantId YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek --force diff --git a/docs/docs/cmd/entra/oauth2grant/oauth2grant-set.mdx b/docs/docs/cmd/entra/oauth2grant/oauth2grant-set.mdx index 235a88dcd1d..23cbff2c390 100644 --- a/docs/docs/cmd/entra/oauth2grant/oauth2grant-set.mdx +++ b/docs/docs/cmd/entra/oauth2grant/oauth2grant-set.mdx @@ -10,20 +10,14 @@ Update OAuth2 permissions for the service principal m365 entra oauth2grant set [options] ``` -## Alias - -```sh -m365 aad oauth2grant set [options] -``` - ## Options ```md definition-list `-i, --grantId ` -: `objectId` of OAuth2 permission grant to update +: `objectId` of OAuth2 permission grant to update. `-s, --scope ` -: Permissions to grant +: Permissions to grant. ``` @@ -40,7 +34,7 @@ m365 entra oauth2grant set --grantId \\-Zc1JRY8REeLxmXz5KtixAYU3Q6noCBPlhwGiX7px ## Examples -Update the existing OAuth2 permission grant with ID _YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek_ to the _Calendars.Read Mail.Read_ permissions +Update the existing OAuth2 permission grant with ID _YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek_ to the _Calendars.Read Mail.Read_ permissions. ```sh m365 entra oauth2grant set --grantId YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek --scope "Calendars.Read Mail.Read" diff --git a/docs/docs/cmd/entra/policy/policy-list.mdx b/docs/docs/cmd/entra/policy/policy-list.mdx index 2e7c36042c6..cecde3e994d 100644 --- a/docs/docs/cmd/entra/policy/policy-list.mdx +++ b/docs/docs/cmd/entra/policy/policy-list.mdx @@ -12,30 +12,24 @@ Returns policies from Entra ID m365 entra policy list [options] ``` -## Alias - -```sh -m365 aad policy list [options] -``` - ## Options ```md definition-list `-t, --type [type]` -: The type of policies to return. Allowed values `activityBasedTimeout`, `authorization`, `claimsMapping`, `homeRealmDiscovery`, `identitySecurityDefaultsEnforcement`, `tokenIssuance`, `tokenLifetime`. If omitted, all policies are returned +: The type of policies to return. Allowed values `activityBasedTimeout`, `authorization`, `claimsMapping`, `homeRealmDiscovery`, `identitySecurityDefaultsEnforcement`, `tokenIssuance`, `tokenLifetime`. If omitted, all policies are returned. ``` ## Examples -Returns all policies from Entra ID +Returns all policies from Entra ID. ```sh m365 entra policy list ``` -Returns claim-mapping policies from Entra ID +Returns claim-mapping policies from Entra ID. ```sh m365 entra policy list --type "claimsMapping" diff --git a/docs/docs/cmd/entra/siteclassification/siteclassification-disable.mdx b/docs/docs/cmd/entra/siteclassification/siteclassification-disable.mdx index 3f338393686..05d786f0022 100644 --- a/docs/docs/cmd/entra/siteclassification/siteclassification-disable.mdx +++ b/docs/docs/cmd/entra/siteclassification/siteclassification-disable.mdx @@ -10,30 +10,24 @@ Disables site classification m365 entra siteclassification disable [options] ``` -## Alias - -```sh -m365 aad siteclassification disable [options] -``` - ## Options ```md definition-list `-f, --force` -: Don't prompt for confirming disabling site classification +: Don't prompt for confirming disabling site classification. ``` ## Examples -Disable site classification +Disable site classification. ```sh m365 entra siteclassification disable ``` -Disable site classification without confirmation +Disable site classification without confirmation. ```sh m365 entra siteclassification disable --force diff --git a/docs/docs/cmd/entra/siteclassification/siteclassification-enable.mdx b/docs/docs/cmd/entra/siteclassification/siteclassification-enable.mdx index e3c037dd66b..3c6a99d53ea 100644 --- a/docs/docs/cmd/entra/siteclassification/siteclassification-enable.mdx +++ b/docs/docs/cmd/entra/siteclassification/siteclassification-enable.mdx @@ -10,45 +10,39 @@ Enables site classification configuration m365 entra siteclassification enable [options] ``` -## Alias - -```sh -m365 aad siteclassification enable [options] -``` - ## Options ```md definition-list `-c, --classifications ` -: Comma-separated list of classifications to enable in the tenant +: Comma-separated list of classifications to enable in the tenant. `-d, --defaultClassification ` -: Classification to use by default +: Classification to use by default. `-u, --usageGuidelinesUrl [usageGuidelinesUrl]` -: URL with usage guidelines for members +: URL with usage guidelines for members. `-g, --guestUsageGuidelinesUrl [guestUsageGuidelinesUrl]` -: URL with usage guidelines for guests +: URL with usage guidelines for guests. ``` ## Examples -Enable site classification +Enable site classification. ```sh m365 entra siteclassification enable --classifications "High, Medium, Low" --defaultClassification "Medium" ``` -Enable site classification with a usage guidelines URL +Enable site classification with a usage guidelines URL. ```sh m365 entra siteclassification enable --classifications "High, Medium, Low" --defaultClassification "Medium" --usageGuidelinesUrl "http://aka.ms/pnp" ``` -Enable site classification with usage guidelines URLs for guests and members +Enable site classification with usage guidelines URLs for guests and members. ```sh m365 entra siteclassification enable --classifications "High, Medium, Low" --defaultClassification "Medium" --usageGuidelinesUrl "http://aka.ms/pnp" --guestUsageGuidelinesUrl "http://aka.ms/pnp" diff --git a/docs/docs/cmd/entra/siteclassification/siteclassification-get.mdx b/docs/docs/cmd/entra/siteclassification/siteclassification-get.mdx index 5069a976e88..514a8a92a21 100644 --- a/docs/docs/cmd/entra/siteclassification/siteclassification-get.mdx +++ b/docs/docs/cmd/entra/siteclassification/siteclassification-get.mdx @@ -12,19 +12,13 @@ Gets site classification configuration m365 entra siteclassification get [options] ``` -## Alias - -```sh -m365 aad siteclassification get [options] -``` - ## Options ## Examples -Get information about the Microsoft 365 Tenant site classification +Get information about the Microsoft 365 Tenant site classification. ```sh m365 entra siteclassification get diff --git a/docs/docs/cmd/entra/siteclassification/siteclassification-set.mdx b/docs/docs/cmd/entra/siteclassification/siteclassification-set.mdx index 8fc85094be8..85434e81b6a 100644 --- a/docs/docs/cmd/entra/siteclassification/siteclassification-set.mdx +++ b/docs/docs/cmd/entra/siteclassification/siteclassification-set.mdx @@ -10,26 +10,20 @@ Updates site classification configuration m365 entra siteclassification set [options] ``` -## Alias - -```sh -m365 aad siteclassification set [options] -``` - ## Options ```md definition-list `-c, --classifications [classifications]` -: Comma-separated list of classifications +: Comma-separated list of classifications. `-d, --defaultClassification [defaultClassification]` -: Classification to use by default +: Classification to use by default. `-u, --usageGuidelinesUrl [usageGuidelinesUrl]` -: URL with usage guidelines for members +: URL with usage guidelines for members. `-g, --guestUsageGuidelinesUrl [guestUsageGuidelinesUrl]` -: URL with usage guidelines for guests +: URL with usage guidelines for guests. ``` @@ -37,25 +31,25 @@ m365 aad siteclassification set [options] ## Examples -Update Microsoft 365 Tenant site classification configuration +Update Microsoft 365 Tenant site classification configuration. ```sh m365 entra siteclassification set --classifications "High, Medium, Low" --defaultClassification "Medium" ``` -Update only the default classification +Update only the default classification. ```sh m365 entra siteclassification set --defaultClassification "Low" ``` -Update site classification with a usage guidelines URL +Update site classification with a usage guidelines URL. ```sh m365 entra siteclassification set --usageGuidelinesUrl "http://aka.ms/pnp" ``` -Update site classification with usage guidelines URLs for guests and members +Update site classification with usage guidelines URLs for guests and members. ```sh m365 entra siteclassification set --usageGuidelinesUrl "http://aka.ms/pnp" --guestUsageGuidelinesUrl "http://aka.ms/pnp" diff --git a/docs/docs/cmd/entra/user/user-add.mdx b/docs/docs/cmd/entra/user/user-add.mdx index 4b4789d8ca5..6ac299830e4 100644 --- a/docs/docs/cmd/entra/user/user-add.mdx +++ b/docs/docs/cmd/entra/user/user-add.mdx @@ -12,12 +12,6 @@ Creates a new user m365 entra user add [options] ``` -## Alias - -```sh -m365 aad user add [options] -``` - ## Options ```md definition-list @@ -95,25 +89,25 @@ If the specified option is not found, you will receive a `Resource 'xyz' does no ## Examples -Create a user and let him/her update the password at first login +Create a user and let him/her update the password at first login. ```sh m365 entra user add --displayName "John Doe" --userName "john.doe@contoso.com" --password "SomePassw0rd" --forceChangePasswordNextSignIn ``` -Create a user with job information +Create a user with job information. ```sh m365 entra user add --displayName "John Doe" --userName "john.doe@contoso.com" --password "SomePassw0rd" --firstName John --lastName Doe --jobTitle "Sales Manager" --companyName Contoso --department Sales --officeLocation Vosselaar --forceChangePasswordNextSignIn ``` -Create a user with language information +Create a user with language information. ```sh m365 entra user add --displayName "John Doe" --userName "john.doe@contoso.com" --preferredLanguage "nl-BE" --usageLocation BE --forceChangePasswordNextSignIn ``` -Create a user with a manager +Create a user with a manager. ```sh m365 entra user add --displayName "John Doe" --userName "john.doe@contoso.com" --managerUserName "adele@contoso.com" diff --git a/docs/docs/cmd/entra/user/user-get.mdx b/docs/docs/cmd/entra/user/user-get.mdx index ed4d0999d87..8fed3877102 100644 --- a/docs/docs/cmd/entra/user/user-get.mdx +++ b/docs/docs/cmd/entra/user/user-get.mdx @@ -12,12 +12,6 @@ Gets information about the specified user m365 entra user get [options] ``` -## Alias - -```sh -m365 aad user get [options] -``` - ## Options ```md definition-list @@ -47,43 +41,43 @@ If the user with the specified id, user name, or email doesn't exist, you will g ## Examples -Get information about the user with id _1caf7dcd-7e83-4c3a-94f7-932a1299c844_ +Get information about the user by id. ```sh m365 entra user get --id 1caf7dcd-7e83-4c3a-94f7-932a1299c844 ``` -Get information about the user by user name +Get information about the user by user name. ```sh m365 entra user get --userName AarifS@contoso.onmicrosoft.com ``` -Get information about the user by email +Get information about the user by email. ```sh m365 entra user get --email AarifS@contoso.onmicrosoft.com ``` -For the user with id _1caf7dcd-7e83-4c3a-94f7-932a1299c844_ retrieve the user name, e-mail address and full name +For the user with id _1caf7dcd-7e83-4c3a-94f7-932a1299c844_ retrieve the user name, e-mail address and full name. ```sh m365 entra user get --id 1caf7dcd-7e83-4c3a-94f7-932a1299c844 --properties "userPrincipalName,mail,displayName" ``` -Get information about the currently logged user using the Id token +Get information about the currently logged user using the Id token. ```sh m365 entra user get --id "@meId" ``` -Get information about the currently logged in user using the UserName token +Get information about the currently logged in user using the UserName token. ```sh m365 entra user get --userName "@meUserName" ``` -Get information about a user with the manager's details +Get information about a user with the manager's details. ```sh m365 entra user get --userName AarifS@contoso.onmicrosoft.com --withManager diff --git a/docs/docs/cmd/entra/user/user-guest-add.mdx b/docs/docs/cmd/entra/user/user-guest-add.mdx index 0e7bd8c91ba..26ff5f348b3 100644 --- a/docs/docs/cmd/entra/user/user-guest-add.mdx +++ b/docs/docs/cmd/entra/user/user-guest-add.mdx @@ -12,12 +12,6 @@ Invite an external user to the organization m365 entra user guest add [options] ``` -## Alias - -```sh -m365 aad user guest add [options] -``` - ## Options ```md definition-list @@ -47,19 +41,19 @@ m365 aad user guest add [options] ## Examples -Invite a user via email and set the display name +Invite a user via email and set the display name. ```sh m365 entra user guest add --emailAddress john.doe@contoso.com --displayName "John Doe" --sendInvitationMessage ``` -Invite a user with a custom email and custom redirect url +Invite a user with a custom email and custom redirect url. ```sh m365 entra user guest add --emailAddress john.doe@contoso.com --welcomeMessage "Hi John, welcome to the organization!" --inviteRedirectUrl https://contoso.sharepoint.com --sendInvitationMessage ``` -Invite a user and send an invitation mail in Dutch +Invite a user and send an invitation mail in Dutch. ```sh m365 entra user guest add --emailAddress john.doe@contoso.com --messageLanguage nl-BE --sendInvitationMessage diff --git a/docs/docs/cmd/entra/user/user-hibp.mdx b/docs/docs/cmd/entra/user/user-hibp.mdx index b754419ea6d..569f897cb3a 100644 --- a/docs/docs/cmd/entra/user/user-hibp.mdx +++ b/docs/docs/cmd/entra/user/user-hibp.mdx @@ -12,12 +12,6 @@ Allows you to retrieve all accounts that have been pwned with the specified user m365 entra user hibp [options] ``` -## Alias - -```sh -m365 aad user hibp [options] -``` - ## Options ```md definition-list @@ -41,13 +35,13 @@ If `API Key` is invalid, you will get a `Required option apiKey not specified` e ## Examples -Check if user with by a user name is in a data breach +Check if user with by a user name is in a data breach. ```sh m365 entra user hibp --userName account-exists@hibp-integration-tests.com --apiKey _YOUR-API-KEY_ ``` -Check if user by a user name is in a data breach against the domain specified +Check if user by a user name is in a data breach against the domain specified. ```sh m365 entra user hibp --userName account-exists@hibp-integration-tests.com --apiKey _YOUR-API-KEY_ --domain adobe.com diff --git a/docs/docs/cmd/entra/user/user-license-add.mdx b/docs/docs/cmd/entra/user/user-license-add.mdx index f9fa66219d6..c5374f4d587 100644 --- a/docs/docs/cmd/entra/user/user-license-add.mdx +++ b/docs/docs/cmd/entra/user/user-license-add.mdx @@ -12,12 +12,6 @@ Assigns a license to a user m365 entra user license add [options] ``` -## Alias - -```sh -m365 aad user license add [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/user/user-license-list.mdx b/docs/docs/cmd/entra/user/user-license-list.mdx index 0a37510a8e6..802c15d1d0a 100644 --- a/docs/docs/cmd/entra/user/user-license-list.mdx +++ b/docs/docs/cmd/entra/user/user-license-list.mdx @@ -12,12 +12,6 @@ Lists the license details for a given user m365 entra user license list [options] ``` -## Alias - -```sh -m365 aad user license list [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/user/user-license-remove.mdx b/docs/docs/cmd/entra/user/user-license-remove.mdx index b598b08d363..a8bb50c7ec9 100644 --- a/docs/docs/cmd/entra/user/user-license-remove.mdx +++ b/docs/docs/cmd/entra/user/user-license-remove.mdx @@ -10,12 +10,6 @@ Removes a license from a user m365 entra user license remove [options] ``` -## Alias - -```sh -m365 aad user license remove [options] -``` - ## Options ```md definition-list @@ -36,13 +30,13 @@ m365 aad user license remove [options] ## Examples -Remove specific licenses from a specific user by UPN +Remove specific licenses from a specific user by UPN. ```sh m365 entra user license remove --userName "john.doe@contoso.com" --ids "45715bb8-13f9-4bf6-927f-ef96c102d394,bea13e0c-3828-4daa-a392-28af7ff61a0f" ``` -Remove specific licenses from a specific user by ID +Remove specific licenses from a specific user by ID. ```sh m365 entra user license remove --userId 5c241023-2ba5-4ea8-a516-a2481a3e6c51 --ids "45715bb8-13f9-4bf6-927f-ef96c102d394,bea13e0c-3828-4daa-a392-28af7ff61a0f" diff --git a/docs/docs/cmd/entra/user/user-list.mdx b/docs/docs/cmd/entra/user/user-list.mdx index 9b2d79e865e..745f86f21bc 100644 --- a/docs/docs/cmd/entra/user/user-list.mdx +++ b/docs/docs/cmd/entra/user/user-list.mdx @@ -12,12 +12,6 @@ Lists users matching specified criteria m365 entra user list [options] ``` -## Alias - -```sh -m365 aad user list [options] -``` - ## Options ```md definition-list @@ -25,7 +19,7 @@ m365 aad user list [options] : Filter the results to only users of a given type: `Member` or `Guest`. By default, all users are listed. `-p, --properties [properties]` -: Comma-separated list of properties to retrieve +: Comma-separated list of properties to retrieve. ``` diff --git a/docs/docs/cmd/entra/user/user-password-validate.mdx b/docs/docs/cmd/entra/user/user-password-validate.mdx index eede936a320..cbc4ca1eb7a 100644 --- a/docs/docs/cmd/entra/user/user-password-validate.mdx +++ b/docs/docs/cmd/entra/user/user-password-validate.mdx @@ -12,12 +12,6 @@ Check a user's password against the organization's password validation policy m365 entra user password validate [options] ``` -## Alias - -```sh -m365 aad user password validate [options] -``` - ## Options ```md definition-list @@ -37,7 +31,7 @@ This command is based on an API that is currently in preview and is subject to c ## Examples -Validate password _cli365P@ssW0rd_ against the organization's password validation policy +Validate password _cli365P@ssW0rd_ against the organization's password validation policy. ```sh m365 entra user password validate --password "cli365P@ssW0rd" diff --git a/docs/docs/cmd/entra/user/user-recyclebinitem-clear.mdx b/docs/docs/cmd/entra/user/user-recyclebinitem-clear.mdx index 28eed161445..f1e549b1cea 100644 --- a/docs/docs/cmd/entra/user/user-recyclebinitem-clear.mdx +++ b/docs/docs/cmd/entra/user/user-recyclebinitem-clear.mdx @@ -10,12 +10,6 @@ Removes all users from the tenant recycle bin m365 entra user recyclebinitem clear [options] ``` -## Alias - -```sh -m365 aad user recyclebinitem clear [options] -``` - ## Options ```md definition-list @@ -41,13 +35,13 @@ After running this command, it may take a minute before all deleted users are ef ## Examples -Removes all users from the tenant recycle bin +Removes all users from the tenant recycle bin. ```sh m365 entra user recyclebinitem clear ``` -Removes all users from the tenant recycle bin without confirmation prompt +Removes all users from the tenant recycle bin without confirmation prompt. ```sh m365 entra user recyclebinitem clear --force diff --git a/docs/docs/cmd/entra/user/user-recyclebinitem-list.mdx b/docs/docs/cmd/entra/user/user-recyclebinitem-list.mdx index e2094a0876c..88811da7155 100644 --- a/docs/docs/cmd/entra/user/user-recyclebinitem-list.mdx +++ b/docs/docs/cmd/entra/user/user-recyclebinitem-list.mdx @@ -12,12 +12,6 @@ Lists users from the recycle bin in the current tenant m365 entra user recyclebinitem list [options] ``` -## Alias - -```sh -m365 aad user recyclebinitem list [options] -``` - ## Options diff --git a/docs/docs/cmd/entra/user/user-recyclebinitem-remove.mdx b/docs/docs/cmd/entra/user/user-recyclebinitem-remove.mdx index b7a8702296f..0cbc49f54ac 100644 --- a/docs/docs/cmd/entra/user/user-recyclebinitem-remove.mdx +++ b/docs/docs/cmd/entra/user/user-recyclebinitem-remove.mdx @@ -10,12 +10,6 @@ Removes a user from the recycle bin in the current tenant m365 entra user recyclebinitem remove [options] ``` -## Alias - -```sh -m365 aad user recyclebinitem remove [options] -``` - ## Options ```md definition-list @@ -44,13 +38,13 @@ After running this command, it may take a minute before the deleted user is effe ## Examples -Removes a specific user from the recycle bin +Removes a specific user from the recycle bin. ```sh m365 entra user recyclebinitem remove --id 59f80e08-24b1-41f8-8586-16765fd830d3 ``` -Removes a specific user from the recycle bin without confirmation prompt +Removes a specific user from the recycle bin without confirmation prompt. ```sh m365 entra user recyclebinitem remove --id 59f80e08-24b1-41f8-8586-16765fd830d3 --force diff --git a/docs/docs/cmd/entra/user/user-recyclebinitem-restore.mdx b/docs/docs/cmd/entra/user/user-recyclebinitem-restore.mdx index 2c57f079de0..c005e93ee90 100644 --- a/docs/docs/cmd/entra/user/user-recyclebinitem-restore.mdx +++ b/docs/docs/cmd/entra/user/user-recyclebinitem-restore.mdx @@ -12,12 +12,6 @@ Restores a user from the recycle bin in the current tenant m365 entra user recyclebinitem restore [options] ``` -## Alias - -```sh -m365 aad user recyclebinitem restore [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/user/user-registrationdetails-list.mdx b/docs/docs/cmd/entra/user/user-registrationdetails-list.mdx index b66f14044fc..7bced2154c4 100644 --- a/docs/docs/cmd/entra/user/user-registrationdetails-list.mdx +++ b/docs/docs/cmd/entra/user/user-registrationdetails-list.mdx @@ -12,12 +12,6 @@ Retrieves a list of the authentication methods registered for users m365 entra user registrationdetails list [options] ``` -## Alias - -```sh -m365 aad user registrationdetails list [options] -``` - ## Options ```md definition-list @@ -85,43 +79,43 @@ When multiple values are specified for `--registeredMethods` option, the command ## Examples -Retrieve registration details for all users +Retrieve registration details for all users. ```sh m365 entra user registrationdetails list ``` -Retrieve user registration details and returns only specific properties +Retrieve user registration details and returns only specific properties. ```sh m365 entra user registrationdetails list --properties 'id,isAdmin' ``` -Retrieve registration details for admins +Retrieve registration details for admins. ```sh m365 entra user registrationdetails list --isAdmin true ``` -Retrieve registration details for guest users +Retrieve registration details for guest users. ```sh m365 entra user registrationdetails list --userType guest ``` -Retrieve registration details for users who selected either sms or push authentication method as the default second-factor for performing multifactor authentication +Retrieve registration details for users who selected either sms or push authentication method as the default second-factor for performing multifactor authentication. ```sh m365 entra user registrationdetails list --userPreferredMethodForSecondaryAuthentication 'sms,push' ``` -Retrieve registration details for users with push authentication method as the most secure authentication method among the registered methods for second factor authentication determined by the system +Retrieve registration details for users with push authentication method as the most secure authentication method among the registered methods for second factor authentication determined by the system. ```sh m365 entra user registrationdetails list --systemPreferredAuthenticationMethods push ``` -Retrieve registration details for users who have used either Microsoft Authenticator app or mobile phone during registration +Retrieve registration details for users who have used either Microsoft Authenticator app or mobile phone during registration. ```sh m365 entra user registrationdetails list --methodsRegistered 'microsoftAuthenticatorPush,mobilePhone' diff --git a/docs/docs/cmd/entra/user/user-remove.mdx b/docs/docs/cmd/entra/user/user-remove.mdx index 33ac9738ad5..e8aaeebdbc5 100644 --- a/docs/docs/cmd/entra/user/user-remove.mdx +++ b/docs/docs/cmd/entra/user/user-remove.mdx @@ -10,12 +10,6 @@ Removes a specific user m365 entra user remove [options] ``` -## Alias - -```sh -m365 aad user remove [options] -``` - ## Options ```md definition-list @@ -53,13 +47,13 @@ After running this command, it may take a minute before the user is effectively ## Examples -Removes a specific user by id +Removes a specific user by id. ```sh m365 entra user remove --id a33bd401-9117-4e0e-bb7b-3f61c1539e10 ``` -Removes a specific user by its UPN +Removes a specific user by its UPN. ```sh m365 entra user remove --userName john.doe@contoso.com diff --git a/docs/docs/cmd/entra/user/user-set.mdx b/docs/docs/cmd/entra/user/user-set.mdx index d1313cc96bf..0064d82a674 100644 --- a/docs/docs/cmd/entra/user/user-set.mdx +++ b/docs/docs/cmd/entra/user/user-set.mdx @@ -10,12 +10,6 @@ Updates information of the specified user m365 entra user set [options] ``` -## Alias - -```sh -m365 aad user set [options] -``` - ## Options ```md definition-list diff --git a/docs/docs/cmd/entra/user/user-signin-list.mdx b/docs/docs/cmd/entra/user/user-signin-list.mdx index 18f384aac5e..8c7b7e92f1a 100644 --- a/docs/docs/cmd/entra/user/user-signin-list.mdx +++ b/docs/docs/cmd/entra/user/user-signin-list.mdx @@ -12,12 +12,6 @@ Retrieves the Entra ID user sign-ins for the tenant m365 entra user signin list [options] ``` -## Alias - -```sh -m365 aad user signin list [options] -``` - ## Options ```md definition-list @@ -38,55 +32,55 @@ m365 aad user signin list [options] ## Examples -Get all user's sign-ins in your tenant +Get all user's sign-ins in your tenant. ```sh m365 entra user signin list ``` -Get all user's sign-ins filter by given user's UPN in the tenant +Get all user's sign-ins filter by given user's UPN in the tenant. ```sh m365 entra user signin list --userName 'johndoe@example.com' ``` -Get all user's sign-ins filter by given user's Id in the tenant +Get all user's sign-ins filter by given user's Id in the tenant. ```sh m365 entra user signin list --userId '11111111-1111-1111-1111-111111111111' ``` -Get all user's sign-ins filter by given application display name in the tenant +Get all user's sign-ins filter by given application display name in the tenant. ```sh m365 entra user signin list --appDisplayName 'Graph explorer' ``` -Get all user's sign-ins filter by given application identifier in the tenant +Get all user's sign-ins filter by given application identifier in the tenant. ```sh m365 entra user signin list --appId '00000000-0000-0000-0000-000000000000' ``` -Get all user's sign-ins filter by given user's UPN and application display name in the tenant +Get all user's sign-ins filter by given user's UPN and application display name in the tenant. ```sh m365 entra user signin list --userName 'johndoe@example.com' --appDisplayName 'Graph explorer' ``` -Get all user's sign-ins filter by given user's Id and application display name in the tenant +Get all user's sign-ins filter by given user's Id and application display name in the tenant. ```sh m365 entra user signin list --userId '11111111-1111-1111-1111-111111111111' --appDisplayName 'Graph explorer' ``` -Get all user's sign-ins filter by given user's UPN and application identifier in the tenant +Get all user's sign-ins filter by given user's UPN and application identifier in the tenant. ```sh m365 entra user signin list --userName 'johndoe@example.com' --appId '00000000-0000-0000-0000-000000000000' ``` -Get all user's sign-ins filter by given user's Id and application identifier in the tenant +Get all user's sign-ins filter by given user's Id and application identifier in the tenant. ```sh m365 entra user signin list --userId '11111111-1111-1111-1111-111111111111' --appId '00000000-0000-0000-0000-000000000000' diff --git a/docs/docs/cmd/spo/group/group-member-add.mdx b/docs/docs/cmd/spo/group/group-member-add.mdx index 090310f31f3..ae521aed3f0 100644 --- a/docs/docs/cmd/spo/group/group-member-add.mdx +++ b/docs/docs/cmd/spo/group/group-member-add.mdx @@ -25,32 +25,26 @@ m365 spo group member add [options] : Name of the SharePoint Group to which the user needs to be added. Specify either `groupId` or `groupName`. `--userNames [userNames]` -: User's UPN (user principal name, eg. megan.bowen@contoso.com). If multiple users need to be added, they have to be comma-separated (e.g. megan.bowen@contoso.com,alex.wilber@contoso.com). Specify either `userIds`, `userNames`, `emails`, `aadGroupIds` or `aadGroupNames`. +: User's UPN (user principal name, eg. megan.bowen@contoso.com). If multiple users need to be added, they have to be comma-separated (e.g. megan.bowen@contoso.com,alex.wilber@contoso.com). Specify either `userIds`, `userNames`, `emails`, `entraGroupIds` or `entraGroupNames`. `--emails [emails]` -: User's email (eg. megan.bowen@contoso.com). If multiple users need to be added, they have to be comma-separated (e.g. megan.bowen@contoso.com,alex.wilber@contoso.com). Specify either `userIds`, `userNames`, `emails`, `aadGroupIds` or `aadGroupNames`. +: User's email (eg. megan.bowen@contoso.com). If multiple users need to be added, they have to be comma-separated (e.g. megan.bowen@contoso.com,alex.wilber@contoso.com). Specify either `userIds`, `userNames`, `emails`, `entraGroupIds` or `entraGroupNames`. `--userIds [userIds]` -: The user Id of the user to add as a member. (Id of the site user, for example: 14) If multiple users need to be added, the Ids have to be comma-separated. Specify either `userIds`, `userNames`, `emails`, `aadGroupIds` or `aadGroupNames`. +: The user Id of the user to add as a member. (Id of the site user, for example: 14). If multiple users need to be added, the Ids have to be comma-separated. Specify either `userIds`, `userNames`, `emails`, `entraGroupIds` or `entraGroupNames`. `--entraGroupIds [entraGroupIds]` -: The object Id of the Entra group to add as a member. If multiple groups need to be added, the Ids have to be comma-separated. Specify either `userIds`, `userNames`, `emails`, `aadGroupIds`, `entraGroupIds`, `aadGroupNames`, or `entraGroupNames`. - -`--aadGroupIds [aadGroupIds]` -: (deprecated. Use `entraGroupIds` instead) The object ID of the Microsoft Entra group to add as a member. If multiple groups need to be added, the Ids have to be comma-separated. Specify either `userIds`, `userNames`, `emails`, `aadGroupIds`, `entraGroupIds`, `aadGroupNames`, or `entraGroupNames`. +: The object Id of the Entra group to add as a member. If multiple groups need to be added, the Ids have to be comma-separated. Specify either `userIds`, `userNames`, `emails`, `entraGroupIds` or `entraGroupNames`. `--entraGroupNames [entraGroupNames]` -: The name of the Entra group to add as a member. If multiple groups need to be added, they have to be comma-separated. Specify either `userIds`, `userNames`, `emails`, `aadGroupIds`, `entraGroupIds`, `aadGroupNames`, or `entraGroupNames`. - -`--aadGroupNames [aadGroupNames]` -: (deprecated. Use `entraGroupNames` instead) The name of the Microsoft Entra group to add as a member. If multiple groups need to be added, they have to be comma-separated. Specify either `userIds`, `userNames`, `emails`, `aadGroupIds`, `entraGroupIds`, `aadGroupNames`, or `entraGroupNames`. +: The name of the Entra group to add as a member. If multiple groups need to be added, they have to be comma-separated. Specify either `userIds`, `userNames`, `emails`, `entraGroupIds` or `entraGroupNames`. ``` ## Remarks -For the `userIds`, `userNames`, `emails`, `aadGroupIds`, `entraGroupIds`, `aadGroupNames`, or `entraGroupNames` options you can specify multiple values by separating them with a comma. If one of the specified entries is not valid, the command will fail with an error message showing the list of invalid values. +For the `userIds`, `userNames`, `emails`, `entraGroupIds`, or `entraGroupNames` options you can specify multiple values by separating them with a comma. If one of the specified entries is not valid, the command will fail with an error message showing the list of invalid values. ## Examples @@ -63,7 +57,7 @@ m365 spo group member add --webUrl https://contoso.sharepoint.com/sites/SiteA -- Add multiple users with the userNames parameter to a SharePoint group with the groupId parameter. ```sh -m365 spo group member add --webUrl https://contoso.sharepoint.com/sites/SiteA --groupId 5 --userNames "Alex.Wilber@contoso.com, Adele.Vance@contoso.com" +m365 spo group member add --webUrl https://contoso.sharepoint.com/sites/SiteA --groupId 5 --userNames "Alex.Wilber@contoso.com,Adele.Vance@contoso.com" ``` Add a user with the emails parameter to a SharePoint group with the groupName parameter. @@ -75,7 +69,7 @@ m365 spo group member add --webUrl https://contoso.sharepoint.com/sites/SiteA -- Add multiple users with the emails parameter to a SharePoint group with the groupName parameter. ```sh -m365 spo group member add --webUrl https://contoso.sharepoint.com/sites/SiteA --groupName "Contoso Site Owners" --emails "Alex.Wilber@contoso.com, Adele.Vance@contoso.com" +m365 spo group member add --webUrl https://contoso.sharepoint.com/sites/SiteA --groupName "Contoso Site Owners" --emails "Alex.Wilber@contoso.com,Adele.Vance@contoso.com" ``` Add a user with the userIds parameter to a SharePoint group with the groupId parameter. diff --git a/docs/docs/cmd/spo/group/group-member-remove.mdx b/docs/docs/cmd/spo/group/group-member-remove.mdx index 4b2adc95922..7ed285b92d8 100644 --- a/docs/docs/cmd/spo/group/group-member-remove.mdx +++ b/docs/docs/cmd/spo/group/group-member-remove.mdx @@ -23,25 +23,19 @@ m365 spo group member remove [options] : Name of the SharePoint group from which user has to be removed. Specify either `groupName` or `groupId`, but not both. `--userName [userName]` -: The UPN (user principal name, eg. megan.bowen@contoso.com) of the user that needs to be removed. Specify either `userName`, `email`, `userId`, `aadGroupId` or `aadGroupName`. +: The UPN (user principal name, eg. megan.bowen@contoso.com) of the user that needs to be removed. Specify either `userName`, `email`, `userId`, `entraGroupId` or `entraGroupName`. `--email [email]` -: The email of the user to remove as a member. Specify either `userName`, `email`, `userId`, `aadGroupId` or `aadGroupName`. +: The email of the user to remove as a member. Specify either `userName`, `email`, `userId`, `entraGroupId` or `entraGroupName`. `--userId [userId]` -: The user Id (Id of the site user, eg. 14) of the user to remove as a member. Specify either `userName`, `email`, `userId`, `aadGroupId` or `aadGroupName`. +: The user Id (Id of the site user, eg. 14) of the user to remove as a member. Specify either `userName`, `email`, `userId`, `entraGroupId` or `entraGroupName`. `--entraGroupId [entraGroupId]` -: The object Id of the Entra group to remove as a member. Specify either `userName`, `email`, `userId`, `aadGroupId`, `entraGroupId`, `aadGroupName`, or `entraGroupName`. - -`--aadGroupId [aadGroupId]` -: (deprecated. Use `entraGroupId` instead) The object ID of the Microsoft Entra group to remove as a member. Specify either `userName`, `email`, `userId`, `aadGroupId`, `entraGroupId`, `aadGroupName`, or `entraGroupName`. +: The object Id of the Entra group to remove as a member. Specify either `userName`, `email`, `userId`, `entraGroupId`, or `entraGroupName`. `--entraGroupName [entraGroupName]` -: The name of the Entra group to remove as a member. Specify either `userName`, `email`, `userId`, `aadGroupId`, `entraGroupId`, `aadGroupName`, or `entraGroupName`. - -`--aadGroupName [aadGroupName]` -: (deprecated. Use `entraGroupName` instead) The name of the Microsoft Entra group to remove as a member. Specify either `userName`, `email`, `userId`, `aadGroupId`, `entraGroupId`, `aadGroupName`, or `entraGroupName`. +: The name of the Entra group to remove as a member. Specify either `userName`, `email`, `userId`, `entraGroupId`, or `entraGroupName`. ``` diff --git a/docs/docs/cmd/spo/user/user-ensure.mdx b/docs/docs/cmd/spo/user/user-ensure.mdx index 077838c0c41..12551b5ef98 100644 --- a/docs/docs/cmd/spo/user/user-ensure.mdx +++ b/docs/docs/cmd/spo/user/user-ensure.mdx @@ -19,13 +19,10 @@ m365 spo user ensure [options] : Absolute URL of the site. `--entraId [--entraId]` -: Id of the user in Entra. Specify either `aadId`, `entraId`, or `userName`. - -`--aadId [--aadId]` -: (deprecated. Use `entraId` instead) Id of the user in Microsoft Entra. Specify either `aadId`, `entraId`, or `userName`. +: Id of the user in Entra. Specify either `entraId` or `userName`. `--userName [userName]` -: User's UPN (user principal name, e.g. john@contoso.com). Specify either `aadId`, `entraId`, or `userName`. +: User's UPN (user principal name, e.g. john@contoso.com). Specify either `entraId` or `userName`. ``` diff --git a/src/m365/entra/aadCommands.ts b/src/m365/entra/aadCommands.ts deleted file mode 100644 index 3ceff166f60..00000000000 --- a/src/m365/entra/aadCommands.ts +++ /dev/null @@ -1,89 +0,0 @@ -const prefix: string = 'aad'; - -export default { - ADMINISTRATIVEUNIT_ADD: `${prefix} administrativeunit add`, - ADMINISTRATIVEUNIT_GET: `${prefix} administrativeunit get`, - ADMINISTRATIVEUNIT_LIST: `${prefix} administrativeunit list`, - ADMINISTRATIVEUNIT_REMOVE: `${prefix} administrativeunit remove`, - ADMINISTRATIVEUNIT_MEMBER_ADD: `${prefix} administrativeunit member add`, - ADMINISTRATIVEUNIT_MEMBER_GET: `${prefix} administrativeunit member get`, - ADMINISTRATIVEUNIT_MEMBER_LIST: `${prefix} administrativeunit member list`, - ADMINISTRATIVEUNIT_MEMBER_REMOVE: `${prefix} administrativeunit member remove`, - ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD: `${prefix} administrativeunit roleassignment add`, - APP_ADD: `${prefix} app add`, - APP_GET: `${prefix} app get`, - APP_LIST: `${prefix} app list`, - APP_REMOVE: `${prefix} app remove`, - APP_SET: `${prefix} app set`, - APP_PERMISSION_ADD: `${prefix} app permission add`, - APP_ROLE_ADD: `${prefix} app role add`, - APP_ROLE_LIST: `${prefix} app role list`, - APP_ROLE_REMOVE: `${prefix} app role remove`, - APPROLEASSIGNMENT_ADD: `${prefix} approleassignment add`, - APPROLEASSIGNMENT_LIST: `${prefix} approleassignment list`, - APPROLEASSIGNMENT_REMOVE: `${prefix} approleassignment remove`, - GROUP_ADD: `${prefix} group add`, - GROUP_GET: `${prefix} group get`, - GROUP_LIST: `${prefix} group list`, - GROUP_REMOVE: `${prefix} group remove`, - GROUP_USER_LIST: `${prefix} group user list`, - GROUPSETTING_ADD: `${prefix} groupsetting add`, - GROUPSETTING_GET: `${prefix} groupsetting get`, - GROUPSETTING_LIST: `${prefix} groupsetting list`, - GROUPSETTING_REMOVE: `${prefix} groupsetting remove`, - GROUPSETTING_SET: `${prefix} groupsetting set`, - GROUPSETTINGTEMPLATE_GET: `${prefix} groupsettingtemplate get`, - GROUPSETTINGTEMPLATE_LIST: `${prefix} groupsettingtemplate list`, - LICENSE_LIST: `${prefix} license list`, - M365GROUP_ADD: `${prefix} m365group add`, - M365GROUP_GET: `${prefix} m365group get`, - M365GROUP_LIST: `${prefix} m365group list`, - M365GROUP_CONVERSATION_LIST: `${prefix} m365group conversation list`, - M365GROUP_CONVERSATION_POST_LIST: `${prefix} m365group conversation post list`, - M365GROUP_RECYCLEBINITEM_CLEAR: `${prefix} m365group recyclebinitem clear`, - M365GROUP_RECYCLEBINITEM_LIST: `${prefix} m365group recyclebinitem list`, - M365GROUP_RECYCLEBINITEM_REMOVE: `${prefix} m365group recyclebinitem remove`, - M365GROUP_RECYCLEBINITEM_RESTORE: `${prefix} m365group recyclebinitem restore`, - M365GROUP_SET: `${prefix} m365group set`, - M365GROUP_TEAMIFY: `${prefix} m365group teamify`, - M365GROUP_REMOVE: `${prefix} m365group remove`, - M365GROUP_RENEW: `${prefix} m365group renew`, - M365GROUP_REPORT_ACTIVITYCOUNTS: `${prefix} m365group report activitycounts`, - M365GROUP_REPORT_ACTIVITYDETAIL: `${prefix} m365group report activitydetail`, - M365GROUP_REPORT_ACTIVITYFILECOUNTS: `${prefix} m365group report activityfilecounts`, - M365GROUP_REPORT_ACTIVITYGROUPCOUNTS: `${prefix} m365group report activitygroupcounts`, - M365GROUP_REPORT_ACTIVITYSTORAGE: `${prefix} m365group report activitystorage`, - M365GROUP_USER_ADD: `${prefix} m365group user add`, - M365GROUP_USER_LIST: `${prefix} m365group user list`, - M365GROUP_USER_REMOVE: `${prefix} m365group user remove`, - M365GROUP_USER_SET: `${prefix} m365group user set`, - OAUTH2GRANT_ADD: `${prefix} oauth2grant add`, - OAUTH2GRANT_LIST: `${prefix} oauth2grant list`, - OAUTH2GRANT_REMOVE: `${prefix} oauth2grant remove`, - OAUTH2GRANT_SET: `${prefix} oauth2grant set`, - POLICY_LIST: `${prefix} policy list`, - SITECLASSIFICATION_DISABLE: `${prefix} siteclassification disable`, - SITECLASSIFICATION_ENABLE: `${prefix} siteclassification enable`, - SITECLASSIFICATION_GET: `${prefix} siteclassification get`, - SITECLASSIFICATION_SET: `${prefix} siteclassification set`, - SP_ADD: `${prefix} sp add`, - SP_GET: `${prefix} sp get`, - SP_LIST: `${prefix} sp list`, - USER_ADD: `${prefix} user add`, - USER_GET: `${prefix} user get`, - USER_GUEST_ADD: `${prefix} user guest add`, - USER_HIBP: `${prefix} user hibp`, - USER_LICENSE_ADD: `${prefix} user license add`, - USER_LICENSE_LIST: `${prefix} user license list`, - USER_LICENSE_REMOVE: `${prefix} user license remove`, - USER_LIST: `${prefix} user list`, - USER_PASSWORD_VALIDATE: `${prefix} user password validate`, - USER_RECYCLEBINITEM_CLEAR: `${prefix} user recyclebinitem clear`, - USER_RECYCLEBINITEM_LIST: `${prefix} user recyclebinitem list`, - USER_RECYCLEBINITEM_REMOVE: `${prefix} user recyclebinitem remove`, - USER_REGISTRATIONDETAILS_LIST: `${prefix} user registrationdetails list`, - USER_REMOVE: `${prefix} user remove`, - USER_RECYCLEBINITEM_RESTORE: `${prefix} user recyclebinitem restore`, - USER_SET: `${prefix} user set`, - USER_SIGNIN_LIST: `${prefix} user signin list` -}; diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-add.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-add.spec.ts index a762d5cac0b..7f3b5c2521e 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-add.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-add.spec.ts @@ -12,7 +12,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import request from '../../../../request.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ADMINISTRATIVEUNIT_ADD, () => { const administrativeUnitReponse: any = { @@ -72,16 +71,6 @@ describe(commands.ADMINISTRATIVEUNIT_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.ADMINISTRATIVEUNIT_ADD]); - }); - it('creates an administrative unit with a specific display name', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://graph.microsoft.com/v1.0/directory/administrativeUnits') { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-add.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-add.ts index 4a314149dbd..37e60e17884 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-add.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-add.ts @@ -4,7 +4,6 @@ import { Logger } from "../../../../cli/Logger.js"; import request, { CliRequestOptions } from "../../../../request.js"; import GraphCommand from "../../../base/GraphCommand.js"; import commands from "../../commands.js"; -import aadCommands from "../../aadCommands.js"; interface CommandArgs { options: Options; @@ -25,10 +24,6 @@ class EntraAdministrativeUnitAddCommand extends GraphCommand { return 'Creates an administrative unit'; } - public alias(): string[] | undefined { - return [aadCommands.ADMINISTRATIVEUNIT_ADD]; - } - constructor() { super(); @@ -59,8 +54,6 @@ class EntraAdministrativeUnitAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.ADMINISTRATIVEUNIT_ADD, commands.ADMINISTRATIVEUNIT_ADD); - const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/directory/administrativeUnits`, headers: { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-get.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-get.spec.ts index d5b26f3fac1..2bee010e9c9 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-get.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-get.spec.ts @@ -13,7 +13,6 @@ import request from '../../../../request.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import { CommandError } from '../../../../Command.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ADMINISTRATIVEUNIT_GET, () => { let log: string[]; @@ -83,16 +82,6 @@ describe(commands.ADMINISTRATIVEUNIT_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.ADMINISTRATIVEUNIT_GET]); - }); - it('retrieves information about the specified administrative unit by id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${validId}`) { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-get.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-get.ts index 930c7015c83..9c3755317c9 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-get.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-get.ts @@ -6,7 +6,6 @@ import request, { CliRequestOptions } from "../../../../request.js"; import GraphCommand from "../../../base/GraphCommand.js"; import commands from "../../commands.js"; import { entraAdministrativeUnit } from "../../../../utils/entraAdministrativeUnit.js"; -import aadCommands from "../../aadCommands.js"; interface CommandArgs { options: Options; @@ -26,10 +25,6 @@ class EntraAdministrativeUnitGetCommand extends GraphCommand { return 'Gets information about a specific administrative unit'; } - public alias(): string[] | undefined { - return [aadCommands.ADMINISTRATIVEUNIT_GET]; - } - constructor() { super(); @@ -81,8 +76,6 @@ class EntraAdministrativeUnitGetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.ADMINISTRATIVEUNIT_GET, commands.ADMINISTRATIVEUNIT_GET); - let administrativeUnit: AdministrativeUnit; try { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-list.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-list.spec.ts index f1d18809f7b..c746daa2a22 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-list.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-list.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './administrativeunit-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ADMINISTRATIVEUNIT_LIST, () => { let log: string[]; @@ -60,16 +59,6 @@ describe(commands.ADMINISTRATIVEUNIT_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.ADMINISTRATIVEUNIT_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'visibility']); }); diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-list.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-list.ts index 826bfe432dd..83b84bc1552 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-list.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-list.ts @@ -3,7 +3,6 @@ import { Logger } from '../../../../cli/Logger.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; class EntraAdministrativeUnitListCommand extends GraphCommand { public get name(): string { @@ -14,17 +13,11 @@ class EntraAdministrativeUnitListCommand extends GraphCommand { return 'Retrieves a list of administrative units'; } - public alias(): string[] | undefined { - return [aadCommands.ADMINISTRATIVEUNIT_LIST]; - } - public defaultProperties(): string[] | undefined { return ['id', 'displayName', 'visibility']; } public async commandAction(logger: Logger): Promise { - await this.showDeprecationWarning(logger, aadCommands.ADMINISTRATIVEUNIT_LIST, commands.ADMINISTRATIVEUNIT_LIST); - try { const results = await odata.getAllItems(`${this.resource}/v1.0/directory/administrativeUnits`); await logger.log(results); diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.spec.ts index e1f5b89156b..b83234ecdfe 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.spec.ts @@ -17,7 +17,6 @@ import { entraGroup } from '../../../../utils/entraGroup.js'; import { entraUser } from '../../../../utils/entraUser.js'; import { entraDevice } from '../../../../utils/entraDevice.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ADMINISTRATIVEUNIT_MEMBER_ADD, () => { const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; @@ -83,16 +82,6 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.ADMINISTRATIVEUNIT_MEMBER_ADD]); - }); - it('passes validation when administrativeUnitId is a valid GUID', async () => { const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, userId: '00000000-0000-0000-0000-000000000000' } }, commandInfo); assert.strictEqual(actual, true); diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.ts index 91107daa660..b9a0bd8d964 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.ts @@ -8,7 +8,6 @@ import GraphCommand from "../../../base/GraphCommand.js"; import commands from "../../commands.js"; import request, { CliRequestOptions } from "../../../../request.js"; import { entraDevice } from "../../../../utils/entraDevice.js"; -import aadCommands from "../../aadCommands.js"; interface CommandArgs { options: Options; @@ -34,10 +33,6 @@ class EntraAdministrativeUnitMemberAddCommand extends GraphCommand { return 'Adds a member (user, group, device) to an administrative unit'; } - public alias(): string[] | undefined { - return [aadCommands.ADMINISTRATIVEUNIT_MEMBER_ADD]; - } - constructor() { super(); @@ -119,8 +114,6 @@ class EntraAdministrativeUnitMemberAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.ADMINISTRATIVEUNIT_MEMBER_ADD, commands.ADMINISTRATIVEUNIT_MEMBER_ADD); - let administrativeUnitId = args.options.administrativeUnitId; let memberType; let memberId; diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.spec.ts index 2081aa4a876..1fe76b10455 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import { CommandError } from '../../../../Command.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ADMINISTRATIVEUNIT_MEMBER_GET, () => { const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; @@ -89,16 +88,6 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.ADMINISTRATIVEUNIT_MEMBER_GET]); - }); - it('passes validation when member id and administrativeUnitId are GUIDs', async () => { const actual = await command.validate({ options: { id: userId, administrativeUnitId: administrativeUnitId } }, commandInfo); assert.strictEqual(actual, true); diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.ts index 079176990a8..8e03b756c1c 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.ts @@ -6,7 +6,6 @@ import { Logger } from '../../../../cli/Logger.js'; import { validation } from '../../../../utils/validation.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; import request, { CliRequestOptions } from '../../../../request.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -34,10 +33,6 @@ class EntraAdministrativeUnitMemberGetCommand extends GraphCommand { return 'Retrieve a specific member (user, group, or device) of an administrative unit'; } - public alias(): string[] | undefined { - return [aadCommands.ADMINISTRATIVEUNIT_MEMBER_GET]; - } - constructor() { super(); @@ -95,8 +90,6 @@ class EntraAdministrativeUnitMemberGetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.ADMINISTRATIVEUNIT_MEMBER_GET, commands.ADMINISTRATIVEUNIT_MEMBER_GET); - let administrativeUnitId = args.options.administrativeUnitId; try { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.spec.ts index dde7b23d60e..b62afa68c56 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.spec.ts @@ -14,7 +14,6 @@ import { settingsNames } from '../../../../settingsNames.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { cli } from '../../../../cli/cli.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ADMINISTRATIVEUNIT_MEMBER_LIST, () => { const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; @@ -326,16 +325,6 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.ADMINISTRATIVEUNIT_MEMBER_LIST]); - }); - it('passes validation when administrativeUnitId is a valid GUID', async () => { const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId } }, commandInfo); assert.strictEqual(actual, true); diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.ts index 32bef4536ea..0055e560e47 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.ts @@ -7,7 +7,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; import { validation } from '../../../../utils/validation.js'; import { CliRequestOptions } from '../../../../request.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -35,10 +34,6 @@ class EntraAdministrativeUnitMemberListCommand extends GraphCommand { return 'Retrieves members (users, groups, or devices) of an administrative unit.'; } - public alias(): string[] | undefined { - return [aadCommands.ADMINISTRATIVEUNIT_MEMBER_LIST]; - } - public defaultProperties(): string[] | undefined { return ['id', 'displayName']; } @@ -110,8 +105,6 @@ class EntraAdministrativeUnitMemberListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.ADMINISTRATIVEUNIT_MEMBER_LIST, commands.ADMINISTRATIVEUNIT_MEMBER_LIST); - let administrativeUnitId = args.options.administrativeUnitId; try { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.spec.ts index 8b00fe054ff..3aa290db0af 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.spec.ts @@ -13,7 +13,6 @@ import { cli } from '../../../../cli/cli.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; import command from './administrativeunit-remove.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; @@ -76,16 +75,6 @@ describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.ADMINISTRATIVEUNIT_REMOVE]); - }); - it('removes the specified administrative unit by id without prompting for confirmation', async () => { const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}`) { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts index a46c6638c23..35bb9a277dd 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts @@ -6,8 +6,6 @@ import request, { CliRequestOptions } from "../../../../request.js"; import GraphCommand from "../../../base/GraphCommand.js"; import commands from "../../commands.js"; import { cli } from "../../../../cli/cli.js"; -import aadCommands from '../../aadCommands.js'; - interface CommandArgs { options: Options; @@ -27,10 +25,6 @@ class EntraAdministrativeUnitRemoveCommand extends GraphCommand { return 'Removes an administrative unit'; } - public alias(): string[] | undefined { - return [aadCommands.ADMINISTRATIVEUNIT_REMOVE]; - } - constructor() { super(); @@ -90,8 +84,6 @@ class EntraAdministrativeUnitRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.ADMINISTRATIVEUNIT_REMOVE, commands.ADMINISTRATIVEUNIT_REMOVE); - const removeAdministrativeUnit = async (): Promise => { try { let administrativeUnitId = args.options.id; diff --git a/src/m365/entra/commands/app/app-add.spec.ts b/src/m365/entra/commands/app/app-add.spec.ts index 9b26f9275e3..cbc0f83a0bf 100644 --- a/src/m365/entra/commands/app/app-add.spec.ts +++ b/src/m365/entra/commands/app/app-add.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './app-add.js'; import * as mocks from './app-add.mock.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APP_ADD, () => { @@ -204,16 +203,6 @@ describe(commands.APP_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APP_ADD, commands.APPREGISTRATION_ADD]); - }); - it('creates Microsoft Entra app reg with just the name', async () => { sinon.stub(request, 'get').rejects('Issues GET request'); sinon.stub(request, 'patch').rejects('Issued PATCH request'); diff --git a/src/m365/entra/commands/app/app-add.ts b/src/m365/entra/commands/app/app-add.ts index 2aa503d9d97..c75d34009d7 100644 --- a/src/m365/entra/commands/app/app-add.ts +++ b/src/m365/entra/commands/app/app-add.ts @@ -8,7 +8,6 @@ import { accessToken } from '../../../../utils/accessToken.js'; import { AppCreationOptions, AppInfo, entraApp } from '../../../../utils/entraApp.js'; import GraphCommand from '../../../base/GraphCommand.js'; import { M365RcJson } from '../../../base/M365RcJson.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -41,10 +40,6 @@ class EntraAppAddCommand extends GraphCommand { return 'Creates new Entra app registration'; } - public alias(): string[] | undefined { - return [aadCommands.APP_ADD, commands.APPREGISTRATION_ADD]; - } - constructor() { super(); @@ -211,8 +206,6 @@ class EntraAppAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APP_ADD, commands.APP_ADD); - if (!args.options.name && this.manifest) { args.options.name = this.manifest.name; } diff --git a/src/m365/entra/commands/app/app-get.spec.ts b/src/m365/entra/commands/app/app-get.spec.ts index 50a93894b01..5147ec30235 100644 --- a/src/m365/entra/commands/app/app-get.spec.ts +++ b/src/m365/entra/commands/app/app-get.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './app-get.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APP_GET, () => { let log: string[]; @@ -71,16 +70,6 @@ describe(commands.APP_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APP_GET, commands.APPREGISTRATION_GET]); - }); - it('handles error when the app specified with the appId not found', async () => { sinon.stub(request, 'get').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications?$filter=appId eq '9b1b1e42-794b-4c71-93ac-5ed92488b67f'&$select=id`) { diff --git a/src/m365/entra/commands/app/app-get.ts b/src/m365/entra/commands/app/app-get.ts index 969a5849d0f..d4b9be64cec 100644 --- a/src/m365/entra/commands/app/app-get.ts +++ b/src/m365/entra/commands/app/app-get.ts @@ -9,7 +9,6 @@ import GraphCommand from '../../../base/GraphCommand.js'; import { M365RcJson } from '../../../base/M365RcJson.js'; import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -31,10 +30,6 @@ class EntraAppGetCommand extends GraphCommand { return 'Gets an Entra app registration'; } - public alias(): string[] | undefined { - return [aadCommands.APP_GET, commands.APPREGISTRATION_GET]; - } - constructor() { super(); @@ -84,8 +79,6 @@ class EntraAppGetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APP_GET, commands.APP_GET); - try { const appObjectId = await this.getAppObjectId(args); const appInfo = await this.getAppInfo(appObjectId); diff --git a/src/m365/entra/commands/app/app-list.spec.ts b/src/m365/entra/commands/app/app-list.spec.ts index cf858293c48..e94f74a31d0 100644 --- a/src/m365/entra/commands/app/app-list.spec.ts +++ b/src/m365/entra/commands/app/app-list.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './app-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APP_LIST, () => { let log: string[]; @@ -60,16 +59,6 @@ describe(commands.APP_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APP_LIST, commands.APPREGISTRATION_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['appId', 'id', 'displayName', 'signInAudience']); }); diff --git a/src/m365/entra/commands/app/app-list.ts b/src/m365/entra/commands/app/app-list.ts index dd1c5fd9223..24d920ee920 100644 --- a/src/m365/entra/commands/app/app-list.ts +++ b/src/m365/entra/commands/app/app-list.ts @@ -3,7 +3,6 @@ import { Logger } from '../../../../cli/Logger.js'; import { odata } from "../../../../utils/odata.js"; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; class EntraAppListCommand extends GraphCommand { public get name(): string { @@ -14,17 +13,11 @@ class EntraAppListCommand extends GraphCommand { return 'Retrieves a list of Entra app registrations'; } - public alias(): string[] | undefined { - return [aadCommands.APP_LIST, commands.APPREGISTRATION_LIST]; - } - public defaultProperties(): string[] | undefined { return ['appId', 'id', 'displayName', "signInAudience"]; } public async commandAction(logger: Logger): Promise { - await this.showDeprecationWarning(logger, aadCommands.APP_LIST, commands.APP_LIST); - try { const results = await odata.getAllItems(`${this.resource}/v1.0/applications`); await logger.log(results); diff --git a/src/m365/entra/commands/app/app-permission-add.spec.ts b/src/m365/entra/commands/app/app-permission-add.spec.ts index 46ae98ab85e..7967484fe47 100644 --- a/src/m365/entra/commands/app/app-permission-add.spec.ts +++ b/src/m365/entra/commands/app/app-permission-add.spec.ts @@ -15,7 +15,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './app-permission-add.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APP_PERMISSION_ADD, () => { const appId = '9c79078b-815e-4a3e-bb80-2aaf2d9e9b3d'; @@ -86,16 +85,6 @@ describe(commands.APP_PERMISSION_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APP_PERMISSION_ADD, commands.APPREGISTRATION_PERMISSION_ADD]); - }); - it('adds application permissions to app specified by appObjectId without granting admin consent', async () => { sinon.stub(odata, 'getAllItems').callsFake(async (url: string) => { switch (url) { diff --git a/src/m365/entra/commands/app/app-permission-add.ts b/src/m365/entra/commands/app/app-permission-add.ts index 849498fdefc..68820520720 100644 --- a/src/m365/entra/commands/app/app-permission-add.ts +++ b/src/m365/entra/commands/app/app-permission-add.ts @@ -6,7 +6,6 @@ import commands from "../../commands.js"; import request, { CliRequestOptions } from "../../../../request.js"; import { Logger } from "../../../../cli/Logger.js"; import { validation } from "../../../../utils/validation.js"; -import aadCommands from "../../aadCommands.js"; import { formatting } from "../../../../utils/formatting.js"; import { cli } from "../../../../cli/cli.js"; @@ -43,10 +42,6 @@ class EntraAppPermissionAddCommand extends GraphCommand { return 'Adds the specified application and/or delegated permissions to a specified Microsoft Entra app'; } - public alias(): string[] | undefined { - return [aadCommands.APP_PERMISSION_ADD, commands.APPREGISTRATION_PERMISSION_ADD]; - } - constructor() { super(); @@ -105,8 +100,6 @@ class EntraAppPermissionAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APP_PERMISSION_ADD, commands.APP_PERMISSION_ADD); - try { const appObject = await this.getAppObject(args.options); const servicePrincipals = await this.getServicePrincipals(); diff --git a/src/m365/entra/commands/app/app-remove.spec.ts b/src/m365/entra/commands/app/app-remove.spec.ts index 4f57cd50a6b..5e564df95b4 100644 --- a/src/m365/entra/commands/app/app-remove.spec.ts +++ b/src/m365/entra/commands/app/app-remove.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './app-remove.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APP_REMOVE, () => { let log: string[]; @@ -100,16 +99,6 @@ describe(commands.APP_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APP_REMOVE, commands.APPREGISTRATION_REMOVE]); - }); - it('fails validation if appId and name specified', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { diff --git a/src/m365/entra/commands/app/app-remove.ts b/src/m365/entra/commands/app/app-remove.ts index 05987b6d612..e32424318f5 100644 --- a/src/m365/entra/commands/app/app-remove.ts +++ b/src/m365/entra/commands/app/app-remove.ts @@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -28,10 +27,6 @@ class EntraAppRemoveCommand extends GraphCommand { return 'Removes an Entra app registration'; } - public alias(): string[] | undefined { - return [aadCommands.APP_REMOVE, commands.APPREGISTRATION_REMOVE]; - } - constructor() { super(); @@ -82,8 +77,6 @@ class EntraAppRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APP_REMOVE, commands.APP_REMOVE); - const deleteApp = async (): Promise => { try { const objectId = await this.getObjectId(args, logger); diff --git a/src/m365/entra/commands/app/app-role-add.spec.ts b/src/m365/entra/commands/app/app-role-add.spec.ts index 8754d96f1d7..d9ee59dc53e 100644 --- a/src/m365/entra/commands/app/app-role-add.spec.ts +++ b/src/m365/entra/commands/app/app-role-add.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './app-role-add.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APP_ROLE_ADD, () => { let log: string[]; @@ -66,16 +65,6 @@ describe(commands.APP_ROLE_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APP_ROLE_ADD, commands.APPREGISTRATION_ROLE_ADD]); - }); - it('creates app role for the specified appId, app has no roles', async () => { sinon.stub(request, 'get').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications?$filter=appId eq 'bc724b77-da87-43a9-b385-6ebaaf969db8'&$select=id`) { diff --git a/src/m365/entra/commands/app/app-role-add.ts b/src/m365/entra/commands/app/app-role-add.ts index e1211b8a714..c9929f8a808 100644 --- a/src/m365/entra/commands/app/app-role-add.ts +++ b/src/m365/entra/commands/app/app-role-add.ts @@ -6,7 +6,6 @@ import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -44,10 +43,6 @@ class EntraAppRoleAddCommand extends GraphCommand { return 'Adds role to the specified Entra app registration'; } - public alias(): string[] | undefined { - return [aadCommands.APP_ROLE_ADD, commands.APPREGISTRATION_ROLE_ADD]; - } - constructor() { super(); @@ -112,8 +107,6 @@ class EntraAppRoleAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APP_ROLE_ADD, commands.APP_ROLE_ADD); - try { const appId = await this.getAppObjectId(args, logger); const appInfo = await this.getAppInfo(appId, logger); diff --git a/src/m365/entra/commands/app/app-role-list.spec.ts b/src/m365/entra/commands/app/app-role-list.spec.ts index da55924e438..7c8fc10cb2c 100644 --- a/src/m365/entra/commands/app/app-role-list.spec.ts +++ b/src/m365/entra/commands/app/app-role-list.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './app-role-list.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APP_ROLE_LIST, () => { let log: string[]; @@ -68,16 +67,6 @@ describe(commands.APP_ROLE_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APP_ROLE_LIST, commands.APPREGISTRATION_ROLE_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['displayName', 'description', 'id']); }); diff --git a/src/m365/entra/commands/app/app-role-list.ts b/src/m365/entra/commands/app/app-role-list.ts index 594ec364a76..bd2660955cd 100644 --- a/src/m365/entra/commands/app/app-role-list.ts +++ b/src/m365/entra/commands/app/app-role-list.ts @@ -7,7 +7,6 @@ import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -28,10 +27,6 @@ class EntraAppRoleListCommand extends GraphCommand { return 'Gets Entra app registration roles'; } - public alias(): string[] | undefined { - return [aadCommands.APP_ROLE_LIST, commands.APPREGISTRATION_ROLE_LIST]; - } - constructor() { super(); @@ -67,8 +62,6 @@ class EntraAppRoleListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APP_ROLE_LIST, commands.APP_ROLE_LIST); - try { const objectId = await this.getAppObjectId(args, logger); const appRoles = await odata.getAllItems(`${this.resource}/v1.0/myorganization/applications/${objectId}/appRoles`); diff --git a/src/m365/entra/commands/app/app-role-remove.spec.ts b/src/m365/entra/commands/app/app-role-remove.spec.ts index 649a6f8bb9e..ff93f72ea01 100644 --- a/src/m365/entra/commands/app/app-role-remove.spec.ts +++ b/src/m365/entra/commands/app/app-role-remove.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './app-role-remove.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APP_ROLE_REMOVE, () => { let log: string[]; @@ -74,16 +73,6 @@ describe(commands.APP_ROLE_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APP_ROLE_REMOVE, commands.APPREGISTRATION_ROLE_REMOVE]); - }); - it('deletes an app role when the role is in enabled state and valid appObjectId, role claim and --force option specified', async () => { sinon.stub(request, 'get').callsFake(async opts => { if (opts.url === 'https://graph.microsoft.com/v1.0/myorganization/applications/5b31c38c-2584-42f0-aa47-657fb3a84230?$select=id,appRoles') { diff --git a/src/m365/entra/commands/app/app-role-remove.ts b/src/m365/entra/commands/app/app-role-remove.ts index 258b09bfa86..b8796f2c13f 100644 --- a/src/m365/entra/commands/app/app-role-remove.ts +++ b/src/m365/entra/commands/app/app-role-remove.ts @@ -7,7 +7,6 @@ import { formatting } from "../../../../utils/formatting.js"; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from "../../aadCommands.js"; interface CommandArgs { options: Options; @@ -31,10 +30,6 @@ class EntraAppRoleRemoveCommand extends GraphCommand { return 'Removes role from the specified Entra app registration'; } - public alias(): string[] | undefined { - return [aadCommands.APP_ROLE_REMOVE, commands.APPREGISTRATION_ROLE_REMOVE]; - } - constructor() { super(); @@ -91,8 +86,6 @@ class EntraAppRoleRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APP_ROLE_REMOVE, commands.APP_ROLE_REMOVE); - const deleteAppRole = async (): Promise => { try { await this.processAppRoleDelete(logger, args); diff --git a/src/m365/entra/commands/app/app-set.spec.ts b/src/m365/entra/commands/app/app-set.spec.ts index 88612d755f9..3936f23a3d6 100644 --- a/src/m365/entra/commands/app/app-set.spec.ts +++ b/src/m365/entra/commands/app/app-set.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './app-set.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APP_SET, () => { @@ -74,16 +73,6 @@ describe(commands.APP_SET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APP_SET, commands.APPREGISTRATION_SET]); - }); - it('updates uris for the specified appId', async () => { sinon.stub(request, 'get').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/myorganization/applications?$filter=appId eq 'bc724b77-da87-43a9-b385-6ebaaf969db8'&$select=id`) { diff --git a/src/m365/entra/commands/app/app-set.ts b/src/m365/entra/commands/app/app-set.ts index 070f6b9302d..21fcc119f72 100644 --- a/src/m365/entra/commands/app/app-set.ts +++ b/src/m365/entra/commands/app/app-set.ts @@ -7,7 +7,6 @@ import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -38,10 +37,6 @@ class EntraAppSetCommand extends GraphCommand { return 'Updates Entra app registration'; } - public alias(): string[] | undefined { - return [aadCommands.APP_SET, commands.APPREGISTRATION_SET]; - } - constructor() { super(); @@ -130,8 +125,6 @@ class EntraAppSetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APP_SET, commands.APP_SET); - try { let objectId = await this.getAppObjectId(args, logger); objectId = await this.configureUri(args, objectId, logger); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts index e0838ad535d..48b06f262e3 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './approleassignment-add.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APPROLEASSIGNMENT_ADD, () => { let log: string[]; @@ -89,16 +88,6 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APPROLEASSIGNMENT_ADD]); - }); - it('sets App Role assignments for service principal with specified appDisplayName', async () => { getRequestStub(); postRequestStub(); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-add.ts b/src/m365/entra/commands/approleassignment/approleassignment-add.ts index c2cae66c315..b48eb8e930b 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-add.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-add.ts @@ -8,7 +8,6 @@ import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { ServicePrincipal } from '@microsoft/microsoft-graph-types'; import { cli } from '../../../../cli/cli.js'; -import aadCommands from '../../aadCommands.js'; interface AppRole { objectId: string; @@ -37,10 +36,6 @@ class EntraAppRoleAssignmentAddCommand extends GraphCommand { return 'Adds service principal permissions also known as scopes and app role assignments for specified Microsoft Entra application registration'; } - public alias(): string[] | undefined { - return [aadCommands.APPROLEASSIGNMENT_ADD]; - } - constructor() { super(); @@ -102,8 +97,6 @@ class EntraAppRoleAssignmentAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APPROLEASSIGNMENT_ADD, commands.APPROLEASSIGNMENT_ADD); - let objectId: string = ''; let queryFilter: string = ''; if (args.options.appId) { diff --git a/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts index c8227378468..c25ff909230 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './approleassignment-list.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; class ServicePrincipalAppRoleAssignments { private static AppRoleAssignments: any = { @@ -482,16 +481,6 @@ describe(commands.APPROLEASSIGNMENT_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APPROLEASSIGNMENT_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['resourceDisplayName', 'roleName']); }); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-list.ts b/src/m365/entra/commands/approleassignment/approleassignment-list.ts index 60d702996a8..3d82c94fac6 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-list.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-list.ts @@ -6,7 +6,6 @@ import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -27,10 +26,6 @@ class EntraAppRoleAssignmentListCommand extends GraphCommand { return 'Lists app role assignments for the specified application registration'; } - public alias(): string[] | undefined { - return [aadCommands.APPROLEASSIGNMENT_LIST]; - } - constructor() { super(); @@ -89,8 +84,6 @@ class EntraAppRoleAssignmentListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APPROLEASSIGNMENT_LIST, commands.APPROLEASSIGNMENT_LIST); - try { const spAppRoleAssignments = await this.getAppRoleAssignments(args.options); // the role assignment has an appRoleId but no name. To get the name, diff --git a/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts index 43c8a605b40..5cbe52b11af 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './approleassignment-remove.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.APPROLEASSIGNMENT_REMOVE, () => { let log: string[]; @@ -92,16 +91,6 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.APPROLEASSIGNMENT_REMOVE]); - }); - it('prompts before removing the app role assignment when force option not passed', async () => { await command.action(logger, { options: { appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'SharePoint', scope: 'Sites.Read.All' } }); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-remove.ts b/src/m365/entra/commands/approleassignment/approleassignment-remove.ts index c1619daf51b..eb67e31fcdb 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-remove.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-remove.ts @@ -8,7 +8,6 @@ import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { AppRole, AppRoleAssignment, ServicePrincipal } from '@microsoft/microsoft-graph-types'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -32,10 +31,6 @@ class EntraAppRoleAssignmentRemoveCommand extends GraphCommand { return 'Deletes an app role assignment for the specified Entra Application Registration'; } - public alias(): string[] | undefined { - return [aadCommands.APPROLEASSIGNMENT_REMOVE]; - } - constructor() { super(); @@ -101,8 +96,6 @@ class EntraAppRoleAssignmentRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.APPROLEASSIGNMENT_REMOVE, commands.APPROLEASSIGNMENT_REMOVE); - const removeAppRoleAssignment = async (): Promise => { let sp: ServicePrincipal; // get the service principal associated with the appId diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.spec.ts index b6810d693cd..009558afa22 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './enterpriseapp-add.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ENTERPRISEAPP_ADD, () => { let log: string[]; @@ -69,16 +68,6 @@ describe(commands.ENTERPRISEAPP_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.SP_ADD, commands.SP_ADD]); - }); - it('fails validation if neither the id, displayName, nor objectId option specified', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.ts index aa0cdab113e..f732ea1a64a 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.ts @@ -6,7 +6,6 @@ import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -27,10 +26,6 @@ class EntraEnterpriseAppAddCommand extends GraphCommand { return 'Creates an enterprise application (or service principal) for a registered Entra app'; } - public alias(): string[] | undefined { - return [aadCommands.SP_ADD, commands.SP_ADD]; - } - constructor() { super(); @@ -123,8 +118,6 @@ class EntraEnterpriseAppAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.SP_ADD, commands.SP_ADD); - try { const appId = await this.getAppId(args); diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts index e5664bbfe2a..a95f812c6d5 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './enterpriseapp-get.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ENTERPRISEAPP_GET, () => { let log: string[]; @@ -83,16 +82,6 @@ describe(commands.ENTERPRISEAPP_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.SP_GET, commands.SP_GET]); - }); - it('retrieves information about the specified enterprise application using its display name', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/v1.0/servicePrincipals?$filter=displayName eq `) > -1) { diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.ts index e159fbee368..0fe94b35b4a 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.ts @@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -27,10 +26,6 @@ class EntraEnterpriseAppGetCommand extends GraphCommand { return 'Gets information about an Enterprise Application'; } - public alias(): string[] | undefined { - return [aadCommands.SP_GET, commands.SP_GET]; - } - constructor() { super(); @@ -123,8 +118,6 @@ class EntraEnterpriseAppGetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.SP_GET, commands.SP_GET); - if (this.verbose) { await logger.logToStderr(`Retrieving enterprise application information...`); } diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.spec.ts index 60e23ae9db2..5e9355420f9 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.spec.ts @@ -9,7 +9,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './enterpriseapp-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.ENTERPRISEAPP_LIST, () => { let log: string[]; @@ -135,16 +134,6 @@ describe(commands.ENTERPRISEAPP_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.SP_LIST, commands.SP_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['appId', 'displayName', 'tag']); }); diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.ts index a2547c38260..903518dfe78 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.ts @@ -2,7 +2,6 @@ import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -27,10 +26,6 @@ class EntraEnterpriseAppListCommand extends GraphCommand { return 'Lists the enterprise applications (or service principals) in Entra ID'; } - public alias(): string[] | undefined { - return [aadCommands.SP_LIST, commands.SP_LIST]; - } - constructor() { super(); @@ -59,8 +54,6 @@ class EntraEnterpriseAppListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.SP_LIST, commands.SP_LIST); - if (this.verbose) { await logger.logToStderr(`Retrieving enterprise application information...`); } diff --git a/src/m365/entra/commands/group/group-add.spec.ts b/src/m365/entra/commands/group/group-add.spec.ts index eee6c612402..47874bc868d 100644 --- a/src/m365/entra/commands/group/group-add.spec.ts +++ b/src/m365/entra/commands/group/group-add.spec.ts @@ -2,7 +2,6 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; import { Logger } from '../../../../cli/Logger.js'; import request from '../../../../request.js'; import { telemetry } from '../../../../telemetry.js'; @@ -223,16 +222,6 @@ describe(commands.GROUP_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUP_ADD]); - }); - it('fails validation if the length of displayName is more than 256 characters', async () => { const displayName = 'lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum'; const actual = await command.validate({ options: { displayName: displayName, type: 'security' } }, commandInfo); diff --git a/src/m365/entra/commands/group/group-add.ts b/src/m365/entra/commands/group/group-add.ts index 70130284397..f20abe55d42 100644 --- a/src/m365/entra/commands/group/group-add.ts +++ b/src/m365/entra/commands/group/group-add.ts @@ -2,7 +2,6 @@ import { Group } from '@microsoft/microsoft-graph-types'; import GlobalOptions from '../../../../GlobalOptions.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; import { validation } from '../../../../utils/validation.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { Logger } from '../../../../cli/Logger.js'; @@ -33,10 +32,6 @@ class EntraGroupAddCommand extends GraphCommand { return 'Creates a Microsoft Entra group'; } - public alias(): string[] | undefined { - return [aadCommands.GROUP_ADD]; - } - public allowUnknownOptions(): boolean | undefined { return true; } diff --git a/src/m365/entra/commands/group/group-get.spec.ts b/src/m365/entra/commands/group/group-get.spec.ts index 04b98462b3c..725c6501aaa 100644 --- a/src/m365/entra/commands/group/group-get.spec.ts +++ b/src/m365/entra/commands/group/group-get.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './group-get.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUP_GET, () => { let log: string[]; @@ -94,16 +93,6 @@ describe(commands.GROUP_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUP_GET]); - }); - it('retrieves information about the specified Microsoft Entra Group by id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/${validId}`) { diff --git a/src/m365/entra/commands/group/group-get.ts b/src/m365/entra/commands/group/group-get.ts index fc2df0c0cc6..c642572d3f1 100644 --- a/src/m365/entra/commands/group/group-get.ts +++ b/src/m365/entra/commands/group/group-get.ts @@ -5,7 +5,6 @@ import { entraGroup } from '../../../../utils/entraGroup.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -25,10 +24,6 @@ class EntraGroupGetCommand extends GraphCommand { return 'Gets information about the specified Entra group'; } - public alias(): string[] | undefined { - return [aadCommands.GROUP_GET]; - } - constructor() { super(); @@ -77,8 +72,6 @@ class EntraGroupGetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUP_GET, commands.GROUP_GET); - let group: Group; try { diff --git a/src/m365/entra/commands/group/group-list.spec.ts b/src/m365/entra/commands/group/group-list.spec.ts index 5751651aba1..9eec88ca640 100644 --- a/src/m365/entra/commands/group/group-list.spec.ts +++ b/src/m365/entra/commands/group/group-list.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './group-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUP_LIST, () => { let log: string[]; @@ -65,16 +64,6 @@ describe(commands.GROUP_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUP_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'groupType']); }); diff --git a/src/m365/entra/commands/group/group-list.ts b/src/m365/entra/commands/group/group-list.ts index b113840f31f..ca2f1acdad0 100644 --- a/src/m365/entra/commands/group/group-list.ts +++ b/src/m365/entra/commands/group/group-list.ts @@ -6,7 +6,6 @@ import { CliRequestOptions } from '../../../../request.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -31,10 +30,6 @@ class EntraGroupListCommand extends GraphCommand { return 'Lists all groups defined in Entra ID.'; } - public alias(): string[] | undefined { - return [aadCommands.GROUP_LIST]; - } - public defaultProperties(): string[] | undefined { return ['id', 'displayName', 'groupType']; } @@ -77,8 +72,6 @@ class EntraGroupListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUP_LIST, commands.GROUP_LIST); - try { let requestUrl: string = `${this.resource}/v1.0/groups`; let useConsistencyLevelHeader = false; diff --git a/src/m365/entra/commands/group/group-member-list.spec.ts b/src/m365/entra/commands/group/group-member-list.spec.ts index 78eea44d419..28865ea0912 100644 --- a/src/m365/entra/commands/group/group-member-list.spec.ts +++ b/src/m365/entra/commands/group/group-member-list.spec.ts @@ -15,7 +15,6 @@ import { settingsNames } from '../../../../settingsNames.js'; import { formatting } from '../../../../utils/formatting.js'; import commands from '../../commands.js'; import command from './group-member-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUP_MEMBER_LIST, () => { const groupId = '2c1ba4c4-cd9b-4417-832f-92a34bc34b2a'; @@ -74,16 +73,6 @@ describe(commands.GROUP_MEMBER_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUP_USER_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'userPrincipalName', 'roles']); }); diff --git a/src/m365/entra/commands/group/group-member-list.ts b/src/m365/entra/commands/group/group-member-list.ts index cca4061b1c3..2728922d49f 100644 --- a/src/m365/entra/commands/group/group-member-list.ts +++ b/src/m365/entra/commands/group/group-member-list.ts @@ -7,7 +7,6 @@ import { odata } from '../../../../utils/odata.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -34,10 +33,6 @@ class EntraGroupMemberListCommand extends GraphCommand { return 'Lists members of a specific Entra group'; } - public alias(): string[] | undefined { - return [aadCommands.GROUP_USER_LIST]; - } - public defaultProperties(): string[] | undefined { return ['id', 'displayName', 'userPrincipalName', 'roles']; } @@ -111,8 +106,6 @@ class EntraGroupMemberListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUP_USER_LIST, commands.GROUP_MEMBER_LIST); - try { const groupId = await this.getGroupId(args.options, logger); diff --git a/src/m365/entra/commands/group/group-remove.spec.ts b/src/m365/entra/commands/group/group-remove.spec.ts index aed1f7eea19..5368f945021 100644 --- a/src/m365/entra/commands/group/group-remove.spec.ts +++ b/src/m365/entra/commands/group/group-remove.spec.ts @@ -15,7 +15,6 @@ import { CommandInfo } from '../../../../cli/CommandInfo.js'; import command from './group-remove.js'; import { settingsNames } from '../../../../settingsNames.js'; import { formatting } from '../../../../utils/formatting.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUP_REMOVE, () => { const groupId = '2c1ba4c4-cd9b-4417-832f-92a34bc34b2a'; @@ -73,16 +72,6 @@ describe(commands.GROUP_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUP_REMOVE]); - }); - it('removes the specified group by id without prompting for confirmation', async () => { const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupId}`) { diff --git a/src/m365/entra/commands/group/group-remove.ts b/src/m365/entra/commands/group/group-remove.ts index 7b05dceb479..3f133610996 100644 --- a/src/m365/entra/commands/group/group-remove.ts +++ b/src/m365/entra/commands/group/group-remove.ts @@ -6,7 +6,6 @@ import commands from '../../commands.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { validation } from '../../../../utils/validation.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -27,10 +26,6 @@ class EntraGroupRemoveCommand extends GraphCommand { return 'Removes an Entra group'; } - public alias(): string[] | undefined { - return [aadCommands.GROUP_REMOVE]; - } - constructor() { super(); @@ -90,8 +85,6 @@ class EntraGroupRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUP_REMOVE, commands.GROUP_REMOVE); - const removeGroup = async (): Promise => { if (this.verbose) { await logger.logToStderr(`Removing group ${args.options.id || args.options.displayName}...`); diff --git a/src/m365/entra/commands/groupsetting/groupsetting-add.spec.ts b/src/m365/entra/commands/groupsetting/groupsetting-add.spec.ts index ee66f1901e3..0b9756407d2 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-add.spec.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-add.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './groupsetting-add.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUPSETTING_ADD, () => { let log: string[]; @@ -66,16 +65,6 @@ describe(commands.GROUPSETTING_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUPSETTING_ADD]); - }); - it('adds group setting using default template setting values', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groupSettingTemplates/62375ab9-6b52-47ed-826b-58e47e0e304b`) { diff --git a/src/m365/entra/commands/groupsetting/groupsetting-add.ts b/src/m365/entra/commands/groupsetting/groupsetting-add.ts index 66b7f62c01d..325b8439bfb 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-add.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-add.ts @@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -24,10 +23,6 @@ class EntraGroupSettingAddCommand extends GraphCommand { return 'Creates a group setting'; } - public alias(): string[] | undefined { - return [aadCommands.GROUPSETTING_ADD]; - } - constructor() { super(); @@ -60,8 +55,6 @@ class EntraGroupSettingAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUPSETTING_ADD, commands.GROUPSETTING_ADD); - if (this.verbose) { await logger.logToStderr(`Retrieving group setting template with id '${args.options.templateId}'...`); } diff --git a/src/m365/entra/commands/groupsetting/groupsetting-get.spec.ts b/src/m365/entra/commands/groupsetting/groupsetting-get.spec.ts index b032cad31e4..fbc6cdf579d 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-get.spec.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-get.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './groupsetting-get.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUPSETTING_GET, () => { let log: string[]; @@ -64,16 +63,6 @@ describe(commands.GROUPSETTING_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUPSETTING_GET]); - }); - it('retrieves information about the specified Group Setting', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groupSettings/1caf7dcd-7e83-4c3a-94f7-932a1299c844`) { diff --git a/src/m365/entra/commands/groupsetting/groupsetting-get.ts b/src/m365/entra/commands/groupsetting/groupsetting-get.ts index e38a13ce2ac..95305c50ae8 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-get.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-get.ts @@ -3,7 +3,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -23,10 +22,6 @@ class EntraGroupSettingGetCommand extends GraphCommand { return 'Gets information about the particular group setting'; } - public alias(): string[] | undefined { - return [aadCommands.GROUPSETTING_GET]; - } - constructor() { super(); @@ -55,8 +50,6 @@ class EntraGroupSettingGetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUPSETTING_GET, commands.GROUPSETTING_GET); - try { const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/groupSettings/${args.options.id}`, diff --git a/src/m365/entra/commands/groupsetting/groupsetting-list.spec.ts b/src/m365/entra/commands/groupsetting/groupsetting-list.spec.ts index 6fc1cec91f5..1ddfe94ef5f 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-list.spec.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-list.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './groupsetting-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUPSETTING_LIST, () => { let log: string[]; @@ -61,16 +60,6 @@ describe(commands.GROUPSETTING_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUPSETTING_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName']); }); diff --git a/src/m365/entra/commands/groupsetting/groupsetting-list.ts b/src/m365/entra/commands/groupsetting/groupsetting-list.ts index 47a090d5e7c..eeab4ae3ebd 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-list.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-list.ts @@ -3,7 +3,6 @@ import { Logger } from '../../../../cli/Logger.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; class EntraGroupSettingListCommand extends GraphCommand { public get name(): string { @@ -14,17 +13,11 @@ class EntraGroupSettingListCommand extends GraphCommand { return 'Lists Entra group settings'; } - public alias(): string[] | undefined { - return [aadCommands.GROUPSETTING_LIST]; - } - public defaultProperties(): string[] | undefined { return ['id', 'displayName']; } public async commandAction(logger: Logger): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUPSETTING_LIST, commands.GROUPSETTING_LIST); - try { const groupSettings = await odata.getAllItems(`${this.resource}/v1.0/groupSettings`); await logger.log(groupSettings); diff --git a/src/m365/entra/commands/groupsetting/groupsetting-remove.spec.ts b/src/m365/entra/commands/groupsetting/groupsetting-remove.spec.ts index da66313ff97..7f616096824 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-remove.spec.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-remove.spec.ts @@ -13,7 +13,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './groupsetting-remove.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUPSETTING_REMOVE, () => { let log: string[]; @@ -73,16 +72,6 @@ describe(commands.GROUPSETTING_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUPSETTING_REMOVE]); - }); - it('removes the specified group setting without prompting for confirmation when force option specified', async () => { const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { if (opts.url === 'https://graph.microsoft.com/v1.0/groupSettings/28beab62-7540-4db1-a23f-29a6018a3848') { diff --git a/src/m365/entra/commands/groupsetting/groupsetting-remove.ts b/src/m365/entra/commands/groupsetting/groupsetting-remove.ts index d19453c86e2..7f89a7a3d03 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-remove.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-remove.ts @@ -4,7 +4,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -25,10 +24,6 @@ class EntraGroupSettingRemoveCommand extends GraphCommand { return 'Removes the particular group setting'; } - public alias(): string[] | undefined { - return [aadCommands.GROUPSETTING_REMOVE]; - } - constructor() { super(); @@ -69,8 +64,6 @@ class EntraGroupSettingRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUPSETTING_REMOVE, commands.GROUPSETTING_REMOVE); - const removeGroupSetting = async (): Promise => { if (this.verbose) { await logger.logToStderr(`Removing group setting: ${args.options.id}...`); diff --git a/src/m365/entra/commands/groupsetting/groupsetting-set.spec.ts b/src/m365/entra/commands/groupsetting/groupsetting-set.spec.ts index 2c8fd4eabfd..9383f71552f 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-set.spec.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-set.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './groupsetting-set.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUPSETTING_SET, () => { let log: string[]; @@ -66,16 +65,6 @@ describe(commands.GROUPSETTING_SET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUPSETTING_SET]); - }); - it('updates group setting', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groupSettings/c391b57d-5783-4c53-9236-cefb5c6ef323`) { diff --git a/src/m365/entra/commands/groupsetting/groupsetting-set.ts b/src/m365/entra/commands/groupsetting/groupsetting-set.ts index b192f0132c8..78be6f8c18d 100644 --- a/src/m365/entra/commands/groupsetting/groupsetting-set.ts +++ b/src/m365/entra/commands/groupsetting/groupsetting-set.ts @@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -24,10 +23,6 @@ class EntraGroupSettingSetCommand extends GraphCommand { return 'Updates the particular group setting'; } - public alias(): string[] | undefined { - return [aadCommands.GROUPSETTING_SET]; - } - public allowUnknownOptions(): boolean | undefined { return true; } @@ -60,8 +55,6 @@ class EntraGroupSettingSetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUPSETTING_SET, commands.GROUPSETTING_SET); - if (this.verbose) { await logger.logToStderr(`Retrieving group setting with id '${args.options.id}'...`); } diff --git a/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-get.spec.ts b/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-get.spec.ts index 02c2dab2b77..bac4967f757 100644 --- a/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-get.spec.ts +++ b/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-get.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './groupsettingtemplate-get.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUPSETTINGTEMPLATE_GET, () => { let log: string[]; @@ -67,16 +66,6 @@ describe(commands.GROUPSETTINGTEMPLATE_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUPSETTINGTEMPLATE_GET]); - }); - it('retrieves group setting template by id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groupSettingTemplates`) { diff --git a/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-get.ts b/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-get.ts index d7f8f624864..13646081cd8 100644 --- a/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-get.ts +++ b/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-get.ts @@ -5,7 +5,6 @@ import { odata } from '../../../../utils/odata.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -25,10 +24,6 @@ class EntraGroupSettingTemplateGetCommand extends GraphCommand { return 'Gets information about the specified Entra group settings template'; } - public alias(): string[] | undefined { - return [aadCommands.GROUPSETTINGTEMPLATE_GET]; - } - constructor() { super(); @@ -76,8 +71,6 @@ class EntraGroupSettingTemplateGetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUPSETTINGTEMPLATE_GET, commands.GROUPSETTINGTEMPLATE_GET); - try { const templates = await odata.getAllItems(`${this.resource}/v1.0/groupSettingTemplates`); diff --git a/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-list.spec.ts b/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-list.spec.ts index fae3e1ef34c..6ae07d80be5 100644 --- a/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-list.spec.ts +++ b/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-list.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './groupsettingtemplate-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.GROUPSETTINGTEMPLATE_LIST, () => { let log: string[]; @@ -61,16 +60,6 @@ describe(commands.GROUPSETTINGTEMPLATE_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.GROUPSETTINGTEMPLATE_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName']); }); diff --git a/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-list.ts b/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-list.ts index 3372562e795..301d0654cd6 100644 --- a/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-list.ts +++ b/src/m365/entra/commands/groupsettingtemplate/groupsettingtemplate-list.ts @@ -3,7 +3,6 @@ import { Logger } from '../../../../cli/Logger.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; class EntraGroupSettingTemplateListCommand extends GraphCommand { public get name(): string { @@ -14,17 +13,11 @@ class EntraGroupSettingTemplateListCommand extends GraphCommand { return 'Lists Entra group settings templates'; } - public alias(): string[] | undefined { - return [aadCommands.GROUPSETTINGTEMPLATE_LIST]; - } - public defaultProperties(): string[] | undefined { return ['id', 'displayName']; } public async commandAction(logger: Logger): Promise { - await this.showDeprecationWarning(logger, aadCommands.GROUPSETTINGTEMPLATE_LIST, commands.GROUPSETTINGTEMPLATE_LIST); - try { const templates = await odata.getAllItems(`${this.resource}/v1.0/groupSettingTemplates`); await logger.log(templates); diff --git a/src/m365/entra/commands/license/license-list.spec.ts b/src/m365/entra/commands/license/license-list.spec.ts index 3352e6afe6a..0209d51c014 100644 --- a/src/m365/entra/commands/license/license-list.spec.ts +++ b/src/m365/entra/commands/license/license-list.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './license-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.LICENSE_LIST, () => { //#region Mocked Responses @@ -110,16 +109,6 @@ describe(commands.LICENSE_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.LICENSE_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'skuId', 'skuPartNumber']); }); diff --git a/src/m365/entra/commands/license/license-list.ts b/src/m365/entra/commands/license/license-list.ts index 15557a9461f..b3bdcc9dfba 100644 --- a/src/m365/entra/commands/license/license-list.ts +++ b/src/m365/entra/commands/license/license-list.ts @@ -1,7 +1,6 @@ import { Logger } from '../../../../cli/Logger.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; class EntraLicenseListCommand extends GraphCommand { @@ -13,17 +12,11 @@ class EntraLicenseListCommand extends GraphCommand { return 'Lists commercial subscriptions that an organization has acquired'; } - public alias(): string[] | undefined { - return [aadCommands.LICENSE_LIST]; - } - public defaultProperties(): string[] | undefined { return ['id', 'skuId', 'skuPartNumber']; } public async commandAction(logger: Logger): Promise { - await this.showDeprecationWarning(logger, aadCommands.LICENSE_LIST, commands.LICENSE_LIST); - if (this.verbose) { await logger.logToStderr(`Retrieving the commercial subscriptions that an organization has acquired`); } diff --git a/src/m365/entra/commands/m365group/m365group-add.spec.ts b/src/m365/entra/commands/m365group/m365group-add.spec.ts index d984ad0309f..de48fd07452 100644 --- a/src/m365/entra/commands/m365group/m365group-add.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-add.spec.ts @@ -13,7 +13,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-add.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_ADD, () => { @@ -121,16 +120,6 @@ describe(commands.M365GROUP_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_ADD]); - }); - it('creates Microsoft 365 Group using basic info', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://graph.microsoft.com/v1.0/groups') { diff --git a/src/m365/entra/commands/m365group/m365group-add.ts b/src/m365/entra/commands/m365group/m365group-add.ts index 0edb70595ee..8238e6ca1e0 100644 --- a/src/m365/entra/commands/m365group/m365group-add.ts +++ b/src/m365/entra/commands/m365group/m365group-add.ts @@ -8,7 +8,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -41,10 +40,6 @@ class EntraM365GroupAddCommand extends GraphCommand { return 'Creates a Microsoft 365 Group'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_ADD]; - } - constructor() { super(); @@ -165,8 +160,6 @@ class EntraM365GroupAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_ADD, commands.M365GROUP_ADD); - let group: Group; let ownerIds: string[] = []; let memberIds: string[] = []; diff --git a/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts b/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts index 593b3915aa6..c9e5ae6745f 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts @@ -12,7 +12,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-conversation-list.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; import { cli } from '../../../../cli/cli.js'; describe(commands.M365GROUP_CONVERSATION_LIST, () => { @@ -91,16 +90,6 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_CONVERSATION_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['topic', 'lastDeliveredDateTime', 'id']); }); diff --git a/src/m365/entra/commands/m365group/m365group-conversation-list.ts b/src/m365/entra/commands/m365group/m365group-conversation-list.ts index bd54d2cacd0..813dd35fe37 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-list.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-list.ts @@ -6,7 +6,6 @@ import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -25,10 +24,6 @@ class EntraM365GroupConversationListCommand extends GraphCommand { return 'Lists conversations for the specified Microsoft 365 group'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_CONVERSATION_LIST]; - } - public defaultProperties(): string[] | undefined { return ['topic', 'lastDeliveredDateTime', 'id']; } @@ -61,8 +56,6 @@ class EntraM365GroupConversationListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_CONVERSATION_LIST, commands.M365GROUP_CONVERSATION_LIST); - try { const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.groupId); diff --git a/src/m365/entra/commands/m365group/m365group-conversation-post-list.spec.ts b/src/m365/entra/commands/m365group/m365group-conversation-post-list.spec.ts index 994f414c56f..1ae642b6c9e 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-post-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-post-list.spec.ts @@ -14,7 +14,6 @@ import commands from '../../commands.js'; import command from './m365group-conversation-post-list.js'; import { settingsNames } from '../../../../settingsNames.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_CONVERSATION_POST_LIST, () => { let log: string[]; @@ -122,19 +121,10 @@ describe(commands.M365GROUP_CONVERSATION_POST_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_CONVERSATION_POST_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['receivedDateTime', 'id']); }); + it('fails validation if groupId and groupName specified', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { @@ -147,6 +137,7 @@ describe(commands.M365GROUP_CONVERSATION_POST_LIST, () => { const actual = await command.validate({ options: { groupId: '1caf7dcd-7e83-4c3a-94f7-932a1299c844', groupName: 'MyGroup', threadId: '123' } }, commandInfo); assert.notStrictEqual(actual, true); }); + it('fails validation if neither groupId nor groupName specified', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { @@ -159,10 +150,12 @@ describe(commands.M365GROUP_CONVERSATION_POST_LIST, () => { const actual = await command.validate({ options: { threadId: '123' } }, commandInfo); assert.notStrictEqual(actual, true); }); + it('fails validation if the groupId is not a valid GUID', async () => { const actual = await command.validate({ options: { groupId: 'not-c49b-4fd4-8223-28f0ac3a6402', threadId: '123' } }, commandInfo); assert.notStrictEqual(actual, true); }); + it('passes validation if the groupId is a valid GUID', async () => { const actual = await command.validate({ options: { groupId: '1caf7dcd-7e83-4c3a-94f7-932a1299c844', threadId: '123' } }, commandInfo); assert.strictEqual(actual, true); @@ -187,6 +180,7 @@ describe(commands.M365GROUP_CONVERSATION_POST_LIST, () => { jsonOutput.value )); }); + it('Retrieve posts for the specified conversation threadId of m365 group groupName in the tenant (verbose)', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if ((opts.url as string).indexOf('/groups?$filter=displayName') > -1) { @@ -237,4 +231,4 @@ describe(commands.M365GROUP_CONVERSATION_POST_LIST, () => { await assert.rejects(command.action(logger, { options: { groupId: groupId, threadId: 'AAQkADkwN2Q2NDg1LWQ3ZGYtNDViZi1iNGRiLTVhYjJmN2Q5NDkxZQAQAOnRAfDf71lIvrdK85FAn5E=' } } as any), new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); }); -}); +}); \ No newline at end of file diff --git a/src/m365/entra/commands/m365group/m365group-conversation-post-list.ts b/src/m365/entra/commands/m365group/m365group-conversation-post-list.ts index a6baa453ed8..3e2d7840eac 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-post-list.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-post-list.ts @@ -7,7 +7,6 @@ import { odata } from '../../../../utils/odata.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -28,10 +27,6 @@ class EntraM365GroupConversationPostListCommand extends GraphCommand { return 'Lists conversation posts of a Microsoft 365 group'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_CONVERSATION_POST_LIST]; - } - constructor() { super(); @@ -85,8 +80,6 @@ class EntraM365GroupConversationPostListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_CONVERSATION_POST_LIST, commands.M365GROUP_CONVERSATION_POST_LIST); - try { const retrievedgroupId = await this.getGroupId(args); const isUnifiedGroup = await entraGroup.isUnifiedGroup(retrievedgroupId); diff --git a/src/m365/entra/commands/m365group/m365group-get.spec.ts b/src/m365/entra/commands/m365group/m365group-get.spec.ts index fdd501dd2cb..962e5824ade 100644 --- a/src/m365/entra/commands/m365group/m365group-get.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-get.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-get.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_GET, () => { let log: string[]; @@ -66,16 +65,6 @@ describe(commands.M365GROUP_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_GET]); - }); - it('retrieves information about the specified Microsoft 365 Group', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/1caf7dcd-7e83-4c3a-94f7-932a1299c844`) { diff --git a/src/m365/entra/commands/m365group/m365group-get.ts b/src/m365/entra/commands/m365group/m365group-get.ts index 9d7611b37b3..6b4dc99bfe6 100644 --- a/src/m365/entra/commands/m365group/m365group-get.ts +++ b/src/m365/entra/commands/m365group/m365group-get.ts @@ -4,7 +4,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; import { GroupExtended } from './GroupExtended.js'; @@ -26,10 +25,6 @@ class EntraM365GroupGetCommand extends GraphCommand { return 'Gets information about the specified Microsoft 365 Group or Microsoft Teams team'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_GET]; - } - constructor() { super(); @@ -61,8 +56,6 @@ class EntraM365GroupGetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_GET, commands.M365GROUP_GET); - let group: GroupExtended; try { diff --git a/src/m365/entra/commands/m365group/m365group-list.spec.ts b/src/m365/entra/commands/m365group/m365group-list.spec.ts index 0b6830d0cf1..47d529c2705 100644 --- a/src/m365/entra/commands/m365group/m365group-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-list.spec.ts @@ -14,7 +14,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_LIST, () => { let log: string[]; @@ -67,16 +66,6 @@ describe(commands.M365GROUP_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'mailNickname', 'siteUrl']); }); diff --git a/src/m365/entra/commands/m365group/m365group-list.ts b/src/m365/entra/commands/m365group/m365group-list.ts index 00019feb2c1..20a777b1bac 100644 --- a/src/m365/entra/commands/m365group/m365group-list.ts +++ b/src/m365/entra/commands/m365group/m365group-list.ts @@ -4,7 +4,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; import { GroupExtended } from './GroupExtended.js'; @@ -28,10 +27,6 @@ class EntraM365GroupListCommand extends GraphCommand { return 'Lists Microsoft 365 Groups in the current tenant'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_LIST]; - } - constructor() { super(); @@ -72,8 +67,6 @@ class EntraM365GroupListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_LIST, commands.M365GROUP_LIST); - const groupFilter: string = `?$filter=groupTypes/any(c:c+eq+'Unified')`; const displayNameFilter: string = args.options.displayName ? ` and startswith(DisplayName,'${formatting.encodeQueryParameter(args.options.displayName)}')` : ''; const mailNicknameFilter: string = args.options.mailNickname ? ` and startswith(MailNickname,'${formatting.encodeQueryParameter(args.options.mailNickname)}')` : ''; diff --git a/src/m365/entra/commands/m365group/m365group-recyclebinitem-clear.spec.ts b/src/m365/entra/commands/m365group/m365group-recyclebinitem-clear.spec.ts index 2c563f32899..304e2bab266 100644 --- a/src/m365/entra/commands/m365group/m365group-recyclebinitem-clear.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-recyclebinitem-clear.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-recyclebinitem-clear.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_RECYCLEBINITEM_CLEAR, () => { let log: string[]; @@ -70,16 +69,6 @@ describe(commands.M365GROUP_RECYCLEBINITEM_CLEAR, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_RECYCLEBINITEM_CLEAR]); - }); - it('clears the recycle bin items without prompting for confirmation when --force option specified', async () => { const deleteStub = sinon.stub(request, 'delete').resolves(); diff --git a/src/m365/entra/commands/m365group/m365group-recyclebinitem-clear.ts b/src/m365/entra/commands/m365group/m365group-recyclebinitem-clear.ts index 9829dc99617..6319d1e3139 100644 --- a/src/m365/entra/commands/m365group/m365group-recyclebinitem-clear.ts +++ b/src/m365/entra/commands/m365group/m365group-recyclebinitem-clear.ts @@ -6,7 +6,6 @@ import request from '../../../../request.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -25,10 +24,6 @@ class EntraM365GroupRecycleBinItemClearCommand extends GraphCommand { return 'Clears all M365 Groups from recycle bin.'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_RECYCLEBINITEM_CLEAR]; - } - constructor() { super(); @@ -53,8 +48,6 @@ class EntraM365GroupRecycleBinItemClearCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_RECYCLEBINITEM_CLEAR, commands.M365GROUP_RECYCLEBINITEM_CLEAR); - const clearM365GroupRecycleBinItems = async (): Promise => { try { await this.processRecycleBinItemsClear(); diff --git a/src/m365/entra/commands/m365group/m365group-recyclebinitem-list.spec.ts b/src/m365/entra/commands/m365group/m365group-recyclebinitem-list.spec.ts index d2d24332087..1d1bd2d3a2b 100644 --- a/src/m365/entra/commands/m365group/m365group-recyclebinitem-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-recyclebinitem-list.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-recyclebinitem-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_RECYCLEBINITEM_LIST, () => { let log: string[]; @@ -61,16 +60,6 @@ describe(commands.M365GROUP_RECYCLEBINITEM_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_RECYCLEBINITEM_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'mailNickname']); }); diff --git a/src/m365/entra/commands/m365group/m365group-recyclebinitem-list.ts b/src/m365/entra/commands/m365group/m365group-recyclebinitem-list.ts index 93859678918..dde8f5219db 100644 --- a/src/m365/entra/commands/m365group/m365group-recyclebinitem-list.ts +++ b/src/m365/entra/commands/m365group/m365group-recyclebinitem-list.ts @@ -5,7 +5,6 @@ import { formatting } from '../../../../utils/formatting.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -25,10 +24,6 @@ class EntraM365GroupRecycleBinItemListCommand extends GraphCommand { return 'Lists Microsoft 365 Groups deleted in the current tenant'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_RECYCLEBINITEM_LIST]; - } - constructor() { super(); @@ -61,8 +56,6 @@ class EntraM365GroupRecycleBinItemListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_RECYCLEBINITEM_LIST, commands.M365GROUP_RECYCLEBINITEM_LIST); - try { const filter: string = `?$filter=groupTypes/any(c:c+eq+'Unified')`; const displayNameFilter: string = args.options.groupName ? ` and startswith(DisplayName,'${formatting.encodeQueryParameter(args.options.groupName).replace(/'/g, `''`)}')` : ''; diff --git a/src/m365/entra/commands/m365group/m365group-recyclebinitem-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-recyclebinitem-remove.spec.ts index c80a4d41dfe..751661880e4 100644 --- a/src/m365/entra/commands/m365group/m365group-recyclebinitem-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-recyclebinitem-remove.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-recyclebinitem-remove.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_RECYCLEBINITEM_REMOVE, () => { const validGroupId = '00000000-0000-0000-0000-000000000000'; @@ -122,16 +121,6 @@ describe(commands.M365GROUP_RECYCLEBINITEM_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_RECYCLEBINITEM_REMOVE]); - }); - it('fails validation when id is not a valid GUID', async () => { const actual = await command.validate({ options: { diff --git a/src/m365/entra/commands/m365group/m365group-recyclebinitem-remove.ts b/src/m365/entra/commands/m365group/m365group-recyclebinitem-remove.ts index 7751fa9338b..64ac3680379 100644 --- a/src/m365/entra/commands/m365group/m365group-recyclebinitem-remove.ts +++ b/src/m365/entra/commands/m365group/m365group-recyclebinitem-remove.ts @@ -7,7 +7,6 @@ import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -29,10 +28,6 @@ class EntraM365GroupRecycleBinItemRemoveCommand extends GraphCommand { return 'Permanently deletes a Microsoft 365 Group from the recycle bin in the current tenant'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_RECYCLEBINITEM_REMOVE]; - } - constructor() { super(); @@ -87,8 +82,6 @@ class EntraM365GroupRecycleBinItemRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_RECYCLEBINITEM_REMOVE, commands.M365GROUP_RECYCLEBINITEM_REMOVE); - const removeGroup: () => Promise = async (): Promise => { try { const groupId = await this.getGroupId(args.options); diff --git a/src/m365/entra/commands/m365group/m365group-recyclebinitem-restore.spec.ts b/src/m365/entra/commands/m365group/m365group-recyclebinitem-restore.spec.ts index b4165dd0370..4d51190373a 100644 --- a/src/m365/entra/commands/m365group/m365group-recyclebinitem-restore.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-recyclebinitem-restore.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-recyclebinitem-restore.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_RECYCLEBINITEM_RESTORE, () => { const validGroupId = '00000000-0000-0000-0000-000000000000'; @@ -116,16 +115,6 @@ describe(commands.M365GROUP_RECYCLEBINITEM_RESTORE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_RECYCLEBINITEM_RESTORE]); - }); - it('fails validation if the id is not a valid GUID', async () => { const actual = await command.validate({ options: { id: 'abc' } }, commandInfo); assert.notStrictEqual(actual, true); diff --git a/src/m365/entra/commands/m365group/m365group-recyclebinitem-restore.ts b/src/m365/entra/commands/m365group/m365group-recyclebinitem-restore.ts index b30a40716ba..02aec59d3b2 100644 --- a/src/m365/entra/commands/m365group/m365group-recyclebinitem-restore.ts +++ b/src/m365/entra/commands/m365group/m365group-recyclebinitem-restore.ts @@ -7,7 +7,6 @@ import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -28,10 +27,6 @@ class EntraM365GroupRecycleBinItemRestoreCommand extends GraphCommand { return 'Restores a deleted Microsoft 365 Group'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_RECYCLEBINITEM_RESTORE]; - } - constructor() { super(); @@ -82,8 +77,6 @@ class EntraM365GroupRecycleBinItemRestoreCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_RECYCLEBINITEM_RESTORE, commands.M365GROUP_RECYCLEBINITEM_RESTORE); - if (this.verbose) { await logger.logToStderr(`Restoring Microsoft 365 Group: ${args.options.id || args.options.displayName || args.options.mailNickname}...`); } diff --git a/src/m365/entra/commands/m365group/m365group-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-remove.spec.ts index a035ded8ede..b2f5cdba427 100644 --- a/src/m365/entra/commands/m365group/m365group-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-remove.spec.ts @@ -15,7 +15,6 @@ import commands from '../../commands.js'; import { spo } from '../../../../utils/spo.js'; import command from './m365group-remove.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_REMOVE, () => { let log: string[]; @@ -160,16 +159,6 @@ describe(commands.M365GROUP_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_REMOVE]); - }); - it('fails validation if the id is not a valid GUID', async () => { const actual = await command.validate({ options: { id: 'abc' } }, commandInfo); assert.notStrictEqual(actual, true); diff --git a/src/m365/entra/commands/m365group/m365group-remove.ts b/src/m365/entra/commands/m365group/m365group-remove.ts index 142c2509617..c6384714180 100644 --- a/src/m365/entra/commands/m365group/m365group-remove.ts +++ b/src/m365/entra/commands/m365group/m365group-remove.ts @@ -10,7 +10,6 @@ import config from '../../../../config.js'; import { formatting } from '../../../../utils/formatting.js'; import { ClientSvcResponse, ClientSvcResponseContents, FormDigestInfo, spo } from '../../../../utils/spo.js'; import { setTimeout } from 'timers/promises'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -34,10 +33,6 @@ class EntraM365GroupRemoveCommand extends GraphCommand { return 'Removes a Microsoft 365 Group'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_REMOVE]; - } - constructor() { super(); @@ -82,8 +77,6 @@ class EntraM365GroupRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_REMOVE, commands.M365GROUP_REMOVE); - const removeGroup = async (): Promise => { if (this.verbose) { await logger.logToStderr(`Removing Microsoft 365 Group: ${args.options.id}...`); diff --git a/src/m365/entra/commands/m365group/m365group-renew.spec.ts b/src/m365/entra/commands/m365group/m365group-renew.spec.ts index 086879d22ff..1eb96a99173 100644 --- a/src/m365/entra/commands/m365group/m365group-renew.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-renew.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-renew.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_RENEW, () => { let log: string[]; @@ -69,16 +68,6 @@ describe(commands.M365GROUP_RENEW, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_RENEW]); - }); - it('renews expiration the specified group', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848/renew/') { diff --git a/src/m365/entra/commands/m365group/m365group-renew.ts b/src/m365/entra/commands/m365group/m365group-renew.ts index b68cba2940a..aa251b235ca 100644 --- a/src/m365/entra/commands/m365group/m365group-renew.ts +++ b/src/m365/entra/commands/m365group/m365group-renew.ts @@ -4,7 +4,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -24,10 +23,6 @@ class EntraM365GroupRenewCommand extends GraphCommand { return `Renews Microsoft 365 group's expiration`; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_RENEW]; - } - constructor() { super(); @@ -56,8 +51,6 @@ class EntraM365GroupRenewCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_RENEW, commands.M365GROUP_RENEW); - if (this.verbose) { await logger.logToStderr(`Renewing Microsoft 365 group's expiration: ${args.options.id}...`); } diff --git a/src/m365/entra/commands/m365group/m365group-report-activitycounts.spec.ts b/src/m365/entra/commands/m365group/m365group-report-activitycounts.spec.ts index 1dac6316ff0..13c888a4e8a 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activitycounts.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activitycounts.spec.ts @@ -9,7 +9,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-report-activitycounts.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_REPORT_ACTIVITYCOUNTS, () => { let log: string[]; @@ -58,16 +57,6 @@ describe(commands.M365GROUP_REPORT_ACTIVITYCOUNTS, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_REPORT_ACTIVITYCOUNTS]); - }); - it('gets the number of group activities across group workloads for the given period', async () => { const requestStub: sinon.SinonStub = sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityCounts(period='D7')`) { diff --git a/src/m365/entra/commands/m365group/m365group-report-activitycounts.ts b/src/m365/entra/commands/m365group/m365group-report-activitycounts.ts index 63187fe38f7..38f4963420b 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activitycounts.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activitycounts.ts @@ -1,5 +1,4 @@ import PeriodBasedReport from '../../../base/PeriodBasedReport.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; class M365GroupReportActivityCountsCommand extends PeriodBasedReport { @@ -11,10 +10,6 @@ class M365GroupReportActivityCountsCommand extends PeriodBasedReport { return 'Get the number of group activities across group workloads'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_REPORT_ACTIVITYCOUNTS]; - } - public get usageEndpoint(): string { return 'getOffice365GroupsActivityCounts'; } diff --git a/src/m365/entra/commands/m365group/m365group-report-activitydetail.spec.ts b/src/m365/entra/commands/m365group/m365group-report-activitydetail.spec.ts index 6b8e1e04015..2885c2b1e1c 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activitydetail.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activitydetail.spec.ts @@ -9,7 +9,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-report-activitydetail.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_REPORT_ACTIVITYDETAIL, () => { let log: string[]; @@ -58,16 +57,6 @@ describe(commands.M365GROUP_REPORT_ACTIVITYDETAIL, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_REPORT_ACTIVITYDETAIL]); - }); - it('gets details about Microsoft 365 Groups activity by group for the given period', async () => { const requestStub: sinon.SinonStub = sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityDetail(period='D7')`) { diff --git a/src/m365/entra/commands/m365group/m365group-report-activitydetail.ts b/src/m365/entra/commands/m365group/m365group-report-activitydetail.ts index db2b6153024..f1c372b02ae 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activitydetail.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activitydetail.ts @@ -1,5 +1,4 @@ import DateAndPeriodBasedReport from '../../../base/DateAndPeriodBasedReport.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; class M365GroupReportActivityDetailCommand extends DateAndPeriodBasedReport { @@ -11,10 +10,6 @@ class M365GroupReportActivityDetailCommand extends DateAndPeriodBasedReport { return 'Get details about Microsoft 365 Groups activity by group'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_REPORT_ACTIVITYDETAIL]; - } - public get usageEndpoint(): string { return 'getOffice365GroupsActivityDetail'; } diff --git a/src/m365/entra/commands/m365group/m365group-report-activityfilecounts.spec.ts b/src/m365/entra/commands/m365group/m365group-report-activityfilecounts.spec.ts index a0952e60045..7c1c20df5b3 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activityfilecounts.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activityfilecounts.spec.ts @@ -9,7 +9,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-report-activityfilecounts.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_REPORT_ACTIVITYFILECOUNTS, () => { let log: string[]; @@ -58,16 +57,6 @@ describe(commands.M365GROUP_REPORT_ACTIVITYFILECOUNTS, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_REPORT_ACTIVITYFILECOUNTS]); - }); - it('gets the number of group activities across group workloads for the given period', async () => { const requestStub: sinon.SinonStub = sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityFileCounts(period='D7')`) { @@ -83,4 +72,4 @@ describe(commands.M365GROUP_REPORT_ACTIVITYFILECOUNTS, () => { assert.strictEqual(requestStub.lastCall.args[0].url, "https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityFileCounts(period='D7')"); assert.strictEqual(requestStub.lastCall.args[0].headers["accept"], 'application/json;odata.metadata=none'); }); -}); +}); \ No newline at end of file diff --git a/src/m365/entra/commands/m365group/m365group-report-activityfilecounts.ts b/src/m365/entra/commands/m365group/m365group-report-activityfilecounts.ts index bd8f4a831e8..bd6ccb27659 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activityfilecounts.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activityfilecounts.ts @@ -1,5 +1,4 @@ import PeriodBasedReport from '../../../base/PeriodBasedReport.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; class M365GroupReportActivityFileCountsCommand extends PeriodBasedReport { @@ -11,10 +10,6 @@ class M365GroupReportActivityFileCountsCommand extends PeriodBasedReport { return 'Get the total number of files and how many of them were active across all group sites associated with an Microsoft 365 Group'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_REPORT_ACTIVITYFILECOUNTS]; - } - public get usageEndpoint(): string { return 'getOffice365GroupsActivityFileCounts'; } diff --git a/src/m365/entra/commands/m365group/m365group-report-activitygroupcounts.spec.ts b/src/m365/entra/commands/m365group/m365group-report-activitygroupcounts.spec.ts index 30f823dce3d..ce6703f8c43 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activitygroupcounts.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activitygroupcounts.spec.ts @@ -9,7 +9,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-report-activitygroupcounts.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_REPORT_ACTIVITYGROUPCOUNTS, () => { let log: string[]; @@ -58,16 +57,6 @@ describe(commands.M365GROUP_REPORT_ACTIVITYGROUPCOUNTS, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_REPORT_ACTIVITYGROUPCOUNTS]); - }); - it('gets the daily total number of groups and how many of them were active based on activities for the given period', async () => { const requestStub: sinon.SinonStub = sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityGroupCounts(period='D7')`) { @@ -85,4 +74,4 @@ describe(commands.M365GROUP_REPORT_ACTIVITYGROUPCOUNTS, () => { assert.strictEqual(requestStub.lastCall.args[0].url, "https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityGroupCounts(period='D7')"); assert.strictEqual(requestStub.lastCall.args[0].headers["accept"], 'application/json;odata.metadata=none'); }); -}); +}); \ No newline at end of file diff --git a/src/m365/entra/commands/m365group/m365group-report-activitygroupcounts.ts b/src/m365/entra/commands/m365group/m365group-report-activitygroupcounts.ts index 664b023fdc7..219f4d46a3c 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activitygroupcounts.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activitygroupcounts.ts @@ -1,5 +1,4 @@ import PeriodBasedReport from '../../../base/PeriodBasedReport.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; class M365GroupReportActivityGroupCountsCommand extends PeriodBasedReport { @@ -11,10 +10,6 @@ class M365GroupReportActivityGroupCountsCommand extends PeriodBasedReport { return 'Get the daily total number of groups and how many of them were active based on email conversations, Viva Engage posts, and SharePoint file activities'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_REPORT_ACTIVITYGROUPCOUNTS]; - } - public get usageEndpoint(): string { return 'getOffice365GroupsActivityGroupCounts'; } diff --git a/src/m365/entra/commands/m365group/m365group-report-activitystorage.spec.ts b/src/m365/entra/commands/m365group/m365group-report-activitystorage.spec.ts index c6ba3c19586..144a00803f5 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activitystorage.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activitystorage.spec.ts @@ -9,7 +9,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-report-activitystorage.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_REPORT_ACTIVITYSTORAGE, () => { let log: string[]; @@ -58,16 +57,6 @@ describe(commands.M365GROUP_REPORT_ACTIVITYSTORAGE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_REPORT_ACTIVITYSTORAGE]); - }); - it('get the reports', async () => { const requestStub: sinon.SinonStub = sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityStorage(period='D7')`) { @@ -83,4 +72,4 @@ describe(commands.M365GROUP_REPORT_ACTIVITYSTORAGE, () => { assert.strictEqual(requestStub.lastCall.args[0].url, "https://graph.microsoft.com/v1.0/reports/getOffice365GroupsActivityStorage(period='D7')"); assert.strictEqual(requestStub.lastCall.args[0].headers["accept"], 'application/json;odata.metadata=none'); }); -}); +}); \ No newline at end of file diff --git a/src/m365/entra/commands/m365group/m365group-report-activitystorage.ts b/src/m365/entra/commands/m365group/m365group-report-activitystorage.ts index 442900e5d6d..8d87d227010 100644 --- a/src/m365/entra/commands/m365group/m365group-report-activitystorage.ts +++ b/src/m365/entra/commands/m365group/m365group-report-activitystorage.ts @@ -1,5 +1,4 @@ import PeriodBasedReport from '../../../base/PeriodBasedReport.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; class M365GroupReportActivityStorageCommand extends PeriodBasedReport { @@ -11,10 +10,6 @@ class M365GroupReportActivityStorageCommand extends PeriodBasedReport { return 'Get the total storage used across all group mailboxes and group sites'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_REPORT_ACTIVITYSTORAGE]; - } - public get usageEndpoint(): string { return 'getOffice365GroupsActivityStorage'; } diff --git a/src/m365/entra/commands/m365group/m365group-set.spec.ts b/src/m365/entra/commands/m365group/m365group-set.spec.ts index f41d0376b58..4e4816412dd 100644 --- a/src/m365/entra/commands/m365group/m365group-set.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-set.spec.ts @@ -15,7 +15,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-set.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; import { accessToken } from '../../../../utils/accessToken.js'; describe(commands.M365GROUP_SET, () => { @@ -119,16 +118,6 @@ describe(commands.M365GROUP_SET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_SET]); - }); - it('updates Microsoft 365 Group display name while group is being retrieved by display name', async () => { const groupName = 'Project A'; sinon.stub(entraGroup, 'getGroupIdByDisplayName').withArgs(groupName).resolves(groupId); diff --git a/src/m365/entra/commands/m365group/m365group-set.ts b/src/m365/entra/commands/m365group/m365group-set.ts index 435a8dd573f..65194e77951 100644 --- a/src/m365/entra/commands/m365group/m365group-set.ts +++ b/src/m365/entra/commands/m365group/m365group-set.ts @@ -9,7 +9,6 @@ import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; import { accessToken } from '../../../../utils/accessToken.js'; import auth from '../../../../Auth.js'; import { entraUser } from '../../../../utils/entraUser.js'; @@ -50,10 +49,6 @@ class EntraM365GroupSetCommand extends GraphCommand { return 'Updates Microsoft 365 Group properties'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_SET]; - } - constructor() { super(); @@ -226,8 +221,6 @@ class EntraM365GroupSetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_SET, commands.M365GROUP_SET); - try { if ((args.options.allowExternalSenders !== undefined || args.options.autoSubscribeNewMembers !== undefined) && accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken)) { throw `Option 'allowExternalSenders' and 'autoSubscribeNewMembers' can only be used when using delegated permissions.`; diff --git a/src/m365/entra/commands/m365group/m365group-teamify.spec.ts b/src/m365/entra/commands/m365group/m365group-teamify.spec.ts index f8c1b22baff..727be4534ed 100644 --- a/src/m365/entra/commands/m365group/m365group-teamify.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-teamify.spec.ts @@ -14,7 +14,6 @@ import commands from '../../commands.js'; import command from './m365group-teamify.js'; import { settingsNames } from '../../../../settingsNames.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_TEAMIFY, () => { let log: string[]; @@ -69,16 +68,6 @@ describe(commands.M365GROUP_TEAMIFY, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_TEAMIFY]); - }); - it('fails validation if both id and mailNickname options are not passed', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { diff --git a/src/m365/entra/commands/m365group/m365group-teamify.ts b/src/m365/entra/commands/m365group/m365group-teamify.ts index 2f4a2d8ee7e..de91436791a 100644 --- a/src/m365/entra/commands/m365group/m365group-teamify.ts +++ b/src/m365/entra/commands/m365group/m365group-teamify.ts @@ -6,7 +6,6 @@ import { entraGroup } from '../../../../utils/entraGroup.js'; import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -27,10 +26,6 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { return 'Creates a new Microsoft Teams team under existing Microsoft 365 group'; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_TEAMIFY]; - } - constructor() { super(); @@ -106,8 +101,6 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_TEAMIFY, commands.M365GROUP_TEAMIFY); - try { const groupId = await this.getGroupId(args.options); const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId); diff --git a/src/m365/entra/commands/m365group/m365group-user-add.spec.ts b/src/m365/entra/commands/m365group/m365group-user-add.spec.ts index 66fa016da6d..e27c9624cbf 100644 --- a/src/m365/entra/commands/m365group/m365group-user-add.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-add.spec.ts @@ -10,13 +10,11 @@ import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; -import teamsCommands from '../../../teams/commands.js'; import commands from '../../commands.js'; import command from './m365group-user-add.js'; import { settingsNames } from '../../../../settingsNames.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { entraUser } from '../../../../utils/entraUser.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_USER_ADD, () => { const groupId = '3f04e370-cbc6-4091-80fe-1d038be2ad06'; @@ -75,16 +73,6 @@ describe(commands.M365GROUP_USER_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [teamsCommands.USER_ADD, aadCommands.M365GROUP_USER_ADD]); - }); - it('fails validation if the groupId is not a valid guid.', async () => { const actual = await command.validate({ options: { diff --git a/src/m365/entra/commands/m365group/m365group-user-add.ts b/src/m365/entra/commands/m365group/m365group-user-add.ts index 94320d2847b..0d43d3e3d7f 100644 --- a/src/m365/entra/commands/m365group/m365group-user-add.ts +++ b/src/m365/entra/commands/m365group/m365group-user-add.ts @@ -5,8 +5,6 @@ import { entraGroup } from '../../../../utils/entraGroup.js'; import { validation } from '../../../../utils/validation.js'; import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import teamsCommands from '../../../teams/commands.js'; -import aadCommands from '../../aadCommands.js'; import { entraUser } from '../../../../utils/entraUser.js'; import commands from '../../commands.js'; @@ -35,10 +33,6 @@ class EntraM365GroupUserAddCommand extends GraphCommand { return 'Adds user to specified Microsoft 365 Group or Microsoft Teams team'; } - public alias(): string[] | undefined { - return [teamsCommands.USER_ADD, aadCommands.M365GROUP_USER_ADD]; - } - constructor() { super(); @@ -134,8 +128,6 @@ class EntraM365GroupUserAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_USER_ADD, commands.M365GROUP_USER_ADD); - try { const providedGroupId: string = await this.getGroupId(logger, args); const isUnifiedGroup = await entraGroup.isUnifiedGroup(providedGroupId); diff --git a/src/m365/entra/commands/m365group/m365group-user-list.spec.ts b/src/m365/entra/commands/m365group/m365group-user-list.spec.ts index 15f071e272b..73c329c0bf0 100644 --- a/src/m365/entra/commands/m365group/m365group-user-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-list.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-user-list.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_USER_LIST, () => { let log: string[]; @@ -67,16 +66,6 @@ describe(commands.M365GROUP_USER_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.M365GROUP_USER_LIST]); - }); - it('fails validation if the groupId is not a valid guid.', async () => { const actual = await command.validate({ options: { diff --git a/src/m365/entra/commands/m365group/m365group-user-list.ts b/src/m365/entra/commands/m365group/m365group-user-list.ts index 90de68209ab..237da67859e 100644 --- a/src/m365/entra/commands/m365group/m365group-user-list.ts +++ b/src/m365/entra/commands/m365group/m365group-user-list.ts @@ -7,7 +7,6 @@ import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { CliRequestOptions } from '../../../../request.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -34,10 +33,6 @@ class EntraM365GroupUserListCommand extends GraphCommand { return "Lists users for the specified Microsoft 365 group"; } - public alias(): string[] | undefined { - return [aadCommands.M365GROUP_USER_LIST]; - } - constructor() { super(); @@ -107,8 +102,6 @@ class EntraM365GroupUserListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_USER_LIST, commands.M365GROUP_USER_LIST); - try { const groupId = await this.getGroupId(args.options, logger); const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId); diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts index eec3e1c0fdb..6b2b25dfd02 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts @@ -10,12 +10,10 @@ import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; -import teamsCommands from '../../../teams/commands.js'; import commands from '../../commands.js'; import command from './m365group-user-remove.js'; import { settingsNames } from '../../../../settingsNames.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_USER_REMOVE, () => { let log: string[]; @@ -76,16 +74,6 @@ describe(commands.M365GROUP_USER_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [teamsCommands.USER_REMOVE, aadCommands.M365GROUP_USER_REMOVE]); - }); - it('fails validation if the groupId is not a valid guid.', async () => { const actual = await command.validate({ options: { diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.ts b/src/m365/entra/commands/m365group/m365group-user-remove.ts index 16e94fa3b77..71d14fd1852 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.ts @@ -6,8 +6,6 @@ import { entraGroup } from '../../../../utils/entraGroup.js'; import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import teamsCommands from '../../../teams/commands.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -34,10 +32,6 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { return 'Removes the specified user from specified Microsoft 365 Group or Microsoft Teams team'; } - public alias(): string[] | undefined { - return [teamsCommands.USER_REMOVE, aadCommands.M365GROUP_USER_REMOVE]; - } - constructor() { super(); @@ -95,8 +89,6 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_USER_REMOVE, commands.M365GROUP_USER_REMOVE); - const groupId: string = (typeof args.options.groupId !== 'undefined') ? args.options.groupId : args.options.teamId as string; const removeUser = async (): Promise => { diff --git a/src/m365/entra/commands/m365group/m365group-user-set.spec.ts b/src/m365/entra/commands/m365group/m365group-user-set.spec.ts index 89e98303411..0a2912c336d 100644 --- a/src/m365/entra/commands/m365group/m365group-user-set.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-set.spec.ts @@ -10,12 +10,10 @@ import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; -import teamsCommands from '../../../teams/commands.js'; import commands from '../../commands.js'; import command from './m365group-user-set.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { entraUser } from '../../../../utils/entraUser.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.M365GROUP_USER_SET, () => { const groupId = '630dfae3-6904-4154-acc2-812e11205351'; @@ -73,16 +71,6 @@ describe(commands.M365GROUP_USER_SET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [teamsCommands.USER_SET, aadCommands.M365GROUP_USER_SET]); - }); - it('fails validation if groupId is not a valid GUID', async () => { const actual = await command.validate({ options: { groupId: 'foo', ids: userIds[0], role: 'Member' } }, commandInfo); assert.notStrictEqual(actual, true); diff --git a/src/m365/entra/commands/m365group/m365group-user-set.ts b/src/m365/entra/commands/m365group/m365group-user-set.ts index 02a0d62efba..be1d9e0d51f 100644 --- a/src/m365/entra/commands/m365group/m365group-user-set.ts +++ b/src/m365/entra/commands/m365group/m365group-user-set.ts @@ -4,11 +4,9 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import teamsCommands from '../../../teams/commands.js'; import commands from '../../commands.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { entraUser } from '../../../../utils/entraUser.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -36,11 +34,6 @@ class EntraM365GroupUserSetCommand extends GraphCommand { return 'Updates role of the specified user in the specified Microsoft 365 Group or Microsoft Teams team'; } - - public alias(): string[] | undefined { - return [teamsCommands.USER_SET, aadCommands.M365GROUP_USER_SET]; - } - constructor() { super(); @@ -138,8 +131,6 @@ class EntraM365GroupUserSetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.M365GROUP_USER_SET, commands.M365GROUP_USER_SET); - if (args.options.userName) { await this.warn(logger, `Option 'userName' is deprecated. Please use 'ids' or 'userNames' instead.`); } diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-add.spec.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-add.spec.ts index b59e1297026..fdc6fad32bc 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-add.spec.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-add.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './oauth2grant-add.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.OAUTH2GRANT_ADD, () => { let log: string[]; @@ -66,16 +65,6 @@ describe(commands.OAUTH2GRANT_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.OAUTH2GRANT_ADD]); - }); - it('adds OAuth2 permission grant (debug)', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/v1.0/oauth2PermissionGrants`) > -1) { diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-add.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-add.ts index b27b2d844a6..fd2bd08a12a 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-add.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-add.ts @@ -3,7 +3,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -25,10 +24,6 @@ class EntraOAuth2GrantAddCommand extends GraphCommand { return 'Grant the specified service principal OAuth2 permissions to the specified resource'; } - public alias(): string[] | undefined { - return [aadCommands.OAUTH2GRANT_ADD]; - } - constructor() { super(); @@ -67,8 +62,6 @@ class EntraOAuth2GrantAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.OAUTH2GRANT_ADD, commands.OAUTH2GRANT_ADD); - if (this.verbose) { await logger.logToStderr(`Granting the service principal specified permissions...`); } diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-list.spec.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-list.spec.ts index 35438b19bd1..bd7fbac799d 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-list.spec.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-list.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './oauth2grant-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.OAUTH2GRANT_LIST, () => { let log: string[]; @@ -64,16 +63,6 @@ describe(commands.OAUTH2GRANT_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.OAUTH2GRANT_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['objectId', 'resourceId', 'scope']); }); diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-list.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-list.ts index 72d44ed625e..3d0edbd7738 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-list.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-list.ts @@ -4,7 +4,6 @@ import { formatting } from '../../../../utils/formatting.js'; import { odata } from '../../../../utils/odata.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -24,10 +23,6 @@ class EntraOAuth2GrantListCommand extends GraphCommand { return 'Lists OAuth2 permission grants for the specified service principal'; } - public alias(): string[] | undefined { - return [aadCommands.OAUTH2GRANT_LIST]; - } - public defaultProperties(): string[] | undefined { return ['objectId', 'resourceId', 'scope']; } @@ -60,8 +55,6 @@ class EntraOAuth2GrantListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.OAUTH2GRANT_LIST, commands.OAUTH2GRANT_LIST); - if (this.verbose) { await logger.logToStderr(`Retrieving list of OAuth grants for the service principal...`); } diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-remove.spec.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-remove.spec.ts index 60bb2da88c4..d7756cfde97 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-remove.spec.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-remove.spec.ts @@ -11,7 +11,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './oauth2grant-remove.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.OAUTH2GRANT_REMOVE, () => { let log: string[]; @@ -71,16 +70,6 @@ describe(commands.OAUTH2GRANT_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.OAUTH2GRANT_REMOVE]); - }); - it('removes OAuth2 permission grant when prompt confirmed (debug)', async () => { const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/v1.0/oauth2PermissionGrants/YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek`) > -1) { diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-remove.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-remove.ts index b36db4bc7cd..567b8d53b9f 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-remove.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-remove.ts @@ -4,7 +4,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -24,10 +23,6 @@ class EntraOAuth2GrantRemoveCommand extends GraphCommand { return 'Remove specified service principal OAuth2 permissions'; } - public alias(): string[] | undefined { - return [aadCommands.OAUTH2GRANT_REMOVE]; - } - constructor() { super(); @@ -46,8 +41,6 @@ class EntraOAuth2GrantRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.OAUTH2GRANT_REMOVE, commands.OAUTH2GRANT_REMOVE); - const removeOauth2Grant: () => Promise = async (): Promise => { if (this.verbose) { await logger.logToStderr(`Removing OAuth2 permissions...`); diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-set.spec.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-set.spec.ts index 57f05a617a8..897c0c64a73 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-set.spec.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-set.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './oauth2grant-set.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.OAUTH2GRANT_SET, () => { let log: string[]; @@ -62,16 +61,6 @@ describe(commands.OAUTH2GRANT_SET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.OAUTH2GRANT_SET]); - }); - it('updates OAuth2 permission grant (debug)', async () => { sinon.stub(request, 'patch').callsFake(async (opts) => { if ((opts.url as string).indexOf(`/v1.0/oauth2PermissionGrants/YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek`) > -1) { diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-set.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-set.ts index f06fbb66a2d..14c27647eb7 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-set.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-set.ts @@ -3,7 +3,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -24,10 +23,6 @@ class EntraOAuth2GrantSetCommand extends GraphCommand { return 'Update OAuth2 permissions for the service principal'; } - public alias(): string[] | undefined { - return [aadCommands.OAUTH2GRANT_SET]; - } - constructor() { super(); @@ -46,8 +41,6 @@ class EntraOAuth2GrantSetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.OAUTH2GRANT_SET, commands.OAUTH2GRANT_SET); - if (this.verbose) { await logger.logToStderr(`Updating OAuth2 permissions...`); } diff --git a/src/m365/entra/commands/policy/policy-list.spec.ts b/src/m365/entra/commands/policy/policy-list.spec.ts index 9f9628d1350..b563b4e0657 100644 --- a/src/m365/entra/commands/policy/policy-list.spec.ts +++ b/src/m365/entra/commands/policy/policy-list.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './policy-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.POLICY_LIST, () => { let log: string[]; @@ -65,16 +64,6 @@ describe(commands.POLICY_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.POLICY_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'isOrganizationDefault']); }); diff --git a/src/m365/entra/commands/policy/policy-list.ts b/src/m365/entra/commands/policy/policy-list.ts index 138fb52a536..edd239f17f2 100644 --- a/src/m365/entra/commands/policy/policy-list.ts +++ b/src/m365/entra/commands/policy/policy-list.ts @@ -2,7 +2,6 @@ import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -34,10 +33,6 @@ class EntraPolicyListCommand extends GraphCommand { return 'Returns policies from Entra ID'; } - public alias(): string[] | undefined { - return [aadCommands.POLICY_LIST]; - } - constructor() { super(); @@ -83,8 +78,6 @@ class EntraPolicyListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.POLICY_LIST, commands.POLICY_LIST); - const policyType: string = args.options.type ? args.options.type.toLowerCase() : 'all'; try { diff --git a/src/m365/entra/commands/siteclassification/siteclassification-disable.spec.ts b/src/m365/entra/commands/siteclassification/siteclassification-disable.spec.ts index 61e16367733..39090557a71 100644 --- a/src/m365/entra/commands/siteclassification/siteclassification-disable.spec.ts +++ b/src/m365/entra/commands/siteclassification/siteclassification-disable.spec.ts @@ -11,7 +11,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './siteclassification-disable.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.SITECLASSIFICATION_DISABLE, () => { let log: string[]; @@ -69,16 +68,6 @@ describe(commands.SITECLASSIFICATION_DISABLE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.SITECLASSIFICATION_DISABLE]); - }); - it('prompts before disabling siteclassification when force option not passed', async () => { await command.action(logger, { options: {} }); diff --git a/src/m365/entra/commands/siteclassification/siteclassification-disable.ts b/src/m365/entra/commands/siteclassification/siteclassification-disable.ts index c2e5432ee34..b34fd79a5c5 100644 --- a/src/m365/entra/commands/siteclassification/siteclassification-disable.ts +++ b/src/m365/entra/commands/siteclassification/siteclassification-disable.ts @@ -5,7 +5,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -24,10 +23,6 @@ class EntraSiteClassificationDisableCommand extends GraphCommand { return 'Disables site classification'; } - public alias(): string[] | undefined { - return [aadCommands.SITECLASSIFICATION_DISABLE]; - } - constructor() { super(); @@ -52,8 +47,6 @@ class EntraSiteClassificationDisableCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.SITECLASSIFICATION_DISABLE, commands.SITECLASSIFICATION_DISABLE); - const disableSiteClassification = async (): Promise => { try { let requestOptions: CliRequestOptions = { diff --git a/src/m365/entra/commands/siteclassification/siteclassification-enable.spec.ts b/src/m365/entra/commands/siteclassification/siteclassification-enable.spec.ts index 9e4c640ba6e..66b385a2ad3 100644 --- a/src/m365/entra/commands/siteclassification/siteclassification-enable.spec.ts +++ b/src/m365/entra/commands/siteclassification/siteclassification-enable.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './siteclassification-enable.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.SITECLASSIFICATION_ENABLE, () => { let log: string[]; @@ -59,16 +58,6 @@ describe(commands.SITECLASSIFICATION_ENABLE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.SITECLASSIFICATION_ENABLE]); - }); - it('handles Microsoft 365 Tenant siteclassification missing DirectorySettingTemplate', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groupSettingTemplates`) { diff --git a/src/m365/entra/commands/siteclassification/siteclassification-enable.ts b/src/m365/entra/commands/siteclassification/siteclassification-enable.ts index 3847528c54c..69afbb7cd57 100644 --- a/src/m365/entra/commands/siteclassification/siteclassification-enable.ts +++ b/src/m365/entra/commands/siteclassification/siteclassification-enable.ts @@ -4,7 +4,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -26,10 +25,6 @@ class EntraSiteClassificationEnableCommand extends GraphCommand { return 'Enables site classification configuration'; } - public alias(): string[] | undefined { - return [aadCommands.SITECLASSIFICATION_ENABLE]; - } - constructor() { super(); @@ -64,8 +59,6 @@ class EntraSiteClassificationEnableCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.SITECLASSIFICATION_ENABLE, commands.SITECLASSIFICATION_ENABLE); - try { let requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/groupSettingTemplates`, diff --git a/src/m365/entra/commands/siteclassification/siteclassification-get.spec.ts b/src/m365/entra/commands/siteclassification/siteclassification-get.spec.ts index cdc4a5ad3ea..444b2bb304e 100644 --- a/src/m365/entra/commands/siteclassification/siteclassification-get.spec.ts +++ b/src/m365/entra/commands/siteclassification/siteclassification-get.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './siteclassification-get.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.SITECLASSIFICATION_GET, () => { let log: string[]; @@ -60,16 +59,6 @@ describe(commands.SITECLASSIFICATION_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.SITECLASSIFICATION_GET]); - }); - it('handles Microsoft 365 Tenant siteclassification is not enabled', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groupSettings`) { diff --git a/src/m365/entra/commands/siteclassification/siteclassification-get.ts b/src/m365/entra/commands/siteclassification/siteclassification-get.ts index dc3232003ce..ec2e5203799 100644 --- a/src/m365/entra/commands/siteclassification/siteclassification-get.ts +++ b/src/m365/entra/commands/siteclassification/siteclassification-get.ts @@ -4,7 +4,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { SiteClassificationSettings } from './SiteClassificationSettings.js'; -import aadCommands from '../../aadCommands.js'; class EntraSiteClassificationGetCommand extends GraphCommand { public get name(): string { @@ -15,13 +14,7 @@ class EntraSiteClassificationGetCommand extends GraphCommand { return 'Gets site classification configuration'; } - public alias(): string[] | undefined { - return [aadCommands.SITECLASSIFICATION_GET]; - } - public async commandAction(logger: Logger): Promise { - await this.showDeprecationWarning(logger, aadCommands.SITECLASSIFICATION_GET, commands.SITECLASSIFICATION_GET); - try { const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/groupSettings`, diff --git a/src/m365/entra/commands/siteclassification/siteclassification-set.spec.ts b/src/m365/entra/commands/siteclassification/siteclassification-set.spec.ts index 59b239434ce..231cec97a2e 100644 --- a/src/m365/entra/commands/siteclassification/siteclassification-set.spec.ts +++ b/src/m365/entra/commands/siteclassification/siteclassification-set.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './siteclassification-set.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.SITECLASSIFICATION_SET, () => { let log: string[]; @@ -63,16 +62,6 @@ describe(commands.SITECLASSIFICATION_SET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.SITECLASSIFICATION_SET]); - }); - it('fails validation if none of the options are specified', async () => { const actual = await command.validate({ options: { diff --git a/src/m365/entra/commands/siteclassification/siteclassification-set.ts b/src/m365/entra/commands/siteclassification/siteclassification-set.ts index 621405ba2d5..d0322a8df28 100644 --- a/src/m365/entra/commands/siteclassification/siteclassification-set.ts +++ b/src/m365/entra/commands/siteclassification/siteclassification-set.ts @@ -4,7 +4,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -26,10 +25,6 @@ class EntraSiteClassificationSetCommand extends GraphCommand { return 'Updates site classification configuration'; } - public alias(): string[] | undefined { - return [aadCommands.SITECLASSIFICATION_SET]; - } - constructor() { super(); @@ -81,8 +76,6 @@ class EntraSiteClassificationSetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.SITECLASSIFICATION_SET, commands.SITECLASSIFICATION_SET); - try { let requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/groupSettings`, diff --git a/src/m365/entra/commands/user/user-add.spec.ts b/src/m365/entra/commands/user/user-add.spec.ts index 80581d2ac93..ca1bd15add3 100644 --- a/src/m365/entra/commands/user/user-add.spec.ts +++ b/src/m365/entra/commands/user/user-add.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-add.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_ADD, () => { const graphBaseUrl = 'https://graph.microsoft.com/v1.0/users'; @@ -124,16 +123,6 @@ describe(commands.USER_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_ADD]); - }); - it('creates Microsoft Entra user using a preset password but requiring the user to change it the next login', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === graphBaseUrl) { diff --git a/src/m365/entra/commands/user/user-add.ts b/src/m365/entra/commands/user/user-add.ts index 3f6643e892c..2af1e60bd80 100644 --- a/src/m365/entra/commands/user/user-add.ts +++ b/src/m365/entra/commands/user/user-add.ts @@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface ExtendedUser extends User { password: string; @@ -44,10 +43,6 @@ class EntraUserAddCommand extends GraphCommand { return 'Creates a new user'; } - public alias(): string[] | undefined { - return [aadCommands.USER_ADD]; - } - public allowUnknownOptions(): boolean | undefined { return true; } @@ -206,8 +201,6 @@ class EntraUserAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_ADD, commands.USER_ADD); - if (this.verbose) { await logger.logToStderr(`Adding user to AAD with displayName ${args.options.displayName} and userPrincipalName ${args.options.userName}`); } diff --git a/src/m365/entra/commands/user/user-get.spec.ts b/src/m365/entra/commands/user/user-get.spec.ts index 39d1652524e..991de5d6de2 100644 --- a/src/m365/entra/commands/user/user-get.spec.ts +++ b/src/m365/entra/commands/user/user-get.spec.ts @@ -14,7 +14,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-get.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; import { entraUser } from '../../../../utils/entraUser.js'; import { formatting } from '../../../../utils/formatting.js'; @@ -86,16 +85,6 @@ describe(commands.USER_GET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_GET]); - }); - it('retrieves user using id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/users/${userId}`) { @@ -139,7 +128,7 @@ describe(commands.USER_GET, () => { it('retrieves user using user name', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userName) }`) { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userName)}`) { return resultValue; } @@ -185,7 +174,7 @@ describe(commands.USER_GET, () => { "mail": "john.doe@contoso.onmicrosoft.com" }; sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userName) }?$expand=manager($select=displayName,userPrincipalName,id,mail)`) { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userName)}?$expand=manager($select=displayName,userPrincipalName,id,mail)`) { return resultValueWithManger; } @@ -198,7 +187,7 @@ describe(commands.USER_GET, () => { it('retrieves user using @meusername token', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userName) }`) { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userName)}`) { return resultValue; } @@ -227,7 +216,7 @@ describe(commands.USER_GET, () => { it('retrieves only the specified properties', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userName) }?$select=id,mail`) { + if (opts.url === `https://graph.microsoft.com/v1.0/users/${formatting.encodeQueryParameter(userName)}?$select=id,mail`) { return { "id": "userId", "mail": null }; } @@ -276,7 +265,7 @@ describe(commands.USER_GET, () => { await assert.rejects(command.action(logger, { options: { userName: userName } } as any), new CommandError(`Resource '${userName}' does not exist or one of its queried reference-property objects are not present.`)); }); - + it('fails validation if id or email or userName options are not passed', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { diff --git a/src/m365/entra/commands/user/user-get.ts b/src/m365/entra/commands/user/user-get.ts index e022aeeedaa..f59a5258116 100644 --- a/src/m365/entra/commands/user/user-get.ts +++ b/src/m365/entra/commands/user/user-get.ts @@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; import { entraUser } from '../../../../utils/entraUser.js'; import { formatting } from '../../../../utils/formatting.js'; @@ -30,10 +29,6 @@ class EntraUserGetCommand extends GraphCommand { return 'Gets information about the specified user'; } - public alias(): string[] | undefined { - return [aadCommands.USER_GET]; - } - constructor() { super(); @@ -97,8 +92,6 @@ class EntraUserGetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_GET, commands.USER_GET); - try { let userIdOrPrincipalName = args.options.id; diff --git a/src/m365/entra/commands/user/user-guest-add.spec.ts b/src/m365/entra/commands/user/user-guest-add.spec.ts index 7be7b809be1..edee1366026 100644 --- a/src/m365/entra/commands/user/user-guest-add.spec.ts +++ b/src/m365/entra/commands/user/user-guest-add.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-guest-add.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_GUEST_ADD, () => { const emailAddress = 'john.doe@contoso.com'; @@ -86,16 +85,6 @@ describe(commands.USER_GUEST_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_GUEST_ADD]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'inviteRedeemUrl', 'invitedUserDisplayName', 'invitedUserEmailAddress', 'invitedUserType', 'resetRedemption', 'sendInvitationMessage', 'status']); }); diff --git a/src/m365/entra/commands/user/user-guest-add.ts b/src/m365/entra/commands/user/user-guest-add.ts index 320efed18a0..9f41a752c47 100644 --- a/src/m365/entra/commands/user/user-guest-add.ts +++ b/src/m365/entra/commands/user/user-guest-add.ts @@ -2,7 +2,6 @@ import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -28,10 +27,6 @@ class EntraUserGuestAddCommand extends GraphCommand { return 'Invite an external user to the organization'; } - public alias(): string[] | undefined { - return [aadCommands.USER_GUEST_ADD]; - } - constructor() { super(); @@ -83,8 +78,6 @@ class EntraUserGuestAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_GUEST_ADD, commands.USER_GUEST_ADD); - try { const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/invitations`, diff --git a/src/m365/entra/commands/user/user-hibp.spec.ts b/src/m365/entra/commands/user/user-hibp.spec.ts index 71fa429da01..62c0aef613e 100644 --- a/src/m365/entra/commands/user/user-hibp.spec.ts +++ b/src/m365/entra/commands/user/user-hibp.spec.ts @@ -13,7 +13,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-hibp.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_HIBP, () => { let log: string[]; @@ -63,16 +62,6 @@ describe(commands.USER_HIBP, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_HIBP]); - }); - it('fails validation if userName and apiKey is not specified', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { diff --git a/src/m365/entra/commands/user/user-hibp.ts b/src/m365/entra/commands/user/user-hibp.ts index 04e9567094a..70ed76b2710 100644 --- a/src/m365/entra/commands/user/user-hibp.ts +++ b/src/m365/entra/commands/user/user-hibp.ts @@ -4,7 +4,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import AnonymousCommand from '../../../base/AnonymousCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -26,10 +25,6 @@ class EntraUserHibpCommand extends AnonymousCommand { return 'Allows you to retrieve all accounts that have been pwned with the specified username'; } - public alias(): string[] | undefined { - return [aadCommands.USER_HIBP]; - } - constructor() { super(); @@ -73,8 +68,6 @@ class EntraUserHibpCommand extends AnonymousCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_HIBP, commands.USER_HIBP); - try { const requestOptions: CliRequestOptions = { url: `https://haveibeenpwned.com/api/v3/breachedaccount/${formatting.encodeQueryParameter(args.options.userName)}${(args.options.domain ? `?domain=${formatting.encodeQueryParameter(args.options.domain)}` : '')}`, diff --git a/src/m365/entra/commands/user/user-license-add.spec.ts b/src/m365/entra/commands/user/user-license-add.spec.ts index f50e59b2e62..a6648d63c90 100644 --- a/src/m365/entra/commands/user/user-license-add.spec.ts +++ b/src/m365/entra/commands/user/user-license-add.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-license-add.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_LICENSE_ADD, () => { let commandInfo: CommandInfo; @@ -83,16 +82,6 @@ describe(commands.USER_LICENSE_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_LICENSE_ADD]); - }); - it('fails validation if ids is not a valid guid.', async () => { const actual = await command.validate({ options: { diff --git a/src/m365/entra/commands/user/user-license-add.ts b/src/m365/entra/commands/user/user-license-add.ts index 8471cdca5ed..9a8ed9aecc7 100644 --- a/src/m365/entra/commands/user/user-license-add.ts +++ b/src/m365/entra/commands/user/user-license-add.ts @@ -3,7 +3,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -25,10 +24,6 @@ class EntraUserLicenseAddCommand extends GraphCommand { return 'Assigns a license to a user'; } - public alias(): string[] | undefined { - return [aadCommands.USER_LICENSE_ADD]; - } - constructor() { super(); @@ -84,8 +79,6 @@ class EntraUserLicenseAddCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_LICENSE_ADD, commands.USER_LICENSE_ADD); - const addLicenses = args.options.ids.split(',').map(x => { return { "disabledPlans": [], "skuId": x }; }); const requestBody = { "addLicenses": addLicenses, "removeLicenses": [] }; diff --git a/src/m365/entra/commands/user/user-license-list.spec.ts b/src/m365/entra/commands/user/user-license-list.spec.ts index 4d07ff4a961..6936039da9f 100644 --- a/src/m365/entra/commands/user/user-license-list.spec.ts +++ b/src/m365/entra/commands/user/user-license-list.spec.ts @@ -13,7 +13,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-license-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_LICENSE_LIST, () => { const userId = '59f80e08-24b1-41f8-8586-16765fd830d3'; @@ -103,16 +102,6 @@ describe(commands.USER_LICENSE_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_LICENSE_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'skuId', 'skuPartNumber']); }); diff --git a/src/m365/entra/commands/user/user-license-list.ts b/src/m365/entra/commands/user/user-license-list.ts index 2d3437c393e..bcb2a8d09de 100644 --- a/src/m365/entra/commands/user/user-license-list.ts +++ b/src/m365/entra/commands/user/user-license-list.ts @@ -6,7 +6,6 @@ import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import auth from '../../../../Auth.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -26,10 +25,6 @@ class EntraUserLicenseListCommand extends GraphCommand { return 'Lists the license details for a given user'; } - public alias(): string[] | undefined { - return [aadCommands.USER_LICENSE_LIST]; - } - public defaultProperties(): string[] | undefined { return ['id', 'skuId', 'skuPartNumber']; } @@ -88,8 +83,6 @@ class EntraUserLicenseListCommand extends GraphCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { const isAppOnlyAccessToken = accessToken.isAppOnlyAccessToken(auth.connection.accessTokens[this.resource].accessToken); - await this.showDeprecationWarning(logger, aadCommands.USER_LICENSE_LIST, commands.USER_LICENSE_LIST); - if (isAppOnlyAccessToken && !args.options.userId && !args.options.userName) { this.handleError(`Specify at least 'userId' or 'userName' when using application permissions.`); } diff --git a/src/m365/entra/commands/user/user-license-remove.spec.ts b/src/m365/entra/commands/user/user-license-remove.spec.ts index 09602532ef6..c82a1ebde6a 100644 --- a/src/m365/entra/commands/user/user-license-remove.spec.ts +++ b/src/m365/entra/commands/user/user-license-remove.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-license-remove.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_LICENSE_REMOVE, () => { let commandInfo: CommandInfo; @@ -77,16 +76,6 @@ describe(commands.USER_LICENSE_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_LICENSE_REMOVE]); - }); - it('fails validation if ids is not a valid guid.', async () => { const actual = await command.validate({ options: { diff --git a/src/m365/entra/commands/user/user-license-remove.ts b/src/m365/entra/commands/user/user-license-remove.ts index 4e03576eccd..9afcbfcb811 100644 --- a/src/m365/entra/commands/user/user-license-remove.ts +++ b/src/m365/entra/commands/user/user-license-remove.ts @@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import { cli } from '../../../../cli/cli.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -28,10 +27,6 @@ class EntraUserLicenseRemoveCommand extends GraphCommand { return 'Removes a license from a user'; } - public alias(): string[] | undefined { - return [aadCommands.USER_LICENSE_REMOVE]; - } - constructor() { super(); @@ -95,8 +90,6 @@ class EntraUserLicenseRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: any): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_LICENSE_REMOVE, commands.USER_LICENSE_REMOVE); - if (this.verbose) { await logger.logToStderr(`Removing the licenses for the user '${args.options.userId || args.options.userName}'...`); } diff --git a/src/m365/entra/commands/user/user-list.spec.ts b/src/m365/entra/commands/user/user-list.spec.ts index df51e8622d7..36120d661ba 100644 --- a/src/m365/entra/commands/user/user-list.spec.ts +++ b/src/m365/entra/commands/user/user-list.spec.ts @@ -13,7 +13,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_LIST, () => { let commandInfo: CommandInfo; @@ -65,16 +64,6 @@ describe(commands.USER_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_LIST]); - }); - it('fails validation if type is not a valid user type', async () => { const actual = await command.validate({ options: { type: 'invalid' } }, commandInfo); assert.notStrictEqual(actual, true); diff --git a/src/m365/entra/commands/user/user-list.ts b/src/m365/entra/commands/user/user-list.ts index 1edfd4b30ca..fdce96baafc 100644 --- a/src/m365/entra/commands/user/user-list.ts +++ b/src/m365/entra/commands/user/user-list.ts @@ -5,7 +5,6 @@ import { formatting } from '../../../../utils/formatting.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -27,10 +26,6 @@ class EntraUserListCommand extends GraphCommand { return 'Lists users matching specified criteria'; } - public alias(): string[] | undefined { - return [aadCommands.USER_LIST]; - } - public allowUnknownOptions(): boolean | undefined { return true; } @@ -86,8 +81,6 @@ class EntraUserListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_LIST, commands.USER_LIST); - try { let url = `${this.resource}/v1.0/users`; diff --git a/src/m365/entra/commands/user/user-password-validate.spec.ts b/src/m365/entra/commands/user/user-password-validate.spec.ts index 6e3facf5272..8834d5f6d43 100644 --- a/src/m365/entra/commands/user/user-password-validate.spec.ts +++ b/src/m365/entra/commands/user/user-password-validate.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-password-validate.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_PASSWORD_VALIDATE, () => { let log: string[]; @@ -61,16 +60,6 @@ describe(commands.USER_PASSWORD_VALIDATE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_PASSWORD_VALIDATE]); - }); - it('password is too short', async () => { sinon.stub(request, 'post').callsFake(async opts => { if (opts.url === 'https://graph.microsoft.com/beta/users/validatePassword' && diff --git a/src/m365/entra/commands/user/user-password-validate.ts b/src/m365/entra/commands/user/user-password-validate.ts index 30ea1a691ae..ed88f2d94b1 100644 --- a/src/m365/entra/commands/user/user-password-validate.ts +++ b/src/m365/entra/commands/user/user-password-validate.ts @@ -2,7 +2,6 @@ import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -22,10 +21,6 @@ class EntraUserPasswordValidateCommand extends GraphCommand { return "Check a user's password against the organization's password validation policy"; } - public alias(): string[] | undefined { - return [aadCommands.USER_PASSWORD_VALIDATE]; - } - constructor() { super(); @@ -41,8 +36,6 @@ class EntraUserPasswordValidateCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_PASSWORD_VALIDATE, commands.USER_PASSWORD_VALIDATE); - try { const requestOptions: CliRequestOptions = { url: `${this.resource}/beta/users/validatePassword`, diff --git a/src/m365/entra/commands/user/user-recyclebinitem-clear.spec.ts b/src/m365/entra/commands/user/user-recyclebinitem-clear.spec.ts index 5d52c4b76f3..9e5e59a2008 100644 --- a/src/m365/entra/commands/user/user-recyclebinitem-clear.spec.ts +++ b/src/m365/entra/commands/user/user-recyclebinitem-clear.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-recyclebinitem-clear.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_RECYCLEBINITEM_CLEAR, () => { let log: string[]; @@ -74,16 +73,6 @@ describe(commands.USER_RECYCLEBINITEM_CLEAR, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_RECYCLEBINITEM_CLEAR]); - }); - it('removes a single user when prompt confirmed', async () => { let amountOfBatches = 0; diff --git a/src/m365/entra/commands/user/user-recyclebinitem-clear.ts b/src/m365/entra/commands/user/user-recyclebinitem-clear.ts index 861fb46e7d4..91a6bc37006 100644 --- a/src/m365/entra/commands/user/user-recyclebinitem-clear.ts +++ b/src/m365/entra/commands/user/user-recyclebinitem-clear.ts @@ -6,7 +6,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -25,10 +24,6 @@ class EntraUserRecycleBinItemClearCommand extends GraphCommand { return 'Removes all users from the tenant recycle bin'; } - public alias(): string[] | undefined { - return [aadCommands.USER_RECYCLEBINITEM_CLEAR]; - } - constructor() { super(); @@ -53,8 +48,6 @@ class EntraUserRecycleBinItemClearCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_RECYCLEBINITEM_CLEAR, commands.USER_RECYCLEBINITEM_CLEAR); - const clearRecycleBinUsers = async (): Promise => { try { const users = await odata.getAllItems(`${this.resource}/v1.0/directory/deletedItems/microsoft.graph.user?$select=id`); diff --git a/src/m365/entra/commands/user/user-recyclebinitem-list.spec.ts b/src/m365/entra/commands/user/user-recyclebinitem-list.spec.ts index 7c68be3cb6b..a49b69cfdf5 100644 --- a/src/m365/entra/commands/user/user-recyclebinitem-list.spec.ts +++ b/src/m365/entra/commands/user/user-recyclebinitem-list.spec.ts @@ -10,7 +10,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-recyclebinitem-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_RECYCLEBINITEM_LIST, () => { const deletedUsersResponse = [{ "businessPhones": [], "displayName": "John Doe", "givenName": "John Doe", "jobTitle": "Developer", "mail": "john@contoso.com", "mobilePhone": "0476345130", "officeLocation": "Washington", "preferredLanguage": "nl-BE", "surname": "John", "userPrincipalName": "7e06b56615f340138bf879874d52e68ajohn@contoso.com", "id": "7e06b566-15f3-4013-8bf8-79874d52e68a" }]; @@ -63,16 +62,6 @@ describe(commands.USER_RECYCLEBINITEM_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_RECYCLEBINITEM_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'userPrincipalName']); }); diff --git a/src/m365/entra/commands/user/user-recyclebinitem-list.ts b/src/m365/entra/commands/user/user-recyclebinitem-list.ts index e43e6f910c2..a9da1950cac 100644 --- a/src/m365/entra/commands/user/user-recyclebinitem-list.ts +++ b/src/m365/entra/commands/user/user-recyclebinitem-list.ts @@ -3,7 +3,6 @@ import { Logger } from '../../../../cli/Logger.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; class EntraUserRecycleBinItemListCommand extends GraphCommand { public get name(): string { @@ -14,17 +13,11 @@ class EntraUserRecycleBinItemListCommand extends GraphCommand { return 'Lists users from the recycle bin in the current tenant'; } - public alias(): string[] | undefined { - return [aadCommands.USER_RECYCLEBINITEM_LIST]; - } - public defaultProperties(): string[] | undefined { return ['id', 'displayName', 'userPrincipalName']; } public async commandAction(logger: Logger): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_RECYCLEBINITEM_LIST, commands.USER_RECYCLEBINITEM_LIST); - if (this.verbose) { await logger.logToStderr('Retrieving users from the recycle bin...'); } diff --git a/src/m365/entra/commands/user/user-recyclebinitem-remove.spec.ts b/src/m365/entra/commands/user/user-recyclebinitem-remove.spec.ts index 5fb87f6542d..3c5725df198 100644 --- a/src/m365/entra/commands/user/user-recyclebinitem-remove.spec.ts +++ b/src/m365/entra/commands/user/user-recyclebinitem-remove.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-recyclebinitem-remove.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_RECYCLEBINITEM_REMOVE, () => { const validUserId = 'd839826a-81bf-4c38-8f80-f150d11ce6c7'; @@ -73,16 +72,6 @@ describe(commands.USER_RECYCLEBINITEM_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_RECYCLEBINITEM_REMOVE]); - }); - it('removes the user when prompt confirmed', async () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); diff --git a/src/m365/entra/commands/user/user-recyclebinitem-remove.ts b/src/m365/entra/commands/user/user-recyclebinitem-remove.ts index 56f8c39f213..191c3ec74f7 100644 --- a/src/m365/entra/commands/user/user-recyclebinitem-remove.ts +++ b/src/m365/entra/commands/user/user-recyclebinitem-remove.ts @@ -4,7 +4,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -25,10 +24,6 @@ class EntraUserRecycleBinItemRemoveCommand extends GraphCommand { return 'Removes a user from the recycle bin in the current tenant'; } - public alias(): string[] | undefined { - return [aadCommands.USER_RECYCLEBINITEM_REMOVE]; - } - constructor() { super(); @@ -69,8 +64,6 @@ class EntraUserRecycleBinItemRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_RECYCLEBINITEM_REMOVE, commands.USER_RECYCLEBINITEM_REMOVE); - const clearRecycleBinItem: () => Promise = async (): Promise => { if (this.verbose) { await logger.logToStderr(`Permanently deleting user with id ${args.options.id} from Microsoft Entra ID`); diff --git a/src/m365/entra/commands/user/user-recyclebinitem-restore.spec.ts b/src/m365/entra/commands/user/user-recyclebinitem-restore.spec.ts index 41acd7cda87..79a12f9b9d6 100644 --- a/src/m365/entra/commands/user/user-recyclebinitem-restore.spec.ts +++ b/src/m365/entra/commands/user/user-recyclebinitem-restore.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-recyclebinitem-restore.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_RECYCLEBINITEM_RESTORE, () => { const validUserId = 'd839826a-81bf-4c38-8f80-f150d11ce6c7'; @@ -82,16 +81,6 @@ describe(commands.USER_RECYCLEBINITEM_RESTORE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_RECYCLEBINITEM_RESTORE]); - }); - it('restores the user from the recycle bin', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/directory/deletedItems/${validUserId}/restore`) { diff --git a/src/m365/entra/commands/user/user-recyclebinitem-restore.ts b/src/m365/entra/commands/user/user-recyclebinitem-restore.ts index f66f0c16f05..52e392cfd98 100644 --- a/src/m365/entra/commands/user/user-recyclebinitem-restore.ts +++ b/src/m365/entra/commands/user/user-recyclebinitem-restore.ts @@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -24,10 +23,6 @@ class EntraUserRecycleBinItemRestoreCommand extends GraphCommand { return 'Restores a user from the tenant recycle bin'; } - public alias(): string[] | undefined { - return [aadCommands.USER_RECYCLEBINITEM_RESTORE]; - } - constructor() { super(); @@ -56,8 +51,6 @@ class EntraUserRecycleBinItemRestoreCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_RECYCLEBINITEM_RESTORE, commands.USER_RECYCLEBINITEM_RESTORE); - if (this.verbose) { await logger.logToStderr(`Restoring user with id ${args.options.id} from the recycle bin.`); } diff --git a/src/m365/entra/commands/user/user-registrationdetails-list.spec.ts b/src/m365/entra/commands/user/user-registrationdetails-list.spec.ts index b3c87ee1dcf..e5ee0099f85 100644 --- a/src/m365/entra/commands/user/user-registrationdetails-list.spec.ts +++ b/src/m365/entra/commands/user/user-registrationdetails-list.spec.ts @@ -2,7 +2,6 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; import request from '../../../../request.js'; import command from './user-registrationdetails-list.js'; import { telemetry } from '../../../../telemetry.js'; @@ -133,16 +132,6 @@ describe(commands.USER_REGISTRATIONDETAILS_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_REGISTRATIONDETAILS_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['userPrincipalName', 'methodsRegistered', 'lastUpdatedDateTime']); }); diff --git a/src/m365/entra/commands/user/user-registrationdetails-list.ts b/src/m365/entra/commands/user/user-registrationdetails-list.ts index 960ff6cf6bb..9d8b8507c1c 100644 --- a/src/m365/entra/commands/user/user-registrationdetails-list.ts +++ b/src/m365/entra/commands/user/user-registrationdetails-list.ts @@ -2,7 +2,6 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import { Logger } from '../../../../cli/Logger.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; import { odata } from '../../../../utils/odata.js'; import { UserRegistrationDetails } from '@microsoft/microsoft-graph-types'; import { entraUser } from '../../../../utils/entraUser.js'; @@ -42,10 +41,6 @@ class EntraUserRegistrationDetailsListCommand extends GraphCommand { return 'Retrieves a list of the authentication methods registered for users'; } - public alias(): string[] | undefined { - return [aadCommands.USER_REGISTRATIONDETAILS_LIST]; - } - public defaultProperties(): string[] | undefined { return ['userPrincipalName', 'methodsRegistered', 'lastUpdatedDateTime']; } diff --git a/src/m365/entra/commands/user/user-remove.spec.ts b/src/m365/entra/commands/user/user-remove.spec.ts index 3bbe35bcb3e..b18c879a3be 100644 --- a/src/m365/entra/commands/user/user-remove.spec.ts +++ b/src/m365/entra/commands/user/user-remove.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-remove.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_REMOVE, () => { let commandInfo: CommandInfo; @@ -75,16 +74,6 @@ describe(commands.USER_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_REMOVE]); - }); - it('fails validation if id is not a valid guid.', async () => { const actual = await command.validate({ options: { diff --git a/src/m365/entra/commands/user/user-remove.ts b/src/m365/entra/commands/user/user-remove.ts index 2279e334076..76025701f98 100644 --- a/src/m365/entra/commands/user/user-remove.ts +++ b/src/m365/entra/commands/user/user-remove.ts @@ -5,7 +5,6 @@ import request, { CliRequestOptions } from '../../../../request.js'; import { validation } from '../../../../utils/validation.js'; import { cli } from '../../../../cli/cli.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -27,10 +26,6 @@ class EntraUserRemoveCommand extends GraphCommand { return 'Removes a specific user'; } - public alias(): string[] | undefined { - return [aadCommands.USER_REMOVE]; - } - constructor() { super(); @@ -87,8 +82,6 @@ class EntraUserRemoveCommand extends GraphCommand { } public async commandAction(logger: Logger, args: any): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_REMOVE, commands.USER_REMOVE); - if (this.verbose) { await logger.logToStderr(`Removing user '${args.options.id || args.options.userName}'...`); } diff --git a/src/m365/entra/commands/user/user-set.spec.ts b/src/m365/entra/commands/user/user-set.spec.ts index 12c30875988..3da3de2bf43 100644 --- a/src/m365/entra/commands/user/user-set.spec.ts +++ b/src/m365/entra/commands/user/user-set.spec.ts @@ -15,7 +15,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-set.js'; import { settingsNames } from '../../../../settingsNames.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_SET, () => { const currentPassword = '9%9OLUg6p@Ra'; @@ -95,16 +94,6 @@ describe(commands.USER_SET, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_SET]); - }); - it('fails validation if neither the id nor the userName are specified', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { diff --git a/src/m365/entra/commands/user/user-set.ts b/src/m365/entra/commands/user/user-set.ts index 84cf11eadfe..677261a7477 100644 --- a/src/m365/entra/commands/user/user-set.ts +++ b/src/m365/entra/commands/user/user-set.ts @@ -6,7 +6,6 @@ import { accessToken } from '../../../../utils/accessToken.js'; import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; -import aadCommands from '../../aadCommands.js'; import commands from '../../commands.js'; interface CommandArgs { @@ -45,10 +44,6 @@ class EntraUserSetCommand extends GraphCommand { return 'Updates information about the specified user'; } - public alias(): string[] | undefined { - return [aadCommands.USER_SET]; - } - public allowUnknownOptions(): boolean | undefined { return true; } @@ -249,8 +244,6 @@ class EntraUserSetCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_SET, commands.USER_SET); - try { if (args.options.currentPassword) { if (args.options.id && args.options.id !== accessToken.getUserIdFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken)) { diff --git a/src/m365/entra/commands/user/user-signin-list.spec.ts b/src/m365/entra/commands/user/user-signin-list.spec.ts index 0515ced18e8..632be3d30cc 100644 --- a/src/m365/entra/commands/user/user-signin-list.spec.ts +++ b/src/m365/entra/commands/user/user-signin-list.spec.ts @@ -12,7 +12,6 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-signin-list.js'; -import aadCommands from '../../aadCommands.js'; describe(commands.USER_SIGNIN_LIST, () => { let log: string[]; @@ -192,16 +191,6 @@ describe(commands.USER_SIGNIN_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('defines alias', () => { - const alias = command.alias(); - assert.notStrictEqual(typeof alias, 'undefined'); - }); - - it('defines correct alias', () => { - const alias = command.alias(); - assert.deepStrictEqual(alias, [aadCommands.USER_SIGNIN_LIST]); - }); - it('defines correct properties for the default output', () => { assert.deepStrictEqual(command.defaultProperties(), ['id', 'userPrincipalName', 'appId', 'appDisplayName', 'createdDateTime']); }); diff --git a/src/m365/entra/commands/user/user-signin-list.ts b/src/m365/entra/commands/user/user-signin-list.ts index f34dd17cace..afa9ee5eab8 100644 --- a/src/m365/entra/commands/user/user-signin-list.ts +++ b/src/m365/entra/commands/user/user-signin-list.ts @@ -6,7 +6,6 @@ import { odata } from '../../../../utils/odata.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import aadCommands from '../../aadCommands.js'; interface CommandArgs { options: Options; @@ -28,10 +27,6 @@ class EntraUserSigninListCommand extends GraphCommand { return 'Retrieves the Entra ID user sign-ins for the tenant'; } - public alias(): string[] | undefined { - return [aadCommands.USER_SIGNIN_LIST]; - } - constructor() { super(); @@ -101,8 +96,6 @@ class EntraUserSigninListCommand extends GraphCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - await this.showDeprecationWarning(logger, aadCommands.USER_SIGNIN_LIST, commands.USER_SIGNIN_LIST); - try { let endpoint: string = `${this.resource}/v1.0/auditLogs/signIns`; let filter: string = ""; diff --git a/src/m365/spo/commands/group/group-member-add.spec.ts b/src/m365/spo/commands/group/group-member-add.spec.ts index 03b31d42482..c264f73d894 100644 --- a/src/m365/spo/commands/group/group-member-add.spec.ts +++ b/src/m365/spo/commands/group/group-member-add.spec.ts @@ -6,6 +6,7 @@ import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { Logger } from '../../../../cli/Logger.js'; import request from '../../../../request.js'; import { telemetry } from '../../../../telemetry.js'; +import { formatting } from '../../../../utils/formatting.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; @@ -163,7 +164,7 @@ describe(commands.GROUP_MEMBER_ADD, () => { //#region Option values const webUrl = 'https://contoso.sharepoint.com/sites/Marketing'; - const spGroupName = 'Marketing Site Owners'; + const spGroupName = "Contoso Site Owners"; const spGroupId = 3; const spUserIds = userResponses.map(u => u.Id); const userNames = userResponses.map(u => u.UserPrincipalName); @@ -231,7 +232,7 @@ describe(commands.GROUP_MEMBER_ADD, () => { options: { webUrl: "https://contoso.sharepoint.com/sites/SiteA", groupId: 32, - groupName: "Contoso Site Owners", + groupName: spGroupName, userNames: "Alex.Wilber@contoso.com" } }, commandInfo); @@ -284,18 +285,6 @@ describe(commands.GROUP_MEMBER_ADD, () => { assert.notStrictEqual(actual, true); }); - it('fails validation if both emails and aadGroupIds options are passed', async () => { - const actual = await command.validate({ - options: { - webUrl: "https://contoso.sharepoint.com/sites/SiteA", - groupId: 32, - emails: "Alex.Wilber@contoso.com", - aadGroupIds: "56ca9023-3449-4e98-a96a-69e81a6f4983" - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - it('fails validation if both userIds and entraGroupNames options are passed', async () => { const actual = await command.validate({ options: { @@ -308,18 +297,6 @@ describe(commands.GROUP_MEMBER_ADD, () => { assert.notStrictEqual(actual, true); }); - it('fails validation if both userIds and aadGroupNames options are passed', async () => { - const actual = await command.validate({ - options: { - webUrl: "https://contoso.sharepoint.com/sites/SiteA", - groupId: 32, - userIds: 5, - aadGroupNames: "Microsoft Entra Group name" - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - it('fails validation if both userIds and emails options are passed', async () => { const actual = await command.validate({ options: { @@ -332,7 +309,7 @@ describe(commands.GROUP_MEMBER_ADD, () => { assert.notStrictEqual(actual, true); }); - it('fails validation if userNames, emails, userIds, entraGroupIds, aadGroupIds, entraGroupNames, or aadGroupNames options are not passed', async () => { + it('fails validation if userNames, emails, userIds, entraGroupIds, or entraGroupNames options are not passed', async () => { const actual = await command.validate({ options: { webUrl: "https://contoso.sharepoint.com/sites/SiteA", @@ -372,11 +349,6 @@ describe(commands.GROUP_MEMBER_ADD, () => { assert.notStrictEqual(actual, true); }); - it('fails validation if aadGroupIds is Invalid', async () => { - const actual = await command.validate({ options: { webUrl: "https://contoso.sharepoint.com/sites/SiteA", groupId: 32, aadGroupIds: "56ca9023-3449-4e98-a96a-69e81a6f4983,9" } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - it('passes validation if all the required options are specified', async () => { const actual = await command.validate({ options: { webUrl: "https://contoso.sharepoint.com/sites/SiteA", groupId: 32, userNames: "Alex.Wilber@contoso.com" } }, commandInfo); assert.strictEqual(actual, true); @@ -386,28 +358,6 @@ describe(commands.GROUP_MEMBER_ADD, () => { assert.deepStrictEqual(command.defaultProperties(), ['Title', 'UserPrincipalName']); }); - it('correctly logs deprecation warning for aadGroupIds option', async () => { - const chalk = (await import('chalk')).default; - const loggerErrSpy = sinon.spy(logger, 'logToStderr'); - sinon.stub(request, 'post').resolves(); - - await command.action(logger, { options: { webUrl: webUrl, groupName: spGroupName, aadGroupIds: entraGroupIds[0] } }); - assert.deepStrictEqual(loggerErrSpy.firstCall.firstArg, chalk.yellow(`Option 'aadGroupIds' is deprecated. Please use 'entraGroupIds' instead.`)); - - sinonUtil.restore(loggerErrSpy); - }); - - it('correctly logs deprecation warning for aadGroupNames option', async () => { - const chalk = (await import('chalk')).default; - const loggerErrSpy = sinon.spy(logger, 'logToStderr'); - sinon.stub(request, 'post').resolves(); - - await command.action(logger, { options: { webUrl: webUrl, groupName: spGroupName, aadGroupNames: entraGroupNames[0] } }); - assert.deepStrictEqual(loggerErrSpy.firstCall.firstArg, chalk.yellow(`Option 'aadGroupNames' is deprecated. Please use 'entraGroupNames' instead.`)); - - sinonUtil.restore(loggerErrSpy); - }); - it('correctly logs result when adding users by userNames', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === `${webUrl}/_api/web/SiteGroups/GetById(${spGroupId})/users`) { @@ -428,27 +378,6 @@ describe(commands.GROUP_MEMBER_ADD, () => { assert(loggerLogSpy.calledOnceWithExactly(userResponses)); }); - it('correctly adds users to group by UPNs', async () => { - const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { - if (opts.url === `${webUrl}/_api/web/SiteGroups/GetById(${spGroupId})/users`) { - return userResponses[postStub.callCount - 1]; - } - - throw 'Invalid request: ' + opts.url; - }); - - await command.action(logger, { - options: { - webUrl: webUrl, - groupId: spGroupId, - userNames: userNames.join(',') - } - }); - - assert.deepStrictEqual(postStub.firstCall.args[0].data, { LoginName: 'i:0#.f|membership|adelev@contoso.onmicrosoft.com' }); - assert.deepStrictEqual(postStub.secondCall.args[0].data, { LoginName: 'i:0#.f|membership|johnd@contoso.onmicrosoft.com' }); - }); - it('correctly adds users to group by emails', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === `${webUrl}/_api/web/SiteGroups/GetById(${spGroupId})/users`) { @@ -544,6 +473,28 @@ describe(commands.GROUP_MEMBER_ADD, () => { assert.deepStrictEqual(postStub.secondCall.args[0].data, { LoginName: `c:0t.c|tenant|${entraGroupResponses[1].id}` }); }); + it('correctly adds user to a group by groupName and emails (DEBUG)', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `${webUrl}/_api/web/SiteGroups/GetByName('${formatting.encodeQueryParameter(spGroupName)}')/users`) { + return groupResponses[postStub.callCount - 1]; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + debug: true, + webUrl: webUrl, + groupName: spGroupName, + emails: emails.join(',') + } + }); + + assert.deepStrictEqual(postStub.firstCall.args[0].data, { LoginName: 'i:0#.f|membership|Adele.Vance@contoso.onmicrosoft.com' }); + assert.deepStrictEqual(postStub.secondCall.args[0].data, { LoginName: 'i:0#.f|membership|John.Doe@contoso.onmicrosoft.com' }); + }); + it('correctly adds users to group by entraGroupNames', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === `${webUrl}/_api/web/SiteGroups/GetById(${spGroupId})/users`) { @@ -584,4 +535,4 @@ describe(commands.GROUP_MEMBER_ADD, () => { await assert.rejects(command.action(logger, { options: { webUrl: webUrl, groupId: spGroupId, userNames: userNames.join(',') } }), new CommandError(error.error['odata.error'].message.value)); }); -}); +}); \ No newline at end of file diff --git a/src/m365/spo/commands/group/group-member-add.ts b/src/m365/spo/commands/group/group-member-add.ts index 52779256bd9..fabe2e128ab 100644 --- a/src/m365/spo/commands/group/group-member-add.ts +++ b/src/m365/spo/commands/group/group-member-add.ts @@ -20,9 +20,7 @@ interface Options extends GlobalOptions { emails?: string; userIds?: string; entraGroupIds?: string; - aadGroupIds?: string; entraGroupNames?: string; - aadGroupNames?: string; } class SpoGroupMemberAddCommand extends SpoCommand { @@ -57,9 +55,7 @@ class SpoGroupMemberAddCommand extends SpoCommand { emails: typeof args.options.emails !== 'undefined', userIds: typeof args.options.userIds !== 'undefined', entraGroupIds: typeof args.options.entraGroupIds !== 'undefined', - aadGroupIds: typeof args.options.aadGroupIds !== 'undefined', - entraGroupNames: typeof args.options.entraGroupNames !== 'undefined', - aadGroupNames: typeof args.options.aadGroupNames !== 'undefined' + entraGroupNames: typeof args.options.entraGroupNames !== 'undefined' }); }); } @@ -87,14 +83,8 @@ class SpoGroupMemberAddCommand extends SpoCommand { { option: '--entraGroupIds [entraGroupIds]' }, - { - option: '--aadGroupIds [aadGroupIds]' - }, { option: '--entraGroupNames [entraGroupNames]' - }, - { - option: '--aadGroupNames [aadGroupNames]' } ); } @@ -139,13 +129,6 @@ class SpoGroupMemberAddCommand extends SpoCommand { } } - if (args.options.aadGroupIds) { - const isValidArray = validation.isValidGuidArray(args.options.aadGroupIds); - if (isValidArray !== true) { - return `Option 'aadGroupIds' contains one or more invalid GUIDs: ${isValidArray}.`; - } - } - return true; } ); @@ -154,28 +137,16 @@ class SpoGroupMemberAddCommand extends SpoCommand { #initOptionSets(): void { this.optionSets.push( { options: ['groupId', 'groupName'] }, - { options: ['userNames', 'emails', 'userIds', 'entraGroupIds', 'aadGroupIds', 'entraGroupNames', 'aadGroupNames'] } + { options: ['userNames', 'emails', 'userIds', 'entraGroupIds', 'entraGroupNames'] } ); } #initTypes(): void { - this.types.string.push('webUrl', 'groupName', 'userNames', 'emails', 'userIds', 'entraGroupIds', 'aadGroupIds', 'entraGroupNames', 'aadGroupNames'); + this.types.string.push('webUrl', 'groupName', 'userNames', 'emails', 'userIds', 'entraGroupIds', 'entraGroupNames'); } public async commandAction(logger: Logger, args: CommandArgs): Promise { try { - if (args.options.aadGroupIds) { - args.options.entraGroupIds = args.options.aadGroupIds; - - await this.warn(logger, `Option 'aadGroupIds' is deprecated. Please use 'entraGroupIds' instead.`); - } - - if (args.options.aadGroupNames) { - args.options.entraGroupNames = args.options.aadGroupNames; - - await this.warn(logger, `Option 'aadGroupNames' is deprecated. Please use 'entraGroupNames' instead.`); - } - const loginNames = await this.getLoginNames(logger, args.options); let apiUrl = `${args.options.webUrl}/_api/web/SiteGroups`; diff --git a/src/m365/spo/commands/group/group-member-remove.spec.ts b/src/m365/spo/commands/group/group-member-remove.spec.ts index 84ecd5fa54c..303e4d65037 100644 --- a/src/m365/spo/commands/group/group-member-remove.spec.ts +++ b/src/m365/spo/commands/group/group-member-remove.spec.ts @@ -157,7 +157,7 @@ describe(commands.GROUP_MEMBER_REMOVE, () => { debug: true, webUrl: "https://contoso.sharepoint.com/sites/SiteA", groupName: "Site A Visitors", - aadGroupName: "Microsoft Entra Security Group" + entraGroupName: "Microsoft Entra Security Group" } }); @@ -217,7 +217,7 @@ describe(commands.GROUP_MEMBER_REMOVE, () => { debug: true, webUrl: "https://contoso.sharepoint.com/sites/SiteA", groupName: "Site A Visitors", - aadGroupId: "5786b8e8-c495-4734-b345-756733960730", + entraGroupId: "5786b8e8-c495-4734-b345-756733960730", force: true } }); @@ -710,15 +710,4 @@ describe(commands.GROUP_MEMBER_REMOVE, () => { }, commandInfo); assert.notStrictEqual(actual, true); }); - - it('fails validation if aadGroupId is not a valid guid.', async () => { - const actual = await command.validate({ - options: { - webUrl: "https://contoso.sharepoint.com/sites/SiteA", - groupId: 3, - aadGroupId: 'Invalid GUID' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); }); \ No newline at end of file diff --git a/src/m365/spo/commands/group/group-member-remove.ts b/src/m365/spo/commands/group/group-member-remove.ts index 4ea58a7bc06..420a5b09d83 100644 --- a/src/m365/spo/commands/group/group-member-remove.ts +++ b/src/m365/spo/commands/group/group-member-remove.ts @@ -22,9 +22,7 @@ interface Options extends GlobalOptions { email?: string; userId?: number; entraGroupId?: string; - aadGroupId?: string; entraGroupName?: string; - aadGroupName?: string; force?: boolean; } @@ -55,9 +53,7 @@ class SpoGroupMemberRemoveCommand extends SpoCommand { email: (!(!args.options.email)).toString(), userId: (!(!args.options.userId)).toString(), entraGroupId: (!(!args.options.entraGroupId)).toString(), - aadGroupId: (!(!args.options.groupId)).toString(), entraGroupName: (!(!args.options.entraGroupName)).toString(), - aadGroupName: (!(!args.options.aadGroupName)).toString(), force: (!(!args.options.force)).toString() }); }); @@ -86,15 +82,9 @@ class SpoGroupMemberRemoveCommand extends SpoCommand { { option: '--entraGroupId [entraGroupId]' }, - { - option: '--aadGroupId [aadGroupId]' - }, { option: '--entraGroupName [entraGroupName]' }, - { - option: '--aadGroupName [aadGroupName]' - }, { option: '-f, --force' } @@ -124,10 +114,6 @@ class SpoGroupMemberRemoveCommand extends SpoCommand { return `${args.options.entraGroupId} is not a valid GUID`; } - if (args.options.aadGroupId && !validation.isValidGuid(args.options.aadGroupId as string)) { - return `${args.options.aadGroupId} is not a valid GUID`; - } - return validation.isValidSharePointUrl(args.options.webUrl); } ); @@ -136,7 +122,7 @@ class SpoGroupMemberRemoveCommand extends SpoCommand { #initOptionSets(): void { this.optionSets.push( { options: ['groupName', 'groupId'] }, - { options: ['userName', 'email', 'userId', 'entraGroupId', 'aadGroupId', 'entraGroupName', 'aadGroupName'] } + { options: ['userName', 'email', 'userId', 'entraGroupId', 'entraGroupName'] } ); } @@ -162,18 +148,6 @@ class SpoGroupMemberRemoveCommand extends SpoCommand { } public async commandAction(logger: Logger, args: CommandArgs): Promise { - if (args.options.aadGroupId) { - args.options.entraGroupId = args.options.aadGroupId; - - await this.warn(logger, `Option 'aadGroupId' is deprecated. Please use 'entraGroupId' instead`); - } - - if (args.options.aadGroupName) { - args.options.entraGroupName = args.options.aadGroupName; - - await this.warn(logger, `Option 'aadGroupName' is deprecated. Please use 'entraGroupName' instead`); - } - if (args.options.force) { if (this.debug) { await logger.logToStderr('Confirmation bypassed by entering confirm option. Removing the user from SharePoint Group...'); diff --git a/src/m365/spo/commands/user/user-ensure.spec.ts b/src/m365/spo/commands/user/user-ensure.spec.ts index c9f99d0ded8..9e8382f0723 100644 --- a/src/m365/spo/commands/user/user-ensure.spec.ts +++ b/src/m365/spo/commands/user/user-ensure.spec.ts @@ -116,23 +116,6 @@ describe(commands.USER_ENSURE, () => { assert(loggerLogSpy.calledWith(ensuredUserResponse)); }); - it('ensures user for a specific web by aadId', async () => { - sinon.stub(entraUser, 'getUpnByUserId').callsFake(async () => { - return validUserName; - }); - - sinon.stub(request, 'post').callsFake(async (opts) => { - if (opts.url === `${validWebUrl}/_api/web/ensureuser`) { - return ensuredUserResponse; - } - - throw 'Invalid request'; - }); - - await command.action(logger, { options: { verbose: true, webUrl: validWebUrl, aadId: validEntraId } }); - assert(loggerLogSpy.calledWith(ensuredUserResponse)); - }); - it('throws error message when no user was found with a specific id', async () => { sinon.stub(entraUser, 'getUpnByUserId').callsFake(async (id) => { throw { @@ -186,11 +169,6 @@ describe(commands.USER_ENSURE, () => { assert.notStrictEqual(actual, true); }); - it('fails validation if aadId is not a valid id', async () => { - const actual = await command.validate({ options: { webUrl: validWebUrl, aadId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - it('fails validation if userName is not a valid user principal name', async () => { const actual = await command.validate({ options: { webUrl: validWebUrl, userName: 'invalid' } }, commandInfo); assert.notStrictEqual(actual, true); @@ -201,11 +179,6 @@ describe(commands.USER_ENSURE, () => { assert.strictEqual(actual, true); }); - it('passes validation if the url is valid and aadId is a valid id', async () => { - const actual = await command.validate({ options: { webUrl: validWebUrl, aadId: validEntraId } }, commandInfo); - assert.strictEqual(actual, true); - }); - it('passes validation if the url is valid and userName is a valid user principal name', async () => { const actual = await command.validate({ options: { webUrl: validWebUrl, userName: validUserName } }, commandInfo); assert.strictEqual(actual, true); diff --git a/src/m365/spo/commands/user/user-ensure.ts b/src/m365/spo/commands/user/user-ensure.ts index 7a624851303..c4f681d15aa 100644 --- a/src/m365/spo/commands/user/user-ensure.ts +++ b/src/m365/spo/commands/user/user-ensure.ts @@ -13,7 +13,6 @@ interface CommandArgs { interface Options extends GlobalOptions { webUrl: string; entraId?: string; - aadId?: string; userName?: string; } @@ -39,7 +38,6 @@ class SpoUserEnsureCommand extends SpoCommand { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { entraId: typeof args.options.entraId !== 'undefined', - aadId: typeof args.options.aadId !== 'undefined', userName: typeof args.options.userName !== 'undefined' }); }); @@ -53,9 +51,6 @@ class SpoUserEnsureCommand extends SpoCommand { { option: '--entraId [entraId]' }, - { - option: '--aadId [aadId]' - }, { option: '--userName [userName]' } @@ -74,10 +69,6 @@ class SpoUserEnsureCommand extends SpoCommand { return `${args.options.entraId} is not a valid GUID.`; } - if (args.options.aadId && !validation.isValidGuid(args.options.aadId)) { - return `${args.options.aadId} is not a valid GUID.`; - } - if (args.options.userName && !validation.isValidUserPrincipalName(args.options.userName)) { return `${args.options.userName} is not a valid userName.`; } @@ -88,16 +79,10 @@ class SpoUserEnsureCommand extends SpoCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['entraId', 'aadId', 'userName'] }); + this.optionSets.push({ options: ['entraId', 'userName'] }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { - if (args.options.aadId) { - args.options.entraId = args.options.aadId; - - await this.warn(logger, `Option 'aadId' is deprecated. Please use 'entraId' instead`); - } - if (this.verbose) { await logger.logToStderr(`Ensuring user ${args.options.entraId || args.options.userName} at site ${args.options.webUrl}`); } From a667411fd8c9ed69e4f2a9c8a40d287704941a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3jcik?= Date: Sun, 13 Oct 2024 22:30:21 +0200 Subject: [PATCH 26/43] Updates release notes and upgrade guidance --- docs/docs/about/release-notes.mdx | 7 ++++++- docs/docs/v10-upgrade-guidance.mdx | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/docs/about/release-notes.mdx b/docs/docs/about/release-notes.mdx index b03f873721c..5f65c7d2e22 100644 --- a/docs/docs/about/release-notes.mdx +++ b/docs/docs/about/release-notes.mdx @@ -10,6 +10,10 @@ sidebar_position: 3 - extended `m365 login` with `--ensure` [#5217](https://github.com/pnp/cli-microsoft365/issues/5217) - updated docs thumbnail [#6302](https://github.com/pnp/cli-microsoft365/issues/6302) +- fixed casing and autocomplete for enums based on zod +- updated docs contribution guide with Zod [#6322](https://github.com/pnp/cli-microsoft365/issues/6322) +- removed obsolete example [#6272](https://github.com/pnp/cli-microsoft365/issues/6272) +- fixed 'm365 setup' app registration to use 'CLI for M365' [#6367](https://github.com/pnp/cli-microsoft365/issues/6367) ### ⚠️ Breaking changes @@ -40,7 +44,8 @@ sidebar_position: 3 - updated [cli doctor](../cmd/cli/cli-doctor.mdx) command output [#5923](https://github.com/pnp/cli-microsoft365/issues/5923) - removed duplicate properties from [teams tab list](../cmd/teams/tab/tab-list.mdx) command [#5900](https://github.com/pnp/cli-microsoft365/issues/5900) - renamed `entra group user ` to `entra group member `. [#6396](https://github.com/pnp/cli-microsoft365/issues/6396) -- Updates setting users in `entra m365group set` [#6061](https://github.com/pnp/cli-microsoft365/issues/6061) +- updated setting users in `entra m365group set` [#6061](https://github.com/pnp/cli-microsoft365/issues/6061) +- removed aad options and aliasses [#5823](https://github.com/pnp/cli-microsoft365/issues/5823), [#5676](https://github.com/pnp/cli-microsoft365/issues/5676) ## [v9.1.0](https://github.com/pnp/cli-microsoft365/releases/tag/v9.1.0) diff --git a/docs/docs/v10-upgrade-guidance.mdx b/docs/docs/v10-upgrade-guidance.mdx index 59f22898129..5874f182cc5 100644 --- a/docs/docs/v10-upgrade-guidance.mdx +++ b/docs/docs/v10-upgrade-guidance.mdx @@ -141,6 +141,14 @@ We have renamed the `entra group user` commands to `entra group member` to bette Please update your scripts to use the new command names. +### Removed `aad` options and aliasses to `aad` commands. + +As part of the renaming of Azure AD to Microsoft Entra ID, we have removed the `aad` options and aliases. From now on only `entra` commands are available. + +#### What action do I need to take? + +Please, check if your scripts use any of the `aad` commands and if so update it to `entra` commands. + ## SharePoint ### Updated `spo site appcatalog remove` options From f283f19955b53d69cd0518c92e71ef3dc40efbd5 Mon Sep 17 00:00:00 2001 From: Milan Holemans <11723921+milanholemans@users.noreply.github.com> Date: Sun, 29 Sep 2024 01:35:32 +0200 Subject: [PATCH 27/43] Fixes 'spo site sharingpermission set' example. Closes #6397 --- docs/docs/cmd/spo/site/site-sharingpermission-set.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/cmd/spo/site/site-sharingpermission-set.mdx b/docs/docs/cmd/spo/site/site-sharingpermission-set.mdx index f7670f0a819..05cb8716975 100644 --- a/docs/docs/cmd/spo/site/site-sharingpermission-set.mdx +++ b/docs/docs/cmd/spo/site/site-sharingpermission-set.mdx @@ -46,7 +46,7 @@ m365 spo site sharingpermission set --siteUrl https://siteaddress.com/sites/site Update the sharing permissions for a site where so owners can share the site, but members can only share files ```sh -m365 spo site sharingpermission set --siteUrl https://siteaddress.com/sites/sitename --capability full +m365 spo site sharingpermission set --siteUrl https://siteaddress.com/sites/sitename --capability limited ``` ## Response From 5fbec5481dcd1689adc85254fe753191b0635f04 Mon Sep 17 00:00:00 2001 From: Milan Holemans <11723921+milanholemans@users.noreply.github.com> Date: Fri, 27 Sep 2024 00:22:38 +0200 Subject: [PATCH 28/43] Enhances 'spo folder move' with new endpoint. Closes #6154 --- docs/docs/cmd/spo/file/file-move.mdx | 4 +- docs/docs/cmd/spo/folder/folder-move.mdx | 97 +++- docs/docs/v10-upgrade-guidance.mdx | 17 + src/m365/spo/commands/file/file-copy.spec.ts | 12 +- src/m365/spo/commands/file/file-copy.ts | 14 +- src/m365/spo/commands/file/file-move.spec.ts | 16 +- src/m365/spo/commands/file/file-move.ts | 14 +- .../spo/commands/folder/folder-move.spec.ts | 441 +++++++++++++----- src/m365/spo/commands/folder/folder-move.ts | 92 ++-- src/utils/spo.spec.ts | 211 +++++++-- src/utils/spo.ts | 88 +++- src/utils/timersUtil.ts | 3 +- 12 files changed, 737 insertions(+), 272 deletions(-) diff --git a/docs/docs/cmd/spo/file/file-move.mdx b/docs/docs/cmd/spo/file/file-move.mdx index a3470fa73bf..ff3f5ed7e7e 100644 --- a/docs/docs/cmd/spo/file/file-move.mdx +++ b/docs/docs/cmd/spo/file/file-move.mdx @@ -40,15 +40,13 @@ m365 spo file move [options] : This indicates whether a file with a share lock can still be moved. Use this option to move a file that is locked. `--skipWait` -: Don't wait for the copy operation to complete. +: Don't wait for the move operation to complete. ``` ## Remarks -All file versions are retained while moving a file. - When you specify a value for `nameConflictBehavior`, consider the following: - `fail` will throw an error when the destination file already exists. - `replace` will replace the destination file if it already exists. diff --git a/docs/docs/cmd/spo/folder/folder-move.mdx b/docs/docs/cmd/spo/folder/folder-move.mdx index dbd077356e7..4e6f7083301 100644 --- a/docs/docs/cmd/spo/folder/folder-move.mdx +++ b/docs/docs/cmd/spo/folder/folder-move.mdx @@ -1,4 +1,6 @@ import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; # spo folder move @@ -31,19 +33,14 @@ m365 spo folder move [options] `--nameConflictBehavior [nameConflictBehavior]` : Behavior when a file or folder with the same name is already present at the destination. Allowed values: `fail`, `rename`. Defaults to `fail`. -`--retainEditorAndModified` -: Use this option to retain the editor and modified date. When not specified, these values are reset. - -`--bypassSharedLock` -: This indicates whether a folder with a share lock can still be moved. Use this option to move a folder that is locked. +`--skipWait` +: Don't wait for the move operation to complete. ``` ## Remarks -All folder versions are retained while moving a folder. - When you specify a value for `nameConflictBehavior`, consider the following: - `fail` will throw an error when the destination folder already exists. @@ -69,16 +66,90 @@ Move a folder to another location and use a new name on conflict. m365 spo folder move --webUrl https://contoso.sharepoint.com/sites/project-x --sourceUrl "/sites/project-x/Shared Documents/Reports" --targetUrl "/sites/project-y/Shared Documents/Project files" --nameConflictBehavior rename ``` -Move a folder referenced by its ID to another document library and retain editor and modified date. +Move a folder referenced by its ID to another document library and don't wait for the move job to finish. ```sh -m365 spo folder move --webUrl https://contoso.sharepoint.com/sites/project-x --sourceId b8cc341b-9c11-4f2d-aa2b-0ce9c18bcba2 --targetUrl "/sites/project-x/Project files" --retainEditorAndModified +m365 spo folder move --webUrl https://contoso.sharepoint.com/sites/project-x --sourceId b8cc341b-9c11-4f2d-aa2b-0ce9c18bcba2 --targetUrl "/sites/project-x/Project files" --skipWait ``` ## Response -The command won't return a response on success. +### Standard Response + + + + + ```json + { + "Exists": true, + "ExistsAllowThrowForPolicyFailures": true, + "ExistsWithException": true, + "IsWOPIEnabled": false, + "ItemCount": 6, + "Name": "Company", + "ProgID": null, + "ServerRelativeUrl": "/sites/Sales/Icons/Company", + "TimeCreated": "2024-09-26T22:08:53Z", + "TimeLastModified": "2024-09-26T22:09:31Z", + "UniqueId": "d3a37396-ca16-467b-b968-48f5fc41f2b6", + "WelcomePage": "" + } + ``` + + + + + ```text + Exists : true + ExistsAllowThrowForPolicyFailures: true + ExistsWithException : true + IsWOPIEnabled : false + ItemCount : 6 + Name : Company + ProgID : null + ServerRelativeUrl : /sites/Sales/Icons/Company + TimeCreated : 2024-09-26T22:08:53Z + TimeLastModified : 2024-09-26T22:09:31Z + UniqueId : d3a37396-ca16-467b-b968-48f5fc41f2b6 + WelcomePage : + ``` + + + + + ```csv + Exists,ExistsAllowThrowForPolicyFailures,ExistsWithException,IsWOPIEnabled,ItemCount,Name,ProgID,ServerRelativeUrl,TimeCreated,TimeLastModified,UniqueId,WelcomePage + 1,1,1,0,6,Company,,/sites/Sales/Icons/Company,2024-09-26T22:08:53Z,2024-09-26T22:09:31Z,d3a37396-ca16-467b-b968-48f5fc41f2b6, + ``` + + + + + ```md + # spo folder move --webUrl "https://contoso.sharepoint.com/sites/Marketing" --sourceUrl "/Logos/Contoso" --targetUrl "/sites/Sales/Logos" + + Date: 27/09/2024 + + ## Company (d3a37396-ca16-467b-b968-48f5fc41f2b6) + + Property | Value + ---------|------- + Exists | true + ExistsAllowThrowForPolicyFailures | true + ExistsWithException | true + IsWOPIEnabled | false + ItemCount | 6 + Name | Company + ServerRelativeUrl | /sites/Sales/Icons/Company + TimeCreated | 2024-09-26T22:08:53Z + TimeLastModified | 2024-09-26T22:09:31Z + UniqueId | d3a37396-ca16-467b-b968-48f5fc41f2b6 + WelcomePage | + ``` + + + + +### `skipWait` response -## More information - -- Move items from a SharePoint document library: [https://support.office.com/en-us/article/move-or-copy-items-from-a-sharepoint-document-library-00e2f483-4df3-46be-a861-1f5f0c1a87bc](https://support.office.com/en-us/article/move-or-copy-items-from-a-sharepoint-document-library-00e2f483-4df3-46be-a861-1f5f0c1a87bc) +The command won't return a response on success. diff --git a/docs/docs/v10-upgrade-guidance.mdx b/docs/docs/v10-upgrade-guidance.mdx index 5874f182cc5..1402e97ad85 100644 --- a/docs/docs/v10-upgrade-guidance.mdx +++ b/docs/docs/v10-upgrade-guidance.mdx @@ -230,6 +230,23 @@ In the past versions of CLI for Microsoft 365, the command had no output. When u When using the [spo file move](./cmd/spo/file/file-move.mdx) command, please use the new command input. This means that you'll have to remove option `--retainEditorAndModified` from your scripts and automation tools. +### Updated command `spo folder move` + +Because of some limitations of the current [spo folder move](./cmd/spo/folder/folder-move.mdx) command, we have decided to move it to a new endpoint. This change is necessary to ensure the command's functionality and reliability. Because of the new endpoint, the command input and output have changed. + +**Command options:** + +Unfortunately, we had to drop the `--retainEditorAndModified` and `--bypassSharedLock` options as it's no longer supported by the new endpoint. In return, we were able to add a new option: +- `--skipWait`: Don't wait for the move operation to complete. + +**Command output:** + +In the past versions of CLI for Microsoft 365, the command had no output. When using option `--nameConflictBehavior rename`, it's hard for the user to know what the actual name of the moved folder is. With the new endpoint, the command now returns the folder information about the destination folder, providing you with all the info you need to execute other commands on this folder. + +#### What action do I need to take? + +When using the [spo folder move](./cmd/spo/folder/folder-move.mdx) command, please use the new command input. This means that you'll have to remove options `--retainEditorAndModified` and `--bypassSharedLock` from your scripts and automation tools. + ### Removed `spo folder rename` alias The `spo folder rename` command was removed and replaced by the [spo folder set](./cmd/spo/folder/folder-set.mdx) command. diff --git a/src/m365/spo/commands/file/file-copy.spec.ts b/src/m365/spo/commands/file/file-copy.spec.ts index c9b10a0c4b6..254eecb9628 100644 --- a/src/m365/spo/commands/file/file-copy.spec.ts +++ b/src/m365/spo/commands/file/file-copy.spec.ts @@ -13,7 +13,7 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './file-copy.js'; import { settingsNames } from '../../../../settingsNames.js'; -import { CreateCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; +import { CreateFileCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; describe(commands.FILE_COPY, () => { const sourceWebUrl = 'https://contoso.sharepoint.com/sites/Sales'; @@ -94,7 +94,7 @@ describe(commands.FILE_COPY, () => { commandInfo = cli.getCommandInfo(command); sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName: string, defaultValue: any) => settingName === settingsNames.prompt ? false : defaultValue); - spoUtilCreateCopyJobStub = sinon.stub(spo, 'createCopyJob').resolves(copyJobInfo); + spoUtilCreateCopyJobStub = sinon.stub(spo, 'createFileCopyJob').resolves(copyJobInfo); }); beforeEach(() => { @@ -300,7 +300,7 @@ describe(commands.FILE_COPY, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Fail, bypassSharedLock: false, ignoreVersionHistory: false, operation: 'copy', @@ -332,7 +332,7 @@ describe(commands.FILE_COPY, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Fail, bypassSharedLock: false, ignoreVersionHistory: false, operation: 'copy', @@ -367,7 +367,7 @@ describe(commands.FILE_COPY, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Rename, bypassSharedLock: true, ignoreVersionHistory: true, operation: 'copy', @@ -400,7 +400,7 @@ describe(commands.FILE_COPY, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Replace, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Replace, bypassSharedLock: false, ignoreVersionHistory: false, operation: 'copy', diff --git a/src/m365/spo/commands/file/file-copy.ts b/src/m365/spo/commands/file/file-copy.ts index ca73f20b733..4740014d378 100644 --- a/src/m365/spo/commands/file/file-copy.ts +++ b/src/m365/spo/commands/file/file-copy.ts @@ -1,7 +1,7 @@ import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; -import { CreateCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; +import { CreateFileCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; import { urlUtil } from '../../../../utils/urlUtil.js'; import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; @@ -140,7 +140,7 @@ class SpoFileCopyCommand extends SpoCommand { newName += sourceServerRelativePath.substring(sourceServerRelativePath.lastIndexOf('.')); } - const copyJobResponse = await spo.createCopyJob( + const copyJobResponse = await spo.createFileCopyJob( args.options.webUrl, sourcePath, destinationPath, @@ -208,16 +208,16 @@ class SpoFileCopyCommand extends SpoCommand { return file.DecodedUrl; } - private getNameConflictBehaviorValue(nameConflictBehavior?: string): CreateCopyJobsNameConflictBehavior { + private getNameConflictBehaviorValue(nameConflictBehavior?: string): CreateFileCopyJobsNameConflictBehavior { switch (nameConflictBehavior?.toLowerCase()) { case 'fail': - return CreateCopyJobsNameConflictBehavior.Fail; + return CreateFileCopyJobsNameConflictBehavior.Fail; case 'replace': - return CreateCopyJobsNameConflictBehavior.Replace; + return CreateFileCopyJobsNameConflictBehavior.Replace; case 'rename': - return CreateCopyJobsNameConflictBehavior.Rename; + return CreateFileCopyJobsNameConflictBehavior.Rename; default: - return CreateCopyJobsNameConflictBehavior.Fail; + return CreateFileCopyJobsNameConflictBehavior.Fail; } } diff --git a/src/m365/spo/commands/file/file-move.spec.ts b/src/m365/spo/commands/file/file-move.spec.ts index 6a8bb6be373..5aef495324e 100644 --- a/src/m365/spo/commands/file/file-move.spec.ts +++ b/src/m365/spo/commands/file/file-move.spec.ts @@ -12,7 +12,7 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './file-move.js'; -import { CreateCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; +import { CreateFileCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.FILE_MOVE, () => { @@ -95,7 +95,7 @@ describe(commands.FILE_MOVE, () => { auth.connection.active = true; commandInfo = cli.getCommandInfo(command); sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName: string, defaultValue: any) => settingName === settingsNames.prompt ? false : defaultValue); - spoUtilCreateCopyJobStub = sinon.stub(spo, 'createCopyJob').resolves(copyJobInfo); + spoUtilCreateCopyJobStub = sinon.stub(spo, 'createFileCopyJob').resolves(copyJobInfo); }); beforeEach(() => { @@ -296,7 +296,7 @@ describe(commands.FILE_MOVE, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Fail, bypassSharedLock: false, includeItemPermissions: false, operation: 'move', @@ -328,7 +328,7 @@ describe(commands.FILE_MOVE, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Fail, bypassSharedLock: false, includeItemPermissions: false, operation: 'move', @@ -360,7 +360,7 @@ describe(commands.FILE_MOVE, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Fail, bypassSharedLock: false, includeItemPermissions: false, operation: 'move', @@ -392,7 +392,7 @@ describe(commands.FILE_MOVE, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Replace, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Replace, bypassSharedLock: false, includeItemPermissions: false, operation: 'move', @@ -427,7 +427,7 @@ describe(commands.FILE_MOVE, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Rename, bypassSharedLock: true, includeItemPermissions: true, operation: 'move', @@ -460,7 +460,7 @@ describe(commands.FILE_MOVE, () => { sourceAbsoluteUrl, destAbsoluteTargetUrl, { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Replace, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Replace, bypassSharedLock: false, includeItemPermissions: false, operation: 'move', diff --git a/src/m365/spo/commands/file/file-move.ts b/src/m365/spo/commands/file/file-move.ts index ae001e31ad9..b3d85665ee1 100644 --- a/src/m365/spo/commands/file/file-move.ts +++ b/src/m365/spo/commands/file/file-move.ts @@ -1,7 +1,7 @@ import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; -import { CreateCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; +import { CreateFileCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; import { urlUtil } from '../../../../utils/urlUtil.js'; import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; @@ -141,7 +141,7 @@ class SpoFileMoveCommand extends SpoCommand { newName += sourceServerRelativePath.substring(sourceServerRelativePath.lastIndexOf('.')); } - const copyJobResponse = await spo.createCopyJob( + const copyJobResponse = await spo.createFileCopyJob( args.options.webUrl, sourcePath, destinationPath, @@ -209,16 +209,16 @@ class SpoFileMoveCommand extends SpoCommand { return file.DecodedUrl; } - private getNameConflictBehaviorValue(nameConflictBehavior?: string): CreateCopyJobsNameConflictBehavior { + private getNameConflictBehaviorValue(nameConflictBehavior?: string): CreateFileCopyJobsNameConflictBehavior { switch (nameConflictBehavior?.toLowerCase()) { case 'fail': - return CreateCopyJobsNameConflictBehavior.Fail; + return CreateFileCopyJobsNameConflictBehavior.Fail; case 'replace': - return CreateCopyJobsNameConflictBehavior.Replace; + return CreateFileCopyJobsNameConflictBehavior.Replace; case 'rename': - return CreateCopyJobsNameConflictBehavior.Rename; + return CreateFileCopyJobsNameConflictBehavior.Rename; default: - return CreateCopyJobsNameConflictBehavior.Fail; + return CreateFileCopyJobsNameConflictBehavior.Fail; } } diff --git a/src/m365/spo/commands/folder/folder-move.spec.ts b/src/m365/spo/commands/folder/folder-move.spec.ts index fe9f9a24b5d..f4e8748d3ea 100644 --- a/src/m365/spo/commands/folder/folder-move.spec.ts +++ b/src/m365/spo/commands/folder/folder-move.spec.ts @@ -12,21 +12,68 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './folder-move.js'; +import { settingsNames } from '../../../../settingsNames.js'; +import { CreateFolderCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; + +const sourceWebUrl = 'https://contoso.sharepoint.com/sites/Sales'; +const sourceFolderName = 'Logos'; +const sourceServerRelUrl = '/sites/Sales/Shared Documents/' + sourceFolderName; +const sourceSiteRelUrl = '/Shared Documents/' + sourceFolderName; +const sourceAbsoluteUrl = 'https://contoso.sharepoint.com' + sourceServerRelUrl; +const sourceFolderId = 'f09c4efe-b8c0-4e89-a166-03418661b89b'; + +const destWebUrl = 'https://contoso.sharepoint.com/sites/Marketing'; +const destSiteRelUrl = '/Documents'; +const destServerRelUrl = '/sites/Marketing' + destSiteRelUrl; +const destAbsoluteTargetUrl = 'https://contoso.sharepoint.com' + destServerRelUrl; +const destFolderId = '15488d89-b82b-40be-958a-922b2ed79383'; + +const copyJobInfo = { + EncryptionKey: '2by8+2oizihYOFqk02Tlokj8lWUShePAEE+WMuA9lzA=', + JobId: 'd812e5a0-d95a-4e4f-bcb7-d4415e88c8ee', + JobQueueUri: 'https://spoam1db1m020p4.queue.core.windows.net/2-1499-20240831-29533e6c72c6464780b756c71ea3fe92?sv=2018-03-28&sig=aX%2BNOkUimZ3f%2B%2BvdXI95%2FKJI1e5UE6TU703Dw3Eb5c8%3D&st=2024-08-09T00%3A00%3A00Z&se=2024-08-31T00%3A00%3A00Z&sp=rap', + SourceListItemUniqueIds: [ + sourceFolderId + ] +}; + +const copyJobResult = { + Event: 'JobFinishedObjectInfo', + JobId: '6d1eda82-0d1c-41eb-ab05-1d9cd4afe786', + Time: '08/10/2024 18:59:40.145', + SourceObjectFullUrl: sourceAbsoluteUrl, + TargetServerUrl: 'https://contoso.sharepoint.com', + TargetSiteId: '794dada8-4389-45ce-9559-0de74bf3554a', + TargetWebId: '8de9b4d3-3c30-4fd0-a9d7-2452bd065555', + TargetListId: '44b336a5-e397-4e22-a270-c39e9069b123', + TargetObjectUniqueId: destFolderId, + TargetObjectSiteRelativeUrl: destSiteRelUrl.substring(1), + CorrelationId: '5efd44a1-c034-9000-9692-4e1a1b3ca33b' +}; + +const destFolderResponse = { + Exists: true, + ExistsAllowThrowForPolicyFailures: true, + ExistsWithException: true, + IsWOPIEnabled: false, + ItemCount: 6, + Name: sourceFolderName, + ProgID: null, + ServerRelativeUrl: destServerRelUrl, + TimeCreated: '2024-09-26T20:52:07Z', + TimeLastModified: '2024-09-26T21:16:26Z', + UniqueId: '59abed95-34f9-470b-a133-ae8932480b53', + WelcomePage: '' +}; describe(commands.FOLDER_MOVE, () => { - const folderName = 'Reports'; - const rootUrl = 'https://contoso.sharepoint.com'; - const webUrl = rootUrl + '/sites/project-x'; - const sourceUrl = '/sites/project-x/documents/' + folderName; - const targetUrl = '/sites/project-y/My Documents'; - const absoluteSourceUrl = rootUrl + sourceUrl; - const absoluteTargetUrl = rootUrl + targetUrl; - const sourceId = 'b8cc341b-9c11-4f2d-aa2b-0ce9c18bcba2'; - let log: any[]; let logger: Logger; let commandInfo: CommandInfo; - let postStub: sinon.SinonStub; + let loggerLogSpy: sinon.SinonSpy; + + let spoUtilCreateCopyJobStub: sinon.SinonStub; + let spoUtilGetCopyJobResultStub: sinon.SinonStub; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -36,6 +83,9 @@ describe(commands.FOLDER_MOVE, () => { auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName: string, defaultValue: any) => settingName === settingsNames.prompt ? false : defaultValue); + spoUtilCreateCopyJobStub = sinon.stub(spo, 'createFolderCopyJob').resolves(copyJobInfo); + spoUtilGetCopyJobResultStub = sinon.stub(spo, 'getCopyJobResult').resolves(copyJobResult); }); beforeEach(() => { @@ -52,15 +102,8 @@ describe(commands.FOLDER_MOVE, () => { } }; - postStub = sinon.stub(request, 'post').callsFake(async opts => { - if (opts.url === `${webUrl}/_api/SP.MoveCopyUtil.MoveFolderByPath`) { - return { - 'odata.null': true - }; - } - - throw 'Invalid request: ' + opts.url; - }); + loggerLogSpy = sinon.spy(logger, 'log'); + spoUtilGetCopyJobResultStub.resetHistory(); }); afterEach(() => { @@ -88,181 +131,314 @@ describe(commands.FOLDER_MOVE, () => { }); it('fails validation if the webUrl option is not a valid SharePoint site URL', async () => { - const actual = await command.validate({ options: { webUrl: 'invalid', sourceUrl: sourceUrl, targetUrl: targetUrl } }, commandInfo); + const actual = await command.validate({ options: { webUrl: 'invalid', sourceUrl: sourceServerRelUrl, targetUrl: destServerRelUrl } }, commandInfo); assert.notStrictEqual(actual, true); }); it('fails validation if sourceId is not a valid guid', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, sourceId: 'invalid', targetUrl: targetUrl } }, commandInfo); + const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceId: 'invalid', targetUrl: destServerRelUrl } }, commandInfo); assert.notStrictEqual(actual, true); }); it('fails validation if nameConflictBehavior is not valid', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, sourceUrl: sourceUrl, targetUrl: targetUrl, nameConflictBehavior: 'invalid' } }, commandInfo); + const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceUrl: sourceServerRelUrl, targetUrl: destServerRelUrl, nameConflictBehavior: 'invalid' } }, commandInfo); assert.notStrictEqual(actual, true); }); it('passes validation if the sourceId is a valid GUID', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, sourceId: sourceId, targetUrl: targetUrl } }, commandInfo); + const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceId: sourceFolderId, targetUrl: destServerRelUrl } }, commandInfo); assert.strictEqual(actual, true); }); it('passes validation if the webUrl option is a valid SharePoint site URL', async () => { - const actual = await command.validate({ options: { webUrl: webUrl, sourceUrl: sourceUrl, targetUrl: targetUrl } }, commandInfo); + const actual = await command.validate({ options: { webUrl: sourceWebUrl, sourceUrl: sourceServerRelUrl, targetUrl: destServerRelUrl } }, commandInfo); assert.strictEqual(actual, true); }); - it('moves a folder correctly when specifying sourceId', async () => { - sinon.stub(request, 'get').callsFake(async opts => { - if (opts.url === `${webUrl}/_api/Web/GetFolderById('${sourceId}')?$select=ServerRelativePath`) { + it('correctly outputs exactly one result when folder is moved when using sourceId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${sourceWebUrl}/_api/Web/GetFolderById('${sourceFolderId}')/ServerRelativePath`) { return { - ServerRelativePath: { - DecodedUrl: sourceUrl - } + DecodedUrl: destAbsoluteTargetUrl + `/${sourceFolderName}` + }; + } + + if (opts.url === `${destWebUrl}/_api/Web/GetFolderById('${destFolderId}')`) { + return destFolderResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + webUrl: sourceWebUrl, + sourceId: sourceFolderId, + targetUrl: destAbsoluteTargetUrl + } + }); + + assert(loggerLogSpy.calledOnce); + }); + + it('correctly outputs result when folder is moved when using sourceId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${sourceWebUrl}/_api/Web/GetFolderById('${sourceFolderId}')/ServerRelativePath`) { + return { + DecodedUrl: sourceAbsoluteUrl + }; + } + + if (opts.url === `${destWebUrl}/_api/Web/GetFolderById('${destFolderId}')`) { + return destFolderResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + webUrl: sourceWebUrl, + sourceId: sourceFolderId, + targetUrl: destAbsoluteTargetUrl + } + }); + + assert(loggerLogSpy.calledOnce); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], destFolderResponse); + }); + + it('correctly outputs result when folder is moved when using sourceUrl', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${destWebUrl}/_api/Web/GetFolderById('${destFolderId}')`) { + return destFolderResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + verbose: true, + webUrl: sourceWebUrl, + sourceUrl: sourceServerRelUrl, + targetUrl: destAbsoluteTargetUrl + } + }); + + assert(loggerLogSpy.calledOnce); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], destFolderResponse); + }); + + it('correctly moves a folder when using sourceId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${sourceWebUrl}/_api/Web/GetFolderById('${sourceFolderId}')/ServerRelativePath`) { + return { + DecodedUrl: sourceAbsoluteUrl }; } + if (opts.url === `${destWebUrl}/_api/Web/GetFolderById('${destFolderId}')`) { + return destFolderResponse; + } + throw 'Invalid request: ' + opts.url; }); await command.action(logger, { options: { - webUrl: webUrl, - sourceId: sourceId, - targetUrl: targetUrl, - verbose: true + webUrl: sourceWebUrl, + sourceId: sourceFolderId, + targetUrl: destAbsoluteTargetUrl } }); - assert.deepStrictEqual(postStub.lastCall.args[0].data, + assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [ + sourceWebUrl, + sourceAbsoluteUrl, + destAbsoluteTargetUrl, { - srcPath: { - DecodedUrl: absoluteSourceUrl - }, - destPath: { - DecodedUrl: absoluteTargetUrl + `/${folderName}` - }, - options: { - KeepBoth: false, - ShouldBypassSharedLocks: false, - RetainEditorAndModifiedOnMove: false - } + nameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Fail, + operation: 'move', + newName: undefined } - ); + ]); }); - it('moves a folder correctly when specifying sourceUrl with server relative paths', async () => { + it('correctly moves a folder when using sourceUrl', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${destWebUrl}/_api/Web/GetFolderById('${destFolderId}')`) { + return destFolderResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + await command.action(logger, { options: { - webUrl: webUrl, - sourceUrl: sourceUrl, - targetUrl: targetUrl + webUrl: sourceWebUrl, + sourceUrl: sourceServerRelUrl, + targetUrl: destAbsoluteTargetUrl, + nameConflictBehavior: 'fail' } }); - assert.deepStrictEqual(postStub.lastCall.args[0].data, + assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [ + sourceWebUrl, + sourceAbsoluteUrl, + destAbsoluteTargetUrl, { - srcPath: { - DecodedUrl: absoluteSourceUrl - }, - destPath: { - DecodedUrl: absoluteTargetUrl + `/${folderName}` - }, - options: { - KeepBoth: false, - ShouldBypassSharedLocks: false, - RetainEditorAndModifiedOnMove: false - } + nameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Fail, + operation: 'move', + newName: undefined } - ); + ]); }); - it('moves a folder correctly when specifying sourceUrl with site relative paths', async () => { + it('correctly moves a folder when using site-relative sourceUrl', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${destWebUrl}/_api/Web/GetFolderById('${destFolderId}')`) { + return destFolderResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + await command.action(logger, { options: { - webUrl: webUrl, - sourceUrl: `/Shared Documents/${folderName}`, - targetUrl: targetUrl, + webUrl: sourceWebUrl, + sourceUrl: sourceSiteRelUrl, + targetUrl: destAbsoluteTargetUrl, nameConflictBehavior: 'fail' } }); - assert.deepStrictEqual(postStub.lastCall.args[0].data, + assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [ + sourceWebUrl, + sourceAbsoluteUrl, + destAbsoluteTargetUrl, { - srcPath: { - DecodedUrl: webUrl + `/Shared Documents/${folderName}` - }, - destPath: { - DecodedUrl: absoluteTargetUrl + `/${folderName}` - }, - options: { - KeepBoth: false, - ShouldBypassSharedLocks: false, - RetainEditorAndModifiedOnMove: false - } + nameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Fail, + operation: 'move', + newName: undefined } - ); + ]); }); - it('moves a folder correctly when specifying sourceUrl with absolute paths', async () => { + it('correctly moves a folder when using absolute urls', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${destWebUrl}/_api/Web/GetFolderById('${destFolderId}')`) { + return destFolderResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + await command.action(logger, { options: { - webUrl: webUrl, - sourceUrl: rootUrl + sourceUrl, - targetUrl: rootUrl + targetUrl, - nameConflictBehavior: 'replace' + webUrl: sourceWebUrl, + sourceUrl: sourceAbsoluteUrl, + targetUrl: destAbsoluteTargetUrl, + nameConflictBehavior: 'rename' } }); - assert.deepStrictEqual(postStub.lastCall.args[0].data, + assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [ + sourceWebUrl, + sourceAbsoluteUrl, + destAbsoluteTargetUrl, { - srcPath: { - DecodedUrl: absoluteSourceUrl - }, - destPath: { - DecodedUrl: absoluteTargetUrl + `/${folderName}` - }, - options: { - KeepBoth: false, - ShouldBypassSharedLocks: false, - RetainEditorAndModifiedOnMove: false - } + nameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Rename, + operation: 'move', + newName: undefined } - ); + ]); }); - it('moves a folder correctly when specifying various options', async () => { + it('correctly moves a folder when using sourceUrl with extra options', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${destWebUrl}/_api/Web/GetFolderById('${destFolderId}')`) { + return destFolderResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + await command.action(logger, { options: { - webUrl: webUrl, - sourceUrl: sourceUrl, - targetUrl: targetUrl, - newName: 'Old reports', + webUrl: sourceWebUrl, + sourceUrl: sourceServerRelUrl, + targetUrl: destAbsoluteTargetUrl, nameConflictBehavior: 'rename', - retainEditorAndModified: true, - bypassSharedLock: true + newName: 'Folder-renamed' } }); - assert.deepStrictEqual(postStub.lastCall.args[0].data, + assert.deepStrictEqual(spoUtilCreateCopyJobStub.lastCall.args, [ + sourceWebUrl, + sourceAbsoluteUrl, + destAbsoluteTargetUrl, { - srcPath: { - DecodedUrl: absoluteSourceUrl - }, - destPath: { - DecodedUrl: absoluteTargetUrl + '/Old reports' - }, - options: { - KeepBoth: true, - ShouldBypassSharedLocks: true, - RetainEditorAndModifiedOnMove: true - } + nameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Rename, + operation: 'move', + newName: 'Folder-renamed' + } + ]); + }); + + it('correctly polls for the copy job to finish', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${destWebUrl}/_api/Web/GetFolderById('${destFolderId}')`) { + return destFolderResponse; + } + + throw 'Invalid request: ' + opts.url; + }); + + await command.action(logger, { + options: { + webUrl: sourceWebUrl, + sourceUrl: sourceServerRelUrl, + targetUrl: destAbsoluteTargetUrl + } + }); + + assert.deepStrictEqual(spoUtilGetCopyJobResultStub.lastCall.args, [ + sourceWebUrl, + copyJobInfo + ]); + }); + + it('outputs no result when skipWait is specified', async () => { + await command.action(logger, { + options: { + webUrl: sourceWebUrl, + sourceUrl: sourceServerRelUrl, + targetUrl: destAbsoluteTargetUrl, + skipWait: true + } + }); + + assert(loggerLogSpy.notCalled); + }); + + it('correctly skips polling when skipWait is specified', async () => { + await command.action(logger, { + options: { + webUrl: sourceWebUrl, + sourceUrl: sourceServerRelUrl, + targetUrl: destAbsoluteTargetUrl, + skipWait: true } - ); + }); + + assert(spoUtilGetCopyJobResultStub.notCalled); }); - it('handles error correctly when moving a folder', async () => { - const error = { + it('correctly handles error when sourceId does not exist', async () => { + sinon.stub(request, 'get').rejects({ error: { 'odata.error': { message: { @@ -271,16 +447,27 @@ describe(commands.FOLDER_MOVE, () => { } } } - }; + }); + + await assert.rejects(command.action(logger, { + options: { + webUrl: sourceWebUrl, + sourceId: sourceFolderId, + targetUrl: destAbsoluteTargetUrl + } + }), new CommandError('Folder Not Found.')); + }); - sinon.stub(request, 'get').rejects(error); + it('correctly handles error when getCopyJobResult fails', async () => { + spoUtilGetCopyJobResultStub.restore(); + spoUtilGetCopyJobResultStub = sinon.stub(spo, 'getCopyJobResult').rejects(new Error('Target folder already exists.')); await assert.rejects(command.action(logger, { options: { - webUrl: webUrl, - sourceId: sourceId, - targetUrl: targetUrl + webUrl: sourceWebUrl, + sourceUrl: sourceServerRelUrl, + targetUrl: destAbsoluteTargetUrl } - }), new CommandError(error.error['odata.error'].message.value)); + }), new CommandError('Target folder already exists.')); }); }); diff --git a/src/m365/spo/commands/folder/folder-move.ts b/src/m365/spo/commands/folder/folder-move.ts index ec29e58a38a..0534e123d67 100644 --- a/src/m365/spo/commands/folder/folder-move.ts +++ b/src/m365/spo/commands/folder/folder-move.ts @@ -1,6 +1,7 @@ import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; +import { CreateFolderCopyJobsNameConflictBehavior, spo } from '../../../../utils/spo.js'; import { urlUtil } from '../../../../utils/urlUtil.js'; import { validation } from '../../../../utils/validation.js'; import SpoCommand from '../../../base/SpoCommand.js'; @@ -17,8 +18,7 @@ interface Options extends GlobalOptions { targetUrl: string; newName?: string; nameConflictBehavior?: string; - retainEditorAndModified?: boolean; - bypassSharedLock?: boolean; + skipWait?: boolean; } class SpoFolderMoveCommand extends SpoCommand { @@ -49,8 +49,7 @@ class SpoFolderMoveCommand extends SpoCommand { sourceId: typeof args.options.sourceId !== 'undefined', newName: typeof args.options.newName !== 'undefined', nameConflictBehavior: typeof args.options.nameConflictBehavior !== 'undefined', - retainEditorAndModified: !!args.options.retainEditorAndModified, - bypassSharedLock: !!args.options.bypassSharedLock + skipWait: !!args.options.skipWait }); }); } @@ -77,10 +76,7 @@ class SpoFolderMoveCommand extends SpoCommand { autocomplete: this.nameConflictBehaviorOptions }, { - option: '--retainEditorAndModified' - }, - { - option: '--bypassSharedLock' + option: '--skipWait' } ); } @@ -112,7 +108,7 @@ class SpoFolderMoveCommand extends SpoCommand { #initTypes(): void { this.types.string.push('webUrl', 'sourceUrl', 'sourceId', 'targetUrl', 'newName', 'nameConflictBehavior'); - this.types.boolean.push('retainEditorAndModified', 'bypassSharedLock'); + this.types.boolean.push('skipWait'); } protected getExcludedOptionsWithUrls(): string[] | undefined { @@ -121,51 +117,70 @@ class SpoFolderMoveCommand extends SpoCommand { public async commandAction(logger: Logger, args: CommandArgs): Promise { try { - const sourcePath = await this.getSourcePath(logger, args.options); + const sourceServerRelativePath = await this.getSourcePath(logger, args.options); + const sourcePath = this.getAbsoluteUrl(args.options.webUrl, sourceServerRelativePath); + const destinationPath = this.getAbsoluteUrl(args.options.webUrl, args.options.targetUrl); if (this.verbose) { - await logger.logToStderr(`Moving folder '${sourcePath}' to '${args.options.targetUrl}'...`); + await logger.logToStderr(`Moving folder '${sourcePath}' to '${destinationPath}'...`); } - const absoluteSourcePath = this.getAbsoluteUrl(args.options.webUrl, sourcePath); - let absoluteTargetPath = this.getAbsoluteUrl(args.options.webUrl, args.options.targetUrl) + '/'; + const copyJobResponse = await spo.createFolderCopyJob( + args.options.webUrl, + sourcePath, + destinationPath, + { + nameConflictBehavior: this.getNameConflictBehaviorValue(args.options.nameConflictBehavior), + newName: args.options.newName, + operation: 'move' + } + ); - if (args.options.newName) { - absoluteTargetPath += args.options.newName; + if (args.options.skipWait) { + return; } - else { - // Keep the original file name - absoluteTargetPath += sourcePath.substring(sourcePath.lastIndexOf('/') + 1); + + if (this.verbose) { + await logger.logToStderr('Waiting for the move job to complete...'); + } + + const copyJobResult = await spo.getCopyJobResult(args.options.webUrl, copyJobResponse); + + if (this.verbose) { + await logger.logToStderr('Getting information about the destination folder...'); } + // Get destination folder data + const siteRelativeDestinationFolder = '/' + copyJobResult.TargetObjectSiteRelativeUrl.substring(0, copyJobResult.TargetObjectSiteRelativeUrl.lastIndexOf('/')); + const absoluteWebUrl = destinationPath.substring(0, destinationPath.toLowerCase().lastIndexOf(siteRelativeDestinationFolder.toLowerCase())); + const requestOptions: CliRequestOptions = { - url: `${args.options.webUrl}/_api/SP.MoveCopyUtil.MoveFolderByPath`, + url: `${absoluteWebUrl}/_api/Web/GetFolderById('${copyJobResult.TargetObjectUniqueId}')`, headers: { accept: 'application/json;odata=nometadata' }, - responseType: 'json', - data: { - srcPath: { - DecodedUrl: absoluteSourcePath - }, - destPath: { - DecodedUrl: absoluteTargetPath - }, - options: { - KeepBoth: args.options.nameConflictBehavior === 'rename', - ShouldBypassSharedLocks: !!args.options.bypassSharedLock, - RetainEditorAndModifiedOnMove: !!args.options.retainEditorAndModified - } - } + responseType: 'json' }; - await request.post(requestOptions); + const destinationFile = await request.get(requestOptions); + await logger.log(destinationFile); } catch (err: any) { this.handleRejectedODataJsonPromise(err); } } + private getNameConflictBehaviorValue(nameConflictBehavior?: string): CreateFolderCopyJobsNameConflictBehavior { + switch (nameConflictBehavior?.toLowerCase()) { + case 'fail': + return CreateFolderCopyJobsNameConflictBehavior.Fail; + case 'rename': + return CreateFolderCopyJobsNameConflictBehavior.Rename; + default: + return CreateFolderCopyJobsNameConflictBehavior.Fail; + } + } + private async getSourcePath(logger: Logger, options: Options): Promise { if (options.sourceUrl) { return urlUtil.getServerRelativePath(options.webUrl, options.sourceUrl); @@ -176,19 +191,20 @@ class SpoFolderMoveCommand extends SpoCommand { } const requestOptions: CliRequestOptions = { - url: `${options.webUrl}/_api/Web/GetFolderById('${options.sourceId}')?$select=ServerRelativePath`, + url: `${options.webUrl}/_api/Web/GetFolderById('${options.sourceId}')/ServerRelativePath`, headers: { accept: 'application/json;odata=nometadata' }, responseType: 'json' }; - const file = await request.get<{ ServerRelativePath: { DecodedUrl: string } }>(requestOptions); - return file.ServerRelativePath.DecodedUrl; + const path = await request.get<{ DecodedUrl: string }>(requestOptions); + return path.DecodedUrl; } private getAbsoluteUrl(webUrl: string, url: string): string { - return url.startsWith('https://') ? url : urlUtil.getAbsoluteUrl(webUrl, url); + const result = url.startsWith('https://') ? url : urlUtil.getAbsoluteUrl(webUrl, url); + return urlUtil.removeTrailingSlashes(result); } } diff --git a/src/utils/spo.spec.ts b/src/utils/spo.spec.ts index 51604369902..37debe16ffb 100644 --- a/src/utils/spo.spec.ts +++ b/src/utils/spo.spec.ts @@ -7,10 +7,11 @@ import config from '../config.js'; import { RoleDefinition } from '../m365/spo/commands/roledefinition/RoleDefinition.js'; import request from '../request.js'; import { sinonUtil } from '../utils/sinonUtil.js'; -import { CreateCopyJobsNameConflictBehavior, FormDigestInfo, SpoOperation, spo, settings } from '../utils/spo.js'; +import { CreateFileCopyJobsNameConflictBehavior, FormDigestInfo, SpoOperation, spo, CreateFolderCopyJobsNameConflictBehavior } from '../utils/spo.js'; import { entraGroup } from './entraGroup.js'; import { formatting } from './formatting.js'; import { Group } from '@microsoft/microsoft-graph-types'; +import { timersUtil } from './timersUtil.js'; const stubPostResponses: any = ( folderAddResp: any = null @@ -96,7 +97,7 @@ describe('utils/spo', () => { before(() => { auth.connection.active = true; - sinon.stub(settings, 'pollingInterval').value(0); + sinon.stub(timersUtil, 'setTimeout').resolves(); }); beforeEach(() => { @@ -2680,7 +2681,7 @@ describe('utils/spo', () => { assert.deepEqual(group, fileResponse); }); - it('correctly outputs result when calling createCopyJob', async () => { + it('correctly outputs result when calling createFileCopyJob', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/CreateCopyJobs') { return { @@ -2693,11 +2694,11 @@ describe('utils/spo', () => { throw 'Invalid request: ' + opts.url; }); - const result = await spo.createCopyJob('https://contoso.sharepoint.com/sites/sales', 'https://contoso.sharepoint.com/sites/sales/Icons/Company.png', 'https://contoso.sharepoint.com/sites/marketing/Shared Documents'); + const result = await spo.createFileCopyJob('https://contoso.sharepoint.com/sites/sales', 'https://contoso.sharepoint.com/sites/sales/Icons/Company.png', 'https://contoso.sharepoint.com/sites/marketing/Shared Documents'); assert.deepStrictEqual(result, copyJobInfo); }); - it('correctly creates a copy job with default options when using createCopyJob', async () => { + it('correctly creates a copy job with default options when using createFileCopyJob', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/CreateCopyJobs') { return { @@ -2710,12 +2711,12 @@ describe('utils/spo', () => { throw 'Invalid request: ' + opts.url; }); - await spo.createCopyJob('https://contoso.sharepoint.com/sites/sales', 'https://contoso.sharepoint.com/sites/sales/Icons/Company.png', 'https://contoso.sharepoint.com/sites/marketing/Shared Documents'); + await spo.createFileCopyJob('https://contoso.sharepoint.com/sites/sales', 'https://contoso.sharepoint.com/sites/sales/Icons/Company.png', 'https://contoso.sharepoint.com/sites/marketing/Shared Documents'); assert.deepStrictEqual(postStub.firstCall.args[0].data, { destinationUri: 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', exportObjectUris: ['https://contoso.sharepoint.com/sites/sales/Icons/Company.png'], options: { - NameConflictBehavior: CreateCopyJobsNameConflictBehavior.Fail, + NameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Fail, AllowSchemaMismatch: true, BypassSharedLock: false, IgnoreVersionHistory: false, @@ -2727,7 +2728,7 @@ describe('utils/spo', () => { }); }); - it('correctly creates a copy job with custom options when using createCopyJob', async () => { + it('correctly creates a copy job with custom options when using createFileCopyJob', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/CreateCopyJobs') { return { @@ -2740,12 +2741,12 @@ describe('utils/spo', () => { throw 'Invalid request: ' + opts.url; }); - await spo.createCopyJob( + await spo.createFileCopyJob( 'https://contoso.sharepoint.com/sites/sales', 'https://contoso.sharepoint.com/sites/sales/Icons/Company.png', 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Rename, bypassSharedLock: true, ignoreVersionHistory: true, newName: 'CompanyV2.png', @@ -2756,7 +2757,7 @@ describe('utils/spo', () => { destinationUri: 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', exportObjectUris: ['https://contoso.sharepoint.com/sites/sales/Icons/Company.png'], options: { - NameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename, + NameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Rename, AllowSchemaMismatch: true, BypassSharedLock: true, IgnoreVersionHistory: true, @@ -2768,7 +2769,7 @@ describe('utils/spo', () => { }); }); - it('correctly creates a copy job with custom move options when using createCopyJob', async () => { + it('correctly creates a copy job with custom move options when using createFileCopyJob', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/CreateCopyJobs') { return { @@ -2781,12 +2782,12 @@ describe('utils/spo', () => { throw 'Invalid request: ' + opts.url; }); - await spo.createCopyJob( + await spo.createFileCopyJob( 'https://contoso.sharepoint.com/sites/sales', 'https://contoso.sharepoint.com/sites/sales/Icons/Company.png', 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', { - nameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename, + nameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Rename, bypassSharedLock: true, includeItemPermissions: true, newName: 'CompanyV2.png', @@ -2797,7 +2798,7 @@ describe('utils/spo', () => { destinationUri: 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', exportObjectUris: ['https://contoso.sharepoint.com/sites/sales/Icons/Company.png'], options: { - NameConflictBehavior: CreateCopyJobsNameConflictBehavior.Rename, + NameConflictBehavior: CreateFileCopyJobsNameConflictBehavior.Rename, AllowSchemaMismatch: true, BypassSharedLock: true, IgnoreVersionHistory: false, @@ -2809,6 +2810,122 @@ describe('utils/spo', () => { }); }); + it('correctly outputs result when calling createFolderCopyJob', async () => { + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/CreateCopyJobs') { + return { + value: [ + copyJobInfo + ] + }; + } + + throw 'Invalid request: ' + opts.url; + }); + + const result = await spo.createFolderCopyJob('https://contoso.sharepoint.com/sites/sales', 'https://contoso.sharepoint.com/sites/sales/Icons', 'https://contoso.sharepoint.com/sites/marketing/Shared Documents'); + assert.deepStrictEqual(result, copyJobInfo); + }); + + it('correctly creates a copy job with default options when using createFolderCopyJob', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/CreateCopyJobs') { + return { + value: [ + copyJobInfo + ] + }; + } + + throw 'Invalid request: ' + opts.url; + }); + + await spo.createFolderCopyJob('https://contoso.sharepoint.com/sites/sales', 'https://contoso.sharepoint.com/sites/sales/Icons', 'https://contoso.sharepoint.com/sites/marketing/Shared Documents'); + assert.deepStrictEqual(postStub.firstCall.args[0].data, { + destinationUri: 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', + exportObjectUris: ['https://contoso.sharepoint.com/sites/sales/Icons'], + options: { + NameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Fail, + AllowSchemaMismatch: true, + CustomizedItemName: undefined, + IsMoveMode: false, + SameWebCopyMoveOptimization: true + } + }); + }); + + it('correctly creates a copy job with custom options when using createFolderCopyJob', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/CreateCopyJobs') { + return { + value: [ + copyJobInfo + ] + }; + } + + throw 'Invalid request: ' + opts.url; + }); + + await spo.createFolderCopyJob( + 'https://contoso.sharepoint.com/sites/sales', + 'https://contoso.sharepoint.com/sites/sales/Icons', + 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', + { + nameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Rename, + newName: 'Company icons', + operation: 'copy' + } + ); + assert.deepStrictEqual(postStub.firstCall.args[0].data, { + destinationUri: 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', + exportObjectUris: ['https://contoso.sharepoint.com/sites/sales/Icons'], + options: { + NameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Rename, + AllowSchemaMismatch: true, + IsMoveMode: false, + CustomizedItemName: ['Company icons'], + SameWebCopyMoveOptimization: true + } + }); + }); + + it('correctly creates a copy job with custom move options when using createFolderCopyJob', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/CreateCopyJobs') { + return { + value: [ + copyJobInfo + ] + }; + } + + throw 'Invalid request: ' + opts.url; + }); + + await spo.createFolderCopyJob( + 'https://contoso.sharepoint.com/sites/sales', + 'https://contoso.sharepoint.com/sites/sales/Icons', + 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', + { + nameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Rename, + newName: 'Company icons', + operation: 'move' + } + ); + assert.deepStrictEqual(postStub.firstCall.args[0].data, { + destinationUri: 'https://contoso.sharepoint.com/sites/marketing/Shared Documents', + exportObjectUris: ['https://contoso.sharepoint.com/sites/sales/Icons'], + options: { + NameConflictBehavior: CreateFolderCopyJobsNameConflictBehavior.Rename, + AllowSchemaMismatch: true, + IsMoveMode: true, + CustomizedItemName: ['Company icons'], + SameWebCopyMoveOptimization: true + } + }); + }); + it('correctly polls for copy job status when using getCopyJobResult', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://contoso.sharepoint.com/sites/sales/_api/Site/GetCopyJobProgress') { @@ -2819,37 +2936,45 @@ describe('utils/spo', () => { }; } + if (postStub.callCount === 5) { + return { + JobState: 4, + Logs: [ + JSON.stringify({ + Event: 'JobStart', + JobId: 'fb4cc143-383c-4da0-bd91-02d2acbb01c7', + Time: '08/10/2024 16:30:39.004', + SiteId: '53dec431-9d4f-415b-b12b-010259d5b4e1', + WebId: 'af102f32-b389-49dc-89bf-d116a17e0aa6', + DBId: '5a926054-85d7-4cf6-85f0-c38fa01c4d39', + FarmId: '823af112-cd95-49a2-adf5-eccb09c8ba5d', + ServerId: 'a6145d7e-1b85-4124-895e-b1e618bfe5ae', + SubscriptionId: '18c58817-3bc9-489d-ac63-f7264fb357e5', + TotalRetryCount: '0', + MigrationType: 'Copy', + MigrationDirection: 'Import', + CorrelationId: 'd8f444a1-10a8-9000-862c-0bad6eff1006' + }), + JSON.stringify({ + Event: 'JobFinishedObjectInfo', + JobId: '6d1eda82-0d1c-41eb-ab05-1d9cd4afe786', + Time: '08/10/2024 18:59:40.145', + SourceObjectFullUrl: 'https://contoso.sharepoint.com/sites/marketing/Shared Documents/Icons/Company.png', + TargetServerUrl: 'https://contoso.sharepoint.com', + TargetSiteId: '794dada8-4389-45ce-9559-0de74bf3554a', + TargetWebId: '8de9b4d3-3c30-4fd0-a9d7-2452bd065555', + TargetListId: '44b336a5-e397-4e22-a270-c39e9069b123', + TargetObjectUniqueId: '15488d89-b82b-40be-958a-922b2ed79383', + TargetObjectSiteRelativeUrl: 'Shared Documents/Icons/Company.png', + CorrelationId: '5efd44a1-c034-9000-9692-4e1a1b3ca33b' + }) + ] + }; + } + return { JobState: 0, Logs: [ - JSON.stringify({ - Event: 'JobStart', - JobId: 'fb4cc143-383c-4da0-bd91-02d2acbb01c7', - Time: '08/10/2024 16:30:39.004', - SiteId: '53dec431-9d4f-415b-b12b-010259d5b4e1', - WebId: 'af102f32-b389-49dc-89bf-d116a17e0aa6', - DBId: '5a926054-85d7-4cf6-85f0-c38fa01c4d39', - FarmId: '823af112-cd95-49a2-adf5-eccb09c8ba5d', - ServerId: 'a6145d7e-1b85-4124-895e-b1e618bfe5ae', - SubscriptionId: '18c58817-3bc9-489d-ac63-f7264fb357e5', - TotalRetryCount: '0', - MigrationType: 'Copy', - MigrationDirection: 'Import', - CorrelationId: 'd8f444a1-10a8-9000-862c-0bad6eff1006' - }), - JSON.stringify({ - Event: 'JobFinishedObjectInfo', - JobId: '6d1eda82-0d1c-41eb-ab05-1d9cd4afe786', - Time: '08/10/2024 18:59:40.145', - SourceObjectFullUrl: 'https://contoso.sharepoint.com/sites/marketing/Shared Documents/Icons/Company.png', - TargetServerUrl: 'https://contoso.sharepoint.com', - TargetSiteId: '794dada8-4389-45ce-9559-0de74bf3554a', - TargetWebId: '8de9b4d3-3c30-4fd0-a9d7-2452bd065555', - TargetListId: '44b336a5-e397-4e22-a270-c39e9069b123', - TargetObjectUniqueId: '15488d89-b82b-40be-958a-922b2ed79383', - TargetObjectSiteRelativeUrl: 'Shared Documents/Icons/Company.png', - CorrelationId: '5efd44a1-c034-9000-9692-4e1a1b3ca33b' - }), JSON.stringify({ Event: 'JobEnd', JobId: 'fb4cc143-383c-4da0-bd91-02d2acbb01c7', diff --git a/src/utils/spo.ts b/src/utils/spo.ts index 5950a65adff..2384c73b1dd 100644 --- a/src/utils/spo.ts +++ b/src/utils/spo.ts @@ -22,7 +22,7 @@ import { Group, Site } from '@microsoft/microsoft-graph-types'; import { ListItemInstance } from '../m365/spo/commands/listitem/ListItemInstance.js'; import { ListItemFieldValueResult } from '../m365/spo/commands/listitem/ListItemFieldValueResult.js'; import { FileProperties } from '../m365/spo/commands/file/FileProperties.js'; -import { setTimeout } from 'timers/promises'; +import { timersUtil } from './timersUtil.js'; export interface ContextInfo { FormDigestTimeoutSeconds: number; @@ -89,8 +89,8 @@ export interface User { UserPrincipalName: string | null; } -interface CreateCopyJobsOptions { - nameConflictBehavior?: CreateCopyJobsNameConflictBehavior; +interface CreateFileCopyJobsOptions { + nameConflictBehavior?: CreateFileCopyJobsNameConflictBehavior; newName?: string; bypassSharedLock?: boolean; /** @remarks Use only when using operation copy. */ @@ -100,12 +100,23 @@ interface CreateCopyJobsOptions { operation: 'copy' | 'move'; } -export enum CreateCopyJobsNameConflictBehavior { +interface CreateFolderCopyJobsOptions { + nameConflictBehavior?: CreateFolderCopyJobsNameConflictBehavior; + newName?: string; + operation: 'copy' | 'move'; +} + +export enum CreateFileCopyJobsNameConflictBehavior { Fail = 0, Replace = 1, Rename = 2, } +export enum CreateFolderCopyJobsNameConflictBehavior { + Fail = 0, + Rename = 2, +} + interface CreateCopyJobInfo { EncryptionKey: string; JobId: string; @@ -128,9 +139,7 @@ interface CopyJobObjectInfo { } // Wrapping this into a settings object so we can alter the values in tests -export const settings = { - pollingInterval: 3_000 -}; +const pollingInterval = 3_000; export const spo = { async getRequestDigest(siteUrl: string): Promise { @@ -198,7 +207,7 @@ export const spo = { return; } - await setTimeout(operation.PollingInterval); + await timersUtil.setTimeout(pollingInterval); await spo.waitUntilFinished({ operationId: JSON.stringify(operation._ObjectIdentity_), siteUrl, @@ -926,7 +935,7 @@ export const spo = { return; } - await setTimeout(operation.PollingInterval); + await timersUtil.setTimeout(pollingInterval); await spo.waitUntilFinished({ operationId: JSON.stringify(operation._ObjectIdentity_), siteUrl: spoAdminUrl, @@ -1173,7 +1182,7 @@ export const spo = { return; } - await setTimeout(operation.PollingInterval); + await timersUtil.setTimeout(pollingInterval); await spo.waitUntilFinished({ operationId: JSON.stringify(operation._ObjectIdentity_), siteUrl: spoAdminUrl, @@ -1345,7 +1354,7 @@ export const spo = { const operation: SpoOperation = json[json.length - 1]; const isComplete: boolean = operation.IsComplete; if (!isComplete) { - await setTimeout(operation.PollingInterval); + await timersUtil.setTimeout(pollingInterval); await spo.waitUntilFinished({ operationId: JSON.stringify(operation._ObjectIdentity_), siteUrl: spoAdminUrl, @@ -1935,14 +1944,14 @@ export const spo = { }, /** - * Create a SharePoint copy job to copy a file/folder to another location. - * @param webUrl Absolute web URL where the source file/folder is located. - * @param sourceUrl Absolute URL of the source file/folder. + * Create a SharePoint copy job to copy a file to another location. + * @param webUrl Absolute web URL where the source file is located. + * @param sourceUrl Absolute URL of the source file. * @param destinationUrl Absolute URL of the destination folder. * @param options Options for the copy job. * @returns Copy job information. Use {@link spo.getCopyJobResult} to get the result of the copy job. */ - async createCopyJob(webUrl: string, sourceUrl: string, destinationUrl: string, options?: CreateCopyJobsOptions): Promise { + async createFileCopyJob(webUrl: string, sourceUrl: string, destinationUrl: string, options?: CreateFileCopyJobsOptions): Promise { const requestOptions: CliRequestOptions = { url: `${webUrl}/_api/Site/CreateCopyJobs`, headers: { @@ -1953,7 +1962,7 @@ export const spo = { destinationUri: destinationUrl, exportObjectUris: [sourceUrl], options: { - NameConflictBehavior: options?.nameConflictBehavior ?? CreateCopyJobsNameConflictBehavior.Fail, + NameConflictBehavior: options?.nameConflictBehavior ?? CreateFileCopyJobsNameConflictBehavior.Fail, AllowSchemaMismatch: true, BypassSharedLock: !!options?.bypassSharedLock, IgnoreVersionHistory: !!options?.ignoreVersionHistory, @@ -1969,6 +1978,38 @@ export const spo = { return response.value[0]; }, + /** + * Create a SharePoint copy job to copy a folder to another location. + * @param webUrl Absolute web URL where the source folder is located. + * @param sourceUrl Absolute URL of the source folder. + * @param destinationUrl Absolute URL of the destination folder. + * @param options Options for the copy job. + * @returns Copy job information. Use {@link spo.getCopyJobResult} to get the result of the copy job. + */ + async createFolderCopyJob(webUrl: string, sourceUrl: string, destinationUrl: string, options?: CreateFolderCopyJobsOptions): Promise { + const requestOptions: CliRequestOptions = { + url: `${webUrl}/_api/Site/CreateCopyJobs`, + headers: { + accept: 'application/json;odata=nometadata' + }, + responseType: 'json', + data: { + destinationUri: destinationUrl, + exportObjectUris: [sourceUrl], + options: { + NameConflictBehavior: options?.nameConflictBehavior ?? CreateFolderCopyJobsNameConflictBehavior.Fail, + AllowSchemaMismatch: true, + CustomizedItemName: options?.newName ? [options.newName] : undefined, + SameWebCopyMoveOptimization: true, + IsMoveMode: options?.operation === 'move' + } + } + }; + + const response = await request.post<{ value: CreateCopyJobInfo[] }>(requestOptions); + return response.value[0]; + }, + /** * Poll until the copy job is finished and return the result. * @param webUrl Absolute web URL where the copy job was created. @@ -1987,14 +2028,23 @@ export const spo = { copyJobInfo: copyJobInfo } }; + + const logs = []; let progress = await request.post<{ JobState: number; Logs: string[] }>(requestOptions); + const newLogs = progress.Logs?.map(l => JSON.parse(l)); + if (newLogs?.length > 0) { + logs.push(...newLogs); + } while (progress.JobState !== 0) { - await setTimeout(settings.pollingInterval); + await timersUtil.setTimeout(pollingInterval); progress = await request.post<{ JobState: number; Logs: string[] }>(requestOptions); - } - const logs = progress.Logs.map(l => JSON.parse(l)); + const newLogs = progress.Logs?.map(l => JSON.parse(l)); + if (newLogs?.length > 0) { + logs.push(...newLogs); + } + } // Check if the job has failed const errorLog = logs.find(l => l.Event === 'JobError'); diff --git a/src/utils/timersUtil.ts b/src/utils/timersUtil.ts index eb7c9593f44..da320272e1a 100644 --- a/src/utils/timersUtil.ts +++ b/src/utils/timersUtil.ts @@ -5,7 +5,8 @@ export const timersUtil = { * Timeout for a specific duration. * @param duration Duration in milliseconds. */ - /* c8 ignore next 3 */ + /* c8 ignore next 4 */ + // Function is created so we can easily mock it in our tests async setTimeout(duration: number): Promise { return setTimeout(duration); } From 7c07bbfe7380579a3099a3feabd9cc09a57b86f0 Mon Sep 17 00:00:00 2001 From: Nico De Cleyre Date: Mon, 26 Aug 2024 18:01:36 +0200 Subject: [PATCH 29/43] Enhance `entra m365group user set` command. Closes #6224 --- .../cmd/entra/m365group/m365group-user-set.mdx | 11 ++++------- docs/docs/v10-upgrade-guidance.mdx | 10 +++++++++- .../commands/m365group/m365group-user-set.spec.ts | 2 +- .../commands/m365group/m365group-user-set.ts | 15 +++------------ 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/docs/docs/cmd/entra/m365group/m365group-user-set.mdx b/docs/docs/cmd/entra/m365group/m365group-user-set.mdx index f2ac7cc1451..b5b60faf6b6 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-set.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-set.mdx @@ -25,14 +25,11 @@ m365 entra m365group user set [options] `--teamName [teamName]` : The display name of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. -`-n, --userName [userName]` -: (deprecated) User's UPN (User Principal Name), e.g. johndoe@example.com. - `--ids [ids]` -: Microsoft Entra IDs of users. You can also pass a comma-separated list of IDs. Specify only one of the following `userName`, `ids` or `userNames`. +: Microsoft Entra IDs of users. You can also pass a comma-separated list of IDs. Specify only one of the following `ids` or `userNames`. `--userNames [userNames]` -: The user principal names of users. You can also pass a comma-separated list of UPNs. Specify only one of the following `userName`, `ids` or `userNames`. +: The user principal names of users. You can also pass a comma-separated list of UPNs. Specify only one of the following `ids` or `userNames`. `-r, --role ` : Role to set for the given user in the specified Microsoft 365 Group or Microsoft Teams team. Allowed values: `Owner`, `Member` @@ -49,7 +46,7 @@ The command will return an error if the user already has the specified role in t Promote a single user to Owner of the given Microsoft 365 Group ```sh -m365 entra m365group user set --groupId '00000000-0000-0000-0000-000000000000' --userName 'anne.matthews@contoso.onmicrosoft.com' --role Owner +m365 entra m365group user set --groupId '00000000-0000-0000-0000-000000000000' --userNames 'anne.matthews@contoso.onmicrosoft.com' --role Owner ``` Promote multiple users specified by the userNames parameter to Owner of the given Microsoft 365 Group @@ -67,7 +64,7 @@ m365 entra m365group user set --groupId '00000000-0000-0000-0000-000000000000' - Demote a single user from Owner to Member in the given Microsoft 365 Group ```sh -m365 entra m365group user set --groupId '00000000-0000-0000-0000-000000000000' --userName 'anne.matthews@contoso.onmicrosoft.com' --role Member +m365 entra m365group user set --groupId '00000000-0000-0000-0000-000000000000' --userNames 'anne.matthews@contoso.onmicrosoft.com' --role Member ``` Demote multiple users specified by the userNames parameter from Owner to Member of the given Microsoft Teams team diff --git a/docs/docs/v10-upgrade-guidance.mdx b/docs/docs/v10-upgrade-guidance.mdx index 1402e97ad85..a157e7c0367 100644 --- a/docs/docs/v10-upgrade-guidance.mdx +++ b/docs/docs/v10-upgrade-guidance.mdx @@ -68,7 +68,7 @@ The deprecated Guest value was removed from the `role` option in the [aad m365gr #### What action do I need to take? -Please, check the documentation of the aad m365group user list](./cmd/entra/m365group/m365group-user-list.mdx) command to see the updated ``role` option and adjust your scripts accordingly. +Please, check the documentation of the [aad m365group user list](./cmd/entra/m365group/m365group-user-list.mdx) command to see the updated ``role` option and adjust your scripts accordingly. #### Aligned options with naming convention @@ -149,6 +149,14 @@ As part of the renaming of Azure AD to Microsoft Entra ID, we have removed the ` Please, check if your scripts use any of the `aad` commands and if so update it to `entra` commands. +### Removed deprecated option `username` from `entra m365group user set` command. + +The deprecated option `username` was removed from the [entra m365group user set](./cmd/entra/m365group/m365group-user-set.mdx) command. + +#### What action do I need to take? + +Please, check the documentation of the [entra m365group user set](./cmd/entra/m365group/m365group-user-set.mdx) command to see the updated `username` option and adjust your scripts accordingly. + ## SharePoint ### Updated `spo site appcatalog remove` options diff --git a/src/m365/entra/commands/m365group/m365group-user-set.spec.ts b/src/m365/entra/commands/m365group/m365group-user-set.spec.ts index 0a2912c336d..75377fe0d57 100644 --- a/src/m365/entra/commands/m365group/m365group-user-set.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-set.spec.ts @@ -506,7 +506,7 @@ describe(commands.M365GROUP_USER_SET, () => { sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { groupId: groupId, userName: 'anne.matthews@contoso.onmicrosoft.com' } } as any), + await assert.rejects(command.action(logger, { options: { groupId: groupId, userNames: userUpns.join(',') } } as any), new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-user-set.ts b/src/m365/entra/commands/m365group/m365group-user-set.ts index be1d9e0d51f..063876dec42 100644 --- a/src/m365/entra/commands/m365group/m365group-user-set.ts +++ b/src/m365/entra/commands/m365group/m365group-user-set.ts @@ -13,7 +13,6 @@ interface CommandArgs { } interface Options extends GlobalOptions { - userName?: string; ids?: string; userNames?: string; groupId?: string; @@ -59,9 +58,6 @@ class EntraM365GroupUserSetCommand extends GraphCommand { #initOptions(): void { this.options.unshift( - { - option: '-n, --userName [userName]' - }, { option: '--ids [ids]' }, @@ -123,20 +119,15 @@ class EntraM365GroupUserSetCommand extends GraphCommand { #initOptionSets(): void { this.optionSets.push({ options: ['groupId', 'groupName', 'teamId', 'teamName'] }); - this.optionSets.push({ options: ['userName', 'ids', 'userNames'] }); + this.optionSets.push({ options: ['ids', 'userNames'] }); } #initTypes(): void { - this.types.string.push('userName', 'ids', 'userNames', 'groupId', 'groupName', 'teamId', 'teamName', 'role'); + this.types.string.push('ids', 'userNames', 'groupId', 'groupName', 'teamId', 'teamName', 'role'); } public async commandAction(logger: Logger, args: CommandArgs): Promise { - if (args.options.userName) { - await this.warn(logger, `Option 'userName' is deprecated. Please use 'ids' or 'userNames' instead.`); - } - try { - const userNames = args.options.userNames || args.options.userName; const groupId: string = await this.getGroupId(logger, args); const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId); @@ -144,7 +135,7 @@ class EntraM365GroupUserSetCommand extends GraphCommand { throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } - const userIds: string[] = await this.getUserIds(logger, args.options.ids, userNames); + const userIds: string[] = await this.getUserIds(logger, args.options.ids, args.options.userNames); // we can't simply switch the role // first add users to the new role From 55ec8843c26e0e73ecbcee7fc20b96b718f9e319 Mon Sep 17 00:00:00 2001 From: Mathijs Verbeeck Date: Wed, 16 Oct 2024 11:45:31 +0200 Subject: [PATCH 30/43] Updates release notes --- docs/docs/about/release-notes.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/docs/about/release-notes.mdx b/docs/docs/about/release-notes.mdx index 5f65c7d2e22..ce57edf29c4 100644 --- a/docs/docs/about/release-notes.mdx +++ b/docs/docs/about/release-notes.mdx @@ -14,6 +14,7 @@ sidebar_position: 3 - updated docs contribution guide with Zod [#6322](https://github.com/pnp/cli-microsoft365/issues/6322) - removed obsolete example [#6272](https://github.com/pnp/cli-microsoft365/issues/6272) - fixed 'm365 setup' app registration to use 'CLI for M365' [#6367](https://github.com/pnp/cli-microsoft365/issues/6367) +- fixed example in 'spo site sharingpermission set' ### ⚠️ Breaking changes @@ -46,6 +47,8 @@ sidebar_position: 3 - renamed `entra group user ` to `entra group member `. [#6396](https://github.com/pnp/cli-microsoft365/issues/6396) - updated setting users in `entra m365group set` [#6061](https://github.com/pnp/cli-microsoft365/issues/6061) - removed aad options and aliasses [#5823](https://github.com/pnp/cli-microsoft365/issues/5823), [#5676](https://github.com/pnp/cli-microsoft365/issues/5676) +- removed deprecated option `username` from `entra m365group user set` command [#6224](https://github.com/pnp/cli-microsoft365/issues/6224) +- updated endpoint for `spo folder move` command [#6154](https://github.com/pnp/cli-microsoft365/issues/6154) ## [v9.1.0](https://github.com/pnp/cli-microsoft365/releases/tag/v9.1.0) From 47ed12a831077c76b62cfa08bf95b6951ed58ba2 Mon Sep 17 00:00:00 2001 From: Mathijs Verbeeck Date: Mon, 16 Sep 2024 11:50:04 +0200 Subject: [PATCH 31/43] Enhances 'entra m365group user remove' command. Closes #6058 --- .../entra/m365group/m365group-user-remove.mdx | 36 +- .../m365group/m365group-user-remove.spec.ts | 430 +++++++----------- .../m365group/m365group-user-remove.ts | 178 +++++--- 3 files changed, 295 insertions(+), 349 deletions(-) diff --git a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx index e560924f197..930fc5df09f 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx @@ -14,13 +14,25 @@ m365 entra m365group user remove [options] ```md definition-list `-i, --groupId [groupId]` -: The ID of the Microsoft 365 Group from which to remove the user. +: The ID of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. + +`--groupName [groupName]` +: The display name of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`.. `--teamId [teamId]` -: The ID of the Microsoft Teams team from which to remove the user. +: The ID of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. + +`--teamName [teamName]` +: The display name of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. + +`-n, --userName [userName]` +: (deprecated) User's UPN (user principal name), eg. `johndoe@example.com`.. Specify only one of the following: `userName`, `ids` or `userNames`. + +`--ids [ids]` +: Microsoft Entra IDs of users. You can also pass a comma-separated list of IDs. Specify only one of the following `userName`, `ids` or `userNames`. -`-n, --userName ` -: User's UPN (user principal name), eg. `johndoe@example.com`. +`--userNames [userNames]` +: The user principal names of users. You can also pass a comma-separated list of UPNs. Specify only one of the following `userName`, `ids` or `userNames`. `-f, --force` : Don't prompt for confirming removing the user from the specified Microsoft 365 Group or Microsoft Teams team. @@ -37,19 +49,25 @@ You can remove users from a Microsoft 365 Group or Microsoft Teams team if you a Removes user from the specified Microsoft 365 Group. ```sh -m365 entra m365group user remove --groupId '00000000-0000-0000-0000-000000000000' --userName 'anne.matthews@contoso.onmicrosoft.com' +m365 entra m365group user remove --groupId '5b8e4cb1-ea40-484b-a94e-02a4313fefb4' --userNames 'anne.matthews@contoso.onmicrosoft.com' +``` + +Removes user from the specified Microsoft 365 Team specified by id without confirmation. + +```sh +m365 entra m365group user remove --teamId '5b8e4cb1-ea40-484b-a94e-02a4313fefb4' --userNames 'anne.matthews@contoso.onmicrosoft.com' --force ``` -Removes user from the specified Microsoft 365 Group without confirmation. +Removes users specified by a comma separated list of user principal names from the specified Microsoft Teams team specified by name ```sh -m365 entra m365group user remove --groupId '00000000-0000-0000-0000-000000000000' --userName 'anne.matthews@contoso.onmicrosoft.com' --force +m365 entra m365group user remove --teamName 'Project Team' --userNames 'anne.matthews@contoso.onmicrosoft.com,john@contoso.com' ``` -Removes user from the specified Microsoft Teams team. +Removes users specified by a comma separated list of user ids from the specified Microsoft 365 group specified by name. ```sh -m365 entra teams user remove --teamId '00000000-0000-0000-0000-000000000000' --userName 'anne.matthews@contoso.onmicrosoft.com' +m365 entra m365group user remove --groupName 'Project Team' --ids '5b8e4cb1-ea40-484b-a94e-02a4313fefb4,be7a56d8-b045-4938-af35-917ab6e5309f' ``` ## Response diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts index 6b2b25dfd02..5c9345823fd 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts @@ -12,10 +12,15 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './m365group-user-remove.js'; -import { settingsNames } from '../../../../settingsNames.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; +import { entraUser } from '../../../../utils/entraUser.js'; describe(commands.M365GROUP_USER_REMOVE, () => { + const userName = 'adelev@contoso.com'; + const groupOrTeamId = '80ecb711-2501-4262-b29a-838d30bd3387'; + const userId = '8b38aeff-1642-47e4-b6ef-9d50d29638b7'; + const groupOrTeamName = 'Project Team'; + let log: string[]; let logger: Logger; let commandInfo: CommandInfo; @@ -26,7 +31,6 @@ describe(commands.M365GROUP_USER_REMOVE, () => { sinon.stub(telemetry, 'trackEvent').returns(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); - sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); @@ -44,20 +48,24 @@ describe(commands.M365GROUP_USER_REMOVE, () => { log.push(msg); } }; - sinon.stub(cli, 'promptForConfirmation').callsFake(() => { + sinon.stub(cli, 'promptForConfirmation').callsFake(async () => { promptIssued = true; - return Promise.resolve(false); + return false; }); promptIssued = false; + sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); + sinon.stub(entraUser, 'getUserIdsByUpns').resolves([userId]); }); afterEach(() => { sinonUtil.restore([ request.get, request.delete, - global.setTimeout, cli.promptForConfirmation, - cli.getSettingWithDefaultValue + cli.getSettingWithDefaultValue, + entraUser.getUserIdsByUpns, + entraGroup.isUnifiedGroup, + entraGroup.getGroupIdByDisplayName ]); }); @@ -74,80 +82,95 @@ describe(commands.M365GROUP_USER_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if the groupId is not a valid guid.', async () => { + it('fails validation if userName is not a valid upn', async () => { const actual = await command.validate({ options: { - groupId: 'not-c49b-4fd4-8223-28f0ac3a6402', - userName: 'anne.matthews@contoso.onmicrosoft.com' + groupId: groupOrTeamId, + userName: 'invalid' } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation if the teamId is not a valid guid.', async () => { + it('fails validation if the groupId is not a valid guid', async () => { const actual = await command.validate({ options: { - teamId: 'not-c49b-4fd4-8223-28f0ac3a6402', - userName: 'anne.matthews@contoso.onmicrosoft.com' + groupId: 'invalid', + userName: userName } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation if neither the groupId nor the teamID are provided.', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; + it('fails validation if the teamId is not a valid guid', async () => { + const actual = await command.validate({ + options: { + teamId: 'invalid', + userName: userName } + }, commandInfo); + assert.notStrictEqual(actual, true); + }); - return defaultValue; - }); - + it('fails validation if ids contain an invalid guid', async () => { const actual = await command.validate({ options: { - userName: 'anne.matthews@contoso.onmicrosoft.com' + teamId: groupOrTeamId, + ids: `invalid,${userId}` } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation when both groupId and teamId are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; + it('fails validation if userNames contain an invalid upn', async () => { + const actual = await command.validate({ + options: { + teamId: groupOrTeamId, + userNames: `invalid,${userName}` } + }, commandInfo); + assert.notStrictEqual(actual, true); + }); - return defaultValue; - }); + it('passes validation when a valid teamId and userName are specified', async () => { + const actual = await command.validate({ + options: { + teamId: groupOrTeamId, + userName: userName + } + }, commandInfo); + assert.strictEqual(actual, true); + }); + it('passes validation when a valid teamId and userNames are specified', async () => { const actual = await command.validate({ options: { - groupId: '6703ac8a-c49b-4fd4-8223-28f0ac3a6402', - teamId: '6703ac8a-c49b-4fd4-8223-28f0ac3a6402', - userName: 'anne.matthews@contoso.onmicrosoft.com' + teamId: groupOrTeamId, + userNames: `${userName},john@contoso.com` } }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual, true); }); - it('passes validation when valid groupId and userName are specified', async () => { + it('passes validation when a valid teamId and ids are specified', async () => { const actual = await command.validate({ options: { - teamId: '6703ac8a-c49b-4fd4-8223-28f0ac3a6402', - userName: 'anne.matthews@contoso.onmicrosoft.com' + teamId: groupOrTeamId, + ids: `${userId},8b38aeff-1642-47e4-b6ef-9d50d29638b7` } }, commandInfo); assert.strictEqual(actual, true); }); + it('prompts before removing the specified user from the specified Microsoft 365 Group when force option not passed', async () => { - await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com" } }); + await command.action(logger, { options: { groupId: groupOrTeamId, userName: userName } }); assert(promptIssued); }); it('prompts before removing the specified user from the specified Team when force option not passed (debug)', async () => { - await command.action(logger, { options: { debug: true, teamId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com" } }); + await command.action(logger, { options: { debug: true, teamId: "00000000-0000-0000-0000-000000000000", userName: userName } }); assert(promptIssued); }); @@ -157,7 +180,7 @@ describe(commands.M365GROUP_USER_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(false); - await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com" } }); + await command.action(logger, { options: { groupId: groupOrTeamId, userName: userName } }); assert(postSpy.notCalled); }); @@ -166,47 +189,22 @@ describe(commands.M365GROUP_USER_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(false); - await command.action(logger, { options: { debug: true, groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com" } }); + await command.action(logger, { options: { debug: true, groupId: groupOrTeamId, userName: userName } }); assert(postSpy.notCalled); }); it('removes the specified owner from owners and members endpoint of the specified Microsoft 365 Group with accepted prompt', async () => { let memberDeleteCallIssued = false; - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - return { - response: { - status: 200, - data: { - value: "00000000-0000-0000-0000-000000000000" - } - } - }; - } - - throw 'Invalid request'; - }); - sinon.stub(request, 'delete').callsFake(async (opts) => { memberDeleteCallIssued = true; - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { - return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] - }; + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { + return; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000001/$ref`) { - return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] - }; + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/members/${userId}/$ref`) { + return; } throw 'Invalid request'; @@ -215,87 +213,39 @@ describe(commands.M365GROUP_USER_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com" } }); + await command.action(logger, { options: { groupId: groupOrTeamId, userName: userName } }); assert(memberDeleteCallIssued); }); it('removes the specified owner from owners and members endpoint of the specified Microsoft 365 Group when prompt confirmed', async () => { let memberDeleteCallIssued = false; - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - return { - response: { - status: 200, - data: { - value: "00000000-0000-0000-0000-000000000000" - } - } - }; - } - - throw 'Invalid request'; - }); - - sinon.stub(request, 'delete').callsFake(async (opts) => { memberDeleteCallIssued = true; - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { - return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] - }; + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { + return; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000001/$ref`) { - return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] - }; + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/members/${userId}/$ref`) { + return; } throw 'Invalid request'; }); - await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com", force: true } }); + await command.action(logger, { options: { groupId: groupOrTeamId, userName: userName, force: true } }); assert(memberDeleteCallIssued); }); it('removes the specified member from members endpoint of the specified Microsoft 365 Group when prompt confirmed', async () => { let memberDeleteCallIssued = false; - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - return { - response: { - status: 200, - data: { - value: "00000000-0000-0000-0000-000000000000" - } - } - }; - } - - throw 'Invalid request'; - }); - - sinon.stub(request, 'delete').callsFake(async (opts) => { memberDeleteCallIssued = true; - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { throw { "response": { "status": 404 @@ -303,37 +253,30 @@ describe(commands.M365GROUP_USER_REMOVE, () => { }; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000001/$ref`) { - return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] - }; + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/members/${userId}/$ref`) { + return; } throw 'Invalid request'; }); - await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com", force: true } }); + await command.action(logger, { options: { groupId: groupOrTeamId, userName: userName, force: true } }); assert(memberDeleteCallIssued); }); - it('removes the specified owners from owners endpoint of the specified Microsoft 365 Group when prompt confirmed', async () => { - let memberDeleteCallIssued = false; + it('removes the specified members of the specified Microsoft 365 Group specified by teamName', async () => { + sinon.stub(entraGroup, 'getGroupIdByDisplayName').withArgs(groupOrTeamName).resolves(groupOrTeamId); - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; + const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { + return; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - return { - response: { - status: 200, - data: { - value: "00000000-0000-0000-0000-000000000000" - } + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/members/${userId}/$ref`) { + throw { + "response": { + "status": 404 } }; } @@ -341,17 +284,22 @@ describe(commands.M365GROUP_USER_REMOVE, () => { throw 'Invalid request'; }); + sinonUtil.restore(cli.promptForConfirmation); + sinon.stub(cli, 'promptForConfirmation').resolves(true); - sinon.stub(request, 'delete').callsFake(async (opts) => { - memberDeleteCallIssued = true; + await command.action(logger, { options: { teamName: groupOrTeamName, userName: userName, verbose: true } }); + assert(deleteStub.calledTwice); + }); - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { - return { - "value": [{ "id": "00000000-0000-0000-0000-000000000000" }] - }; + it('removes the specified members specified by ids of the specified Microsoft 365 Team specified by teamId', async () => { + sinon.stub(entraGroup, 'getGroupIdByDisplayName').withArgs(groupOrTeamName).resolves(groupOrTeamId); + + const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { + return; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000001/$ref`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/members/${userId}/$ref`) { throw { "response": { "status": 404 @@ -363,39 +311,44 @@ describe(commands.M365GROUP_USER_REMOVE, () => { }); - await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com", force: true } }); - assert(memberDeleteCallIssued); + sinonUtil.restore(cli.promptForConfirmation); + sinon.stub(cli, 'promptForConfirmation').resolves(true); + + await command.action(logger, { options: { teamId: groupOrTeamId, ids: userId, verbose: true } }); + assert(deleteStub.calledTwice); }); - it('does not fail if the user is not owner or member of the specified Microsoft 365 Group when prompt confirmed', async () => { - let memberDeleteCallIssued = false; - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; + it('removes the specified members of the specified Microsoft 365 Group specified by groupName', async () => { + sinon.stub(entraGroup, 'getGroupIdByDisplayName').withArgs(groupOrTeamName).resolves(groupOrTeamId); + + const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { + return; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - return { - response: { - status: 200, - data: { - value: "00000000-0000-0000-0000-000000000000" - } - } - }; + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/members/${userId}/$ref`) { + return; } throw 'Invalid request'; + }); + sinonUtil.restore(cli.promptForConfirmation); + sinon.stub(cli, 'promptForConfirmation').resolves(true); + + await command.action(logger, { options: { teamName: groupOrTeamName, userNames: userName } }); + assert(deleteStub.calledTwice); + }); + + it('does not fail if the user is not owner or member of the specified Microsoft 365 Group when prompt confirmed', async () => { + let memberDeleteCallIssued = false; sinon.stub(request, 'delete').callsFake(async (opts) => { memberDeleteCallIssued = true; - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { return { "response": { "status": 404 @@ -403,7 +356,7 @@ describe(commands.M365GROUP_USER_REMOVE, () => { }; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000001/$ref`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/members/${userId}/$ref`) { throw { "response": { "status": 404 @@ -416,40 +369,18 @@ describe(commands.M365GROUP_USER_REMOVE, () => { }); - await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com", force: true } }); + await command.action(logger, { options: { groupId: groupOrTeamId, userName: userName, force: true } }); assert(memberDeleteCallIssued); }); it('stops removal if an unknown error message is thrown when deleting the owner', async () => { let memberDeleteCallIssued = false; - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - return { - response: { - status: 200, - data: { - value: "00000000-0000-0000-0000-000000000000" - } - } - }; - } - - throw 'Invalid request'; - }); - - sinon.stub(request, 'delete').callsFake(async (opts) => { memberDeleteCallIssued = true; // for example... you must have at least one owner - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { return { "response": { "status": 400 @@ -457,7 +388,7 @@ describe(commands.M365GROUP_USER_REMOVE, () => { }; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000001/$ref`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/members/${userId}/$ref`) { throw { "response": { "status": 404 @@ -469,55 +400,38 @@ describe(commands.M365GROUP_USER_REMOVE, () => { }); - await command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com", force: true } }); + await command.action(logger, { options: { groupId: groupOrTeamId, userName: userName, force: true } }); assert(memberDeleteCallIssued); }); it('correctly retrieves user but does not find the Group Microsoft 365 group', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - throw "Invalid object identifier"; - } - - throw 'Invalid request'; - }); + const errorMessage = `Resource '${groupOrTeamId}' does not exist or one of its queried reference-property objects are not present.`; sinonUtil.restore(cli.promptForConfirmation); - sinon.stub(cli, 'promptForConfirmation').resolves(true); - await assert.rejects(command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com" } } as any), - new CommandError('Invalid object identifier')); - }); - - it('correctly retrieves user and handle error removing owner from specified Microsoft 365 group', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; - } + sinonUtil.restore(entraGroup.isUnifiedGroup); - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - return { - response: { - status: 200, - data: { - value: "00000000-0000-0000-0000-000000000000" - } + sinon.stub(entraGroup, 'isUnifiedGroup').rejects( + { + error: { + code: 'Request_ResourceNotFound', + message: errorMessage, + innerError: { + date: '2024-09-16T22:06:30', + 'request-id': 'c43610b0-70c0-4c00-8c40-ff26b5f37f00', + 'client-request-id': 'c43610b0-70c0-4c00-8c40-ff26b5f37f00' } - }; + } } + ); + sinon.stub(cli, 'promptForConfirmation').resolves(true); - throw 'Invalid request'; - }); + await assert.rejects(command.action(logger, { options: { groupId: groupOrTeamId, userName: userName } } as any), + new CommandError(errorMessage)); + }); + it('correctly retrieves user and handle error removing owner from specified Microsoft 365 group', async () => { sinon.stub(request, 'delete').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { throw { response: { status: 400, @@ -534,44 +448,23 @@ describe(commands.M365GROUP_USER_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await assert.rejects(command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com" } } as any), + await assert.rejects(command.action(logger, { options: { groupId: groupOrTeamId, userName: userName } } as any), new CommandError('Invalid object identifier')); }); it('correctly retrieves user and handle error removing member from specified Microsoft 365 group', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews%40contoso.onmicrosoft.com/id`) { - return { - "value": "00000000-0000-0000-0000-000000000001" - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/id`) { - return { - response: { - status: 200, - data: { - value: "00000000-0000-0000-0000-000000000000" - } - } - }; - } - - throw 'Invalid request'; - }); - sinon.stub(request, 'delete').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/owners/00000000-0000-0000-0000-000000000001/$ref`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/owners/${userId}/$ref`) { return { - "err": { - "response": { - "status": 404 + err: { + response: { + status: 404 } } }; } - if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/members/00000000-0000-0000-0000-000000000001/$ref`) { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupOrTeamId}/members/${userId}/$ref`) { throw { response: { status: 400, @@ -588,34 +481,15 @@ describe(commands.M365GROUP_USER_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await assert.rejects(command.action(logger, { options: { groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com" } } as any), + await assert.rejects(command.action(logger, { options: { groupId: groupOrTeamId, userName: userName } } as any), new CommandError('Invalid object identifier')); }); - it('correctly skips execution when specified user is not found', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/users/anne.matthews.not.found%40contoso.onmicrosoft.com/id`) { - throw "Resource 'anne.matthews.not.found%40contoso.onmicrosoft.com' does not exist or one of its queried reference-property objects are not present."; - } - - throw 'Invalid request'; - }); - - sinon.stub(request, 'delete').resolves(); - - sinonUtil.restore(cli.promptForConfirmation); - sinon.stub(cli, 'promptForConfirmation').resolves(true); - - await assert.rejects(command.action(logger, { options: { debug: true, groupId: "00000000-0000-0000-0000-000000000000", userName: "anne.matthews@contoso.onmicrosoft.com" } } as any), new CommandError("Invalid request")); - }); - it('throws error when the group is not a unified group', async () => { - const groupId = '3f04e370-cbc6-4091-80fe-1d038be2ad06'; - sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { groupId: groupId, userName: 'anne.matthews@contoso.onmicrosoft.com', force: true } } as any), - new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); + await assert.rejects(command.action(logger, { options: { groupId: groupOrTeamId, userName: 'anne.matthews@contoso.onmicrosoft.com', force: true } } as any), + new CommandError(`Specified group with id '${groupOrTeamId}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.ts b/src/m365/entra/commands/m365group/m365group-user-remove.ts index 71d14fd1852..380cc76c50e 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.ts @@ -3,6 +3,7 @@ import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request from '../../../../request.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; +import { entraUser } from '../../../../utils/entraUser.js'; import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; @@ -12,14 +13,14 @@ interface CommandArgs { options: Options; } -interface UserResponse { - value: string -} - interface Options extends GlobalOptions { teamId?: string; + teamName?: string; groupId?: string; - userName: string; + groupName?: string; + userName?: string; + ids?: string; + userNames?: string; force?: boolean; } @@ -39,14 +40,20 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { this.#initOptions(); this.#initValidators(); this.#initOptionSets(); + this.#initTypes(); } #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { - force: (!(!args.options.force)).toString(), + force: !!args.options.force, teamId: typeof args.options.teamId !== 'undefined', - groupId: typeof args.options.groupId !== 'undefined' + groupId: typeof args.options.groupId !== 'undefined', + teamName: typeof args.options.teamName !== 'undefined', + groupName: typeof args.options.groupName !== 'undefined', + userName: typeof args.options.userName !== 'undefined', + ids: typeof args.options.ids !== 'undefined', + userNames: typeof args.options.userNames !== 'undefined' }); }); } @@ -54,13 +61,25 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { #initOptions(): void { this.options.unshift( { - option: "-i, --groupId [groupId]" + option: '-i, --groupId [groupId]' + }, + { + option: '--groupName [groupName]' + }, + { + option: '--teamId [teamId]' + }, + { + option: '--teamName [teamName]' }, { - option: "--teamId [teamId]" + option: '-n, --userName [userName]' }, { - option: '-n, --userName ' + option: '--ids [ids]' + }, + { + option: '--userNames [userNames]' }, { option: '-f, --force' @@ -72,11 +91,29 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { this.validators.push( async (args: CommandArgs) => { if (args.options.teamId && !validation.isValidGuid(args.options.teamId as string)) { - return `${args.options.teamId} is not a valid GUID`; + return `${args.options.teamId} is not a valid GUID for option 'teamId'.`; } if (args.options.groupId && !validation.isValidGuid(args.options.groupId as string)) { - return `${args.options.groupId} is not a valid GUID`; + return `${args.options.groupId} is not a valid GUID for option 'groupId'.`; + } + + if (args.options.ids) { + const isValidGUIDArrayResult = validation.isValidGuidArray(args.options.ids); + if (isValidGUIDArrayResult !== true) { + return `The following GUIDs are invalid for the option 'ids': ${isValidGUIDArrayResult}.`; + } + } + + if (args.options.userNames) { + const isValidUPNArrayResult = validation.isValidUserPrincipalNameArray(args.options.userNames); + if (isValidUPNArrayResult !== true) { + return `The following user principal names are invalid for the option 'userNames': ${isValidUPNArrayResult}.`; + } + } + + if (args.options.userName && !validation.isValidUserPrincipalName(args.options.userName)) { + return `The specified userName '${args.options.userName}' is not a valid user principal name.`; } return true; @@ -85,68 +122,40 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['groupId', 'teamId'] }); + this.optionSets.push( + { + options: ['groupId', 'teamId', 'groupName', 'teamName'] + }, + { + options: ['userName', 'ids', 'userNames'] + } + ); + } + + #initTypes(): void { + this.types.string.push('groupId', 'groupName', 'teamId', 'teamName', 'userName', 'ids', 'userNames'); + this.types.boolean.push('force'); } public async commandAction(logger: Logger, args: CommandArgs): Promise { - const groupId: string = (typeof args.options.groupId !== 'undefined') ? args.options.groupId : args.options.teamId as string; + if (args.options.userName) { + await this.warn(logger, `Option 'userName' is deprecated. Please use 'ids' or 'userNames' instead.`); + } const removeUser = async (): Promise => { try { + const groupId: string = await this.getGroupId(logger, args.options.groupId, args.options.teamId, args.options.groupName, args.options.teamName); const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId); if (!isUnifiedGroup) { throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } - // retrieve user - const user: UserResponse = await request.get({ - url: `${this.resource}/v1.0/users/${formatting.encodeQueryParameter(args.options.userName)}/id`, - headers: { - accept: 'application/json;odata.metadata=none' - }, - responseType: 'json' - }); - - // used to verify if the group exists or not - await request.get({ - url: `${this.resource}/v1.0/groups/${groupId}/id`, - headers: { - 'accept': 'application/json;odata.metadata=none' - } - }); - - try { - // try to delete the user from the owners. Accepted error is 404 - await request.delete({ - url: `${this.resource}/v1.0/groups/${groupId}/owners/${user.value}/$ref`, - headers: { - 'accept': 'application/json;odata.metadata=none' - } - }); - } - catch (err: any) { - // the 404 error is accepted - if (err.response.status !== 404) { - throw err.response.data; - } - } + const userNames = args.options.userNames || args.options.userName; + const userIds: string[] = await this.getUserIds(logger, args.options.ids, userNames); - // try to delete the user from the members. Accepted error is 404 - try { - await request.delete({ - url: `${this.resource}/v1.0/groups/${groupId}/members/${user.value}/$ref`, - headers: { - 'accept': 'application/json;odata.metadata=none' - } - }); - } - catch (err: any) { - // the 404 error is accepted - if (err.response.status !== 404) { - throw err.response.data; - } - } + await this.removeUsersFromGroup(groupId, userIds, 'owners'); + await this.removeUsersFromGroup(groupId, userIds, 'members'); } catch (err: any) { this.handleRejectedODataJsonPromise(err); @@ -157,13 +166,58 @@ class EntraM365GroupUserRemoveCommand extends GraphCommand { await removeUser(); } else { - const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove ${args.options.userName} from the ${(typeof args.options.groupId !== 'undefined' ? 'group' : 'team')} ${groupId}?` }); + const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove ${args.options.userName || args.options.userNames || args.options.ids} from ${args.options.groupId || args.options.groupName || args.options.teamId || args.options.teamName}?` }); if (result) { await removeUser(); } } } + + private async getGroupId(logger: Logger, groupId?: string, teamId?: string, groupName?: string, teamName?: string): Promise { + const id = groupId || teamId; + if (id) { + return id; + } + + const name = groupName ?? teamName; + if (this.verbose) { + await logger.logToStderr(`Retrieving Group ID by display name ${name}...`); + } + + return entraGroup.getGroupIdByDisplayName(name!); + } + + private async getUserIds(logger: Logger, userIds?: string, userNames?: string): Promise { + if (userIds) { + return formatting.splitAndTrim(userIds); + } + + if (this.verbose) { + await logger.logToStderr(`Retrieving user IDs for {userNames}...`); + } + + return entraUser.getUserIdsByUpns(formatting.splitAndTrim(userNames!)); + } + + private async removeUsersFromGroup(groupId: string, userIds: string[], role: string): Promise { + for (const userId of userIds) { + try { + await request.delete({ + url: `${this.resource}/v1.0/groups/${groupId}/${role}/${userId}/$ref`, + headers: { + 'accept': 'application/json;odata.metadata=none' + } + }); + } + catch (err: any) { + // the 404 error is accepted + if (err.response.status !== 404) { + throw err.response.data; + } + } + } + } } export default new EntraM365GroupUserRemoveCommand(); \ No newline at end of file From bd7309b86cd2dd68cfae4dbb55320d2d58fb4878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3jcik?= Date: Thu, 17 Oct 2024 01:05:42 +0200 Subject: [PATCH 32/43] Updates release notes --- docs/docs/about/release-notes.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/docs/about/release-notes.mdx b/docs/docs/about/release-notes.mdx index ce57edf29c4..0bfb5b76831 100644 --- a/docs/docs/about/release-notes.mdx +++ b/docs/docs/about/release-notes.mdx @@ -15,6 +15,7 @@ sidebar_position: 3 - removed obsolete example [#6272](https://github.com/pnp/cli-microsoft365/issues/6272) - fixed 'm365 setup' app registration to use 'CLI for M365' [#6367](https://github.com/pnp/cli-microsoft365/issues/6367) - fixed example in 'spo site sharingpermission set' +- updated 'entra m365group user remove' command [#6058](https://github.com/pnp/cli-microsoft365/issues/6058) ### ⚠️ Breaking changes From 3073bfff655d38ddba35ea2e9bb5ac30428b0e92 Mon Sep 17 00:00:00 2001 From: Mathijs Verbeeck Date: Sat, 21 Sep 2024 00:13:29 +0200 Subject: [PATCH 33/43] Enhances teams util. Closes #5901 --- .../commands/channel/channel-get.spec.ts | 473 ++---------------- .../teams/commands/channel/channel-get.ts | 84 +--- src/m365/teams/commands/team/team-get.spec.ts | 321 +++--------- src/m365/teams/commands/team/team-get.ts | 52 +- src/utils/teams.spec.ts | 83 +++ src/utils/teams.ts | 59 ++- 6 files changed, 299 insertions(+), 773 deletions(-) diff --git a/src/m365/teams/commands/channel/channel-get.spec.ts b/src/m365/teams/commands/channel/channel-get.spec.ts index 0316436280b..53e1e3e4d5f 100644 --- a/src/m365/teams/commands/channel/channel-get.spec.ts +++ b/src/m365/teams/commands/channel/channel-get.spec.ts @@ -12,9 +12,21 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './channel-get.js'; -import { settingsNames } from '../../../../settingsNames.js'; +import { teams } from '../../../../utils/teams.js'; describe(commands.CHANNEL_GET, () => { + const teamId = '39958f28-eefb-4006-8f83-13b6ac2a4a7f'; + const teamName = 'Project Team'; + const channelId = '19:4eKaXAtxQJ4Xj3eUvCt4Zx5TPKBhF8jS7SfQYaA7lBY1@thread.tacv2'; + const channelName = 'channel1'; + const channelResponse = { + id: channelId, + displayName: channelName, + description: null, + email: "", + webUrl: "https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/channel1?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4" + }; + let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; @@ -43,13 +55,15 @@ describe(commands.CHANNEL_GET, () => { } }; loggerLogSpy = sinon.spy(logger, 'log'); - (command as any).items = []; + sinon.stub(teams, 'getTeamIdByDisplayName').resolves(teamId); }); afterEach(() => { sinonUtil.restore([ request.get, - cli.getSettingWithDefaultValue + cli.getSettingWithDefaultValue, + teams.getTeamIdByDisplayName, + teams.getChannelByDisplayName ]); }); @@ -66,195 +80,21 @@ describe(commands.CHANNEL_GET, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if both teamId and teamName options are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - id: '19:00000000000000000000000000000000@thread.skype' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if both teamId and teamName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - teamId: '26b48cd6-3da7-493d-8010-1b246ef552d6', - teamName: 'Team Name', - id: '19:00000000000000000000000000000000@thread.skype' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if id, name and primary options are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - teamId: '26b48cd6-3da7-493d-8010-1b246ef552d6' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if id with primary options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - teamId: '26b48cd6-3da7-493d-8010-1b246ef552d6', - id: '19:00000000000000000000000000000000@thread.skype', - primary: true - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if name and primary options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - teamId: '26b48cd6-3da7-493d-8010-1b246ef552d6', - name: 'Channel Name', - primary: true - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if id, name and primary options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - teamId: '26b48cd6-3da7-493d-8010-1b246ef552d6', - id: '19:00000000000000000000000000000000@thread.skype', - name: 'Channel Name', - primary: true - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if id and name are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - teamId: '26b48cd6-3da7-493d-8010-1b246ef552d6', - id: '19:00000000000000000000000000000000@thread.skype', - name: 'Channel Name' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - it('fails validation if the teamId is not a valid guid.', async () => { const actual = await command.validate({ options: { teamId: 'invalid', - id: '19:493665404ebd4a18adb8a980a31b4986@thread.skype' + id: channelId } }, commandInfo); assert.notStrictEqual(actual, true); }); - it('fails validation if the teamId is not provided.', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - id: '19:00000000000000000000000000000000@thread.skype' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if the id is not provided.', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - teamId: '6703ac8a-c49b-4fd4-8223-28f0ac3a6402' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validates for a incorrect id missing leading 19:.', async () => { - const actual = await command.validate({ - options: { - teamId: '00000000-0000-0000-0000-000000000000', - id: '00000000000000000000000000000000@thread.skype' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validates for a incorrect id missing trailing @thread.skype.', async () => { + it('fails validation for a incorrect id missing leading 19:.', async () => { const actual = await command.validate({ options: { - teamId: '00000000-0000-0000-0000-000000000000', - id: '19:552b7125655c46d5b5b86db02ee7bfdf@thread' + teamId: teamId, + id: 'invalid' } }, commandInfo); assert.notStrictEqual(actual, true); @@ -263,8 +103,8 @@ describe(commands.CHANNEL_GET, () => { it('correctly validates the when all options are valid', async () => { const actual = await command.validate({ options: { - teamId: '6703ac8a-c49b-4fd4-8223-28f0ac3a6402', - id: '19:493665404ebd4a18adb8a980a31b4986@thread.skype' + teamId: teamId, + id: channelId } }, commandInfo); assert.strictEqual(actual, true); @@ -272,14 +112,14 @@ describe(commands.CHANNEL_GET, () => { it('fails to get channel information due to wrong channel id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/teams/39958f28-eefb-4006-8f83-13b6ac2a4a7f/channels/19%3A493665404ebd4a18adb8a980a31b4986%40thread.skype`) { + if (opts.url === `https://graph.microsoft.com/v1.0/teams/${teamId}/channels/${channelId}`) { throw { - "error": { - "code": "ItemNotFound", - "message": "Failed to execute Skype backend request GetThreadS2SRequest.", - "innerError": { - "request-id": "4bebd0d2-d154-491b-b73f-d59ad39646fb", - "date": "2019-04-06T13:40:51" + error: { + code: 'ItemNotFound', + message: 'Failed to execute Skype backend request GetThreadS2SRequest.', + innerError: { + 'request-id': '4bebd0d2-d154-491b-b73f-d59ad39646fb', + date: '2019-04-06T13:40:51' } } }; @@ -289,289 +129,84 @@ describe(commands.CHANNEL_GET, () => { await assert.rejects(command.action(logger, { options: { - debug: true, - teamId: '39958f28-eefb-4006-8f83-13b6ac2a4a7f', - id: '19:493665404ebd4a18adb8a980a31b4986@thread.skype' + teamId: teamId, + id: channelId } } as any), new CommandError('Failed to execute Skype backend request GetThreadS2SRequest.')); }); - it('fails when team name does not exist', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/v1.0/groups?$filter=displayName eq '`) > -1) { - return { - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#teams", - "@odata.count": 1, - "value": [ - { - "id": "00000000-0000-0000-0000-000000000000", - "createdDateTime": null, - "displayName": "Team Name", - "description": "Team Description", - "internalId": null, - "classification": null, - "specialization": null, - "visibility": null, - "webUrl": null, - "isArchived": false, - "isMembershipLimitedToOwners": null, - "memberSettings": null, - "guestSettings": null, - "messagingSettings": null, - "funSettings": null, - "discoverySettings": null, - "resourceProvisioningOptions": [] - } - ] - }; - } - throw 'Invalid request'; - }); - - await assert.rejects(command.action(logger, { - options: { - debug: true, - teamName: 'Team Name', - name: 'Channel Name', - tabName: 'Tab Name' - } - } as any), new CommandError('The specified team does not exist in the Microsoft Teams')); - }); - - it('fails to get channel when channel does not exist', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/channels?$filter=displayName eq '`) > -1) { - return { value: [] }; - } - throw 'Invalid request'; - }); - - await assert.rejects(command.action(logger, { - options: { - debug: true, - teamId: '00000000-0000-0000-0000-000000000000', - name: 'Channel Name', - tabName: 'Tab Name' - } - } as any), new CommandError('The specified channel does not exist in the Microsoft Teams team')); - }); - it('should get channel information for the Microsoft Teams team by id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/teams/39958f28-eefb-4006-8f83-13b6ac2a4a7f/channels/19%3A493665404ebd4a18adb8a980a31b4986%40thread.skype`) { - return { - "id": "19:493665404ebd4a18adb8a980a31b4986@thread.skype", - "displayName": "channel1", - "description": null, - "email": "", - "webUrl": "https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/channel1?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4" - }; + if (opts.url === `https://graph.microsoft.com/v1.0/teams/${teamId}/channels/${channelId}`) { + return channelResponse; } throw 'Invalid request'; }); await command.action(logger, { options: { - teamId: '39958f28-eefb-4006-8f83-13b6ac2a4a7f', - id: '19:493665404ebd4a18adb8a980a31b4986@thread.skype' + teamId: teamId, + id: channelId } }); - const call: sinon.SinonSpyCall = loggerLogSpy.lastCall; - assert.strictEqual(call.args[0].id, '19:493665404ebd4a18adb8a980a31b4986@thread.skype'); - assert.strictEqual(call.args[0].displayName, 'channel1'); - assert.strictEqual(call.args[0].description, null); - assert.strictEqual(call.args[0].email, ''); - assert.strictEqual(call.args[0].webUrl, 'https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/channel1?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4'); + assert(loggerLogSpy.calledWith(channelResponse)); }); it('should get primary channel information for the Microsoft Teams team by id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/teams/39958f28-eefb-4006-8f83-13b6ac2a4a7f/primaryChannel`) { - return { - "id": "19:493665404ebd4a18adb8a980a31b4986@thread.skype", - "displayName": "General", - "description": null, - "email": "", - "webUrl": "https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/general?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4" - }; + if (opts.url === `https://graph.microsoft.com/v1.0/teams/${teamId}/primaryChannel`) { + return channelResponse; } throw 'Invalid request'; }); await command.action(logger, { options: { - teamId: '39958f28-eefb-4006-8f83-13b6ac2a4a7f', + teamId: teamId, primary: true } }); - const call: sinon.SinonSpyCall = loggerLogSpy.lastCall; - assert.strictEqual(call.args[0].id, '19:493665404ebd4a18adb8a980a31b4986@thread.skype'); - assert.strictEqual(call.args[0].displayName, 'General'); - assert.strictEqual(call.args[0].description, null); - assert.strictEqual(call.args[0].email, ''); - assert.strictEqual(call.args[0].webUrl, 'https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/general?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4'); + assert(loggerLogSpy.calledWith(channelResponse)); }); it('should get channel information for the Microsoft Teams team by name', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/v1.0/groups?$filter=displayName eq '`) > -1) { - return { - "value": [ - { - "id": "39958f28-eefb-4006-8f83-13b6ac2a4a7f", - "createdDateTime": null, - "displayName": "Team Name", - "description": "Team Description", - "internalId": null, - "classification": null, - "specialization": null, - "visibility": null, - "webUrl": null, - "isArchived": false, - "isMembershipLimitedToOwners": null, - "memberSettings": null, - "guestSettings": null, - "messagingSettings": null, - "funSettings": null, - "discoverySettings": null, - "resourceProvisioningOptions": ["Team"] - } - ] - }; - } - - if ((opts.url as string).indexOf(`/channels?$filter=displayName eq '`) > -1) { - return { - "value": [ - { - "id": "19:493665404ebd4a18adb8a980a31b4986@thread.skype", - "displayName": "Channel Name", - "description": null, - "email": "", - "webUrl": "https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/channel1?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4", - "membershipType": "standard" - } - ] - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/teams/39958f28-eefb-4006-8f83-13b6ac2a4a7f/channels/19%3A493665404ebd4a18adb8a980a31b4986%40thread.skype`) { - return { - "id": "19:493665404ebd4a18adb8a980a31b4986@thread.skype", - "displayName": "Channel Name", - "description": null, - "email": "", - "webUrl": "https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/channel1?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4" - }; - } - throw 'Invalid request'; - }); + sinon.stub(teams, 'getChannelByDisplayName').withArgs(teamId, channelName).resolves(channelResponse); await command.action(logger, { options: { - teamName: 'Team Name', - name: 'Channel Name' + teamName: teamName, + name: channelName } }); - const call: sinon.SinonSpyCall = loggerLogSpy.lastCall; - assert.strictEqual(call.args[0].id, '19:493665404ebd4a18adb8a980a31b4986@thread.skype'); - assert.strictEqual(call.args[0].displayName, 'Channel Name'); - assert.strictEqual(call.args[0].description, null); - assert.strictEqual(call.args[0].email, ''); - assert.strictEqual(call.args[0].webUrl, 'https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/channel1?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4'); + assert(loggerLogSpy.calledWith(channelResponse)); }); it('should get primary channel information for the Microsoft Teams team by name', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if ((opts.url as string).indexOf(`/v1.0/groups?$filter=displayName eq '`) > -1) { - return { - "value": [ - { - "id": "39958f28-eefb-4006-8f83-13b6ac2a4a7f", - "createdDateTime": null, - "displayName": "Team Name", - "description": "Team Description", - "internalId": null, - "classification": null, - "specialization": null, - "visibility": null, - "webUrl": null, - "isArchived": false, - "isMembershipLimitedToOwners": null, - "memberSettings": null, - "guestSettings": null, - "messagingSettings": null, - "funSettings": null, - "discoverySettings": null, - "resourceProvisioningOptions": ["Team"] - } - ] - }; - } - - if ((opts.url as string).indexOf(`/channels?$filter=displayName eq '`) > -1) { - return { - "value": [ - { - "id": "19:493665404ebd4a18adb8a980a31b4986@thread.skype", - "displayName": "General", - "description": null, - "email": "", - "webUrl": "https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/general?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4", - "membershipType": "standard" - } - ] - }; - } - - if (opts.url === `https://graph.microsoft.com/v1.0/teams/39958f28-eefb-4006-8f83-13b6ac2a4a7f/primaryChannel`) { - return { - "id": "19:493665404ebd4a18adb8a980a31b4986@thread.skype", - "displayName": "General", - "description": null, - "email": "", - "webUrl": "https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/general?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4" - }; + if (opts.url === `https://graph.microsoft.com/v1.0/teams/${teamId}/primaryChannel`) { + return channelResponse; } throw 'Invalid request'; }); await command.action(logger, { options: { - teamName: 'Team Name', + teamName: teamName, primary: true } }); - const call: sinon.SinonSpyCall = loggerLogSpy.lastCall; - assert.strictEqual(call.args[0].id, '19:493665404ebd4a18adb8a980a31b4986@thread.skype'); - assert.strictEqual(call.args[0].displayName, 'General'); - assert.strictEqual(call.args[0].description, null); - assert.strictEqual(call.args[0].email, ''); - assert.strictEqual(call.args[0].webUrl, 'https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/general?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4'); + assert(loggerLogSpy.calledWith(channelResponse)); }); - it('should get channel information for the Microsoft Teams team (debug)', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/teams/39958f28-eefb-4006-8f83-13b6ac2a4a7f/channels/19%3A493665404ebd4a18adb8a980a31b4986%40thread.skype`) { - return { - "id": "19:493665404ebd4a18adb8a980a31b4986@thread.skype", - "displayName": "channel1", - "description": null, - "email": "", - "webUrl": "https://teams.microsoft.com/l/channel/19%3a493665404ebd4a18adb8a980a31b4986%40thread.skype/channel1?groupId=39958f28-eefb-4006-8f83-13b6ac2a4a7f&tenantId=ea1787c6-7ce2-4e71-be47-5e0deb30f9e4" - }; - } - throw 'Invalid request'; - }); + it('should get channel information for the Microsoft Teams team', async () => { + sinon.stub(teams, 'getChannelByDisplayName').withArgs(teamId, channelName).resolves(channelResponse); await command.action(logger, { options: { - debug: true, - teamId: '39958f28-eefb-4006-8f83-13b6ac2a4a7f', - id: '19:493665404ebd4a18adb8a980a31b4986@thread.skype' + teamId: teamId, + name: channelName } }); - const call: sinon.SinonSpyCall = loggerLogSpy.getCall(loggerLogSpy.callCount - 2); - assert.strictEqual(call.args[0].id, '19:493665404ebd4a18adb8a980a31b4986@thread.skype'); + assert(loggerLogSpy.calledWith(channelResponse)); }); }); diff --git a/src/m365/teams/commands/channel/channel-get.ts b/src/m365/teams/commands/channel/channel-get.ts index 0af3cbcadf0..b0f97bda5f2 100644 --- a/src/m365/teams/commands/channel/channel-get.ts +++ b/src/m365/teams/commands/channel/channel-get.ts @@ -1,16 +1,11 @@ -import { Channel, Group } from '@microsoft/microsoft-graph-types'; +import { Channel } from '@microsoft/microsoft-graph-types'; import GlobalOptions from '../../../../GlobalOptions.js'; import { Logger } from '../../../../cli/Logger.js'; import request, { CliRequestOptions } from '../../../../request.js'; -import { entraGroup } from '../../../../utils/entraGroup.js'; -import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; - -interface ExtendedGroup extends Group { - resourceProvisioningOptions: string[]; -} +import { teams } from '../../../../utils/teams.js'; interface CommandArgs { options: Options; @@ -25,8 +20,6 @@ interface Options extends GlobalOptions { } class TeamsChannelGetCommand extends GraphCommand { - private teamId: string = ""; - public get name(): string { return commands.CHANNEL_GET; } @@ -51,7 +44,7 @@ class TeamsChannelGetCommand extends GraphCommand { teamName: typeof args.options.teamName !== 'undefined', id: typeof args.options.id !== 'undefined', name: typeof args.options.name !== 'undefined', - primary: (!(!args.options.primary)).toString() + primary: !!args.options.primary }); }); } @@ -99,68 +92,27 @@ class TeamsChannelGetCommand extends GraphCommand { ); } - private async getTeamId(args: CommandArgs): Promise { - if (args.options.teamId) { - return args.options.teamId; - } - - const group = await entraGroup.getGroupByDisplayName(args.options.teamName!); - if ((group as ExtendedGroup).resourceProvisioningOptions.indexOf('Team') === -1) { - throw `The specified team does not exist in the Microsoft Teams`; - } - - return group.id!; - } - - private async getChannelId(args: CommandArgs): Promise { - if (args.options.id) { - return args.options.id; - } - - if (args.options.primary) { - return ''; - } - - const channelRequestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/teams/${formatting.encodeQueryParameter(this.teamId)}/channels?$filter=displayName eq '${formatting.encodeQueryParameter(args.options.name as string)}'`, - headers: { - accept: 'application/json;odata.metadata=none' - }, - responseType: 'json' - }; - - const response = await request.get<{ value: Channel[] }>(channelRequestOptions); - const channelItem: Channel | undefined = response.value[0]; - - if (!channelItem) { - throw `The specified channel does not exist in the Microsoft Teams team`; - } - - return channelItem.id!; - } - public async commandAction(logger: Logger, args: CommandArgs): Promise { try { - this.teamId = await this.getTeamId(args); - const channelId: string = await this.getChannelId(args); - let url: string = ''; - if (args.options.primary) { - url = `${this.resource}/v1.0/teams/${formatting.encodeQueryParameter(this.teamId)}/primaryChannel`; + const teamId = args.options.teamId || await teams.getTeamIdByDisplayName(args.options.teamName!); + + let channel: Channel; + if (args.options.primary || args.options.id) { + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/teams/${teamId}/${args.options.primary ? 'primaryChannel' : `channels/${args.options.id}`}`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + channel = await request.get(requestOptions); } else { - url = `${this.resource}/v1.0/teams/${formatting.encodeQueryParameter(this.teamId)}/channels/${formatting.encodeQueryParameter(channelId)}`; + channel = await teams.getChannelByDisplayName(teamId, args.options.name!); } - const requestOptions: CliRequestOptions = { - url: url, - headers: { - accept: 'application/json;odata.metadata=none' - }, - responseType: 'json' - }; - - const res: Channel = await request.get(requestOptions); - await logger.log(res); + await logger.log(channel); } catch (err: any) { this.handleRejectedODataJsonPromise(err); diff --git a/src/m365/teams/commands/team/team-get.spec.ts b/src/m365/teams/commands/team/team-get.spec.ts index 8d9cacb8692..819e56a7b63 100644 --- a/src/m365/teams/commands/team/team-get.spec.ts +++ b/src/m365/teams/commands/team/team-get.spec.ts @@ -12,9 +12,52 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './team-get.js'; -import { settingsNames } from '../../../../settingsNames.js'; +import { teams } from '../../../../utils/teams.js'; describe(commands.TEAM_GET, () => { + const teamId = '1caf7dcd-7e83-4c3a-94f7-932a1299c844'; + const teamName = 'Finance'; + const teamResponse: any = { + id: teamId, + createdDateTime: '2017-11-29T03:27:05Z', + displayName: teamName, + description: 'This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.', + classification: null, + specialization: 'none', + visibility: 'Public', + webUrl: 'https://teams.microsoft.com/l/team/19:ASjdflg-xKFnjueOwbm3es6HF2zx3Ki57MyfDFrjeg01%40thread.tacv2/conversations?groupId=1caf7dcd-7e83-4c3a-94f7-932a1299c844&tenantId=dcd219dd-bc68-4b9b-bf0b-4a33a796be35', + isArchived: false, + isMembershipLimitedToOwners: false, + discoverySettings: { + showInTeamsSearchAndSuggestions: false + }, + memberSettings: { + allowCreateUpdateChannels: true, + allowCreatePrivateChannels: true, + allowDeleteChannels: true, + allowAddRemoveApps: true, + allowCreateUpdateRemoveTabs: true, + allowCreateUpdateRemoveConnectors: true + }, + guestSettings: { + allowCreateUpdateChannels: false, + allowDeleteChannels: false + }, + messagingSettings: { + allowUserEditMessages: true, + allowUserDeleteMessages: true, + allowOwnerDeleteMessages: true, + allowTeamMentions: true, + allowChannelMentions: true + }, + funSettings: { + allowGiphy: true, + giphyContentRating: 'moderate', + allowStickersAndMemes: true, + allowCustomMemes: true + } + }; + let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; @@ -48,7 +91,8 @@ describe(commands.TEAM_GET, () => { afterEach(() => { sinonUtil.restore([ request.get, - cli.getSettingWithDefaultValue + cli.getSettingWithDefaultValue, + teams.getTeamByDisplayName ]); }); @@ -65,66 +109,32 @@ describe(commands.TEAM_GET, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if both teamId and teamName options are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if both teamId and teamName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ - options: { - id: '1caf7dcd-7e83-4c3a-94f7-932a1299c844', - name: 'Team Name' - } - }, commandInfo); - assert.notStrictEqual(actual, true); - }); - it('fails validation if the teamId is not a valid GUID', async () => { const actual = await command.validate({ options: { id: 'invalid' } }, commandInfo); assert.notStrictEqual(actual, true); }); it('passes validation if the teamId is a valid GUID', async () => { - const actual = await command.validate({ options: { id: '1caf7dcd-7e83-4c3a-94f7-932a1299c844' } }, commandInfo); + const actual = await command.validate({ options: { id: teamId } }, commandInfo); assert.strictEqual(actual, true); }); it('fails to get team information due to wrong team id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/teams/1caf7dcd-7e83-4c3a-94f7-932a1299c843`) { - return Promise.reject({ - "error": { - "code": "NotFound", - "message": "No team found with Group Id 1caf7dcd-7e83-4c3a-94f7-932a1299c843", - "innerError": { - "message": "No team found with Group Id 1caf7dcd-7e83-4c3a-94f7-932a1299c843", - "code": "ItemNotFound", - "innerError": {}, - "date": "2021-09-23T01:26:41", - "request-id": "717697d2-b63d-422f-863c-d74d0c1c8c6f" + if (opts.url === `https://graph.microsoft.com/v1.0/teams/${teamId}`) { + throw { + error: { + code: 'NotFound', + message: `No team found with Group Id ${teamId}`, + innerError: { + message: `No team found with Group Id ${teamId}`, + code: 'ItemNotFound', + innerError: {}, + date: '2021-09-23T01:26:41', + 'request-id': '717697d2-b63d-422f-863c-d74d0c1c8c6f' } } - }); + }; } throw 'Invalid request'; @@ -132,229 +142,38 @@ describe(commands.TEAM_GET, () => { await assert.rejects(command.action(logger, { options: { - id: '1caf7dcd-7e83-4c3a-94f7-932a1299c843' + id: teamId } - } as any), new CommandError('No team found with Group Id 1caf7dcd-7e83-4c3a-94f7-932a1299c843')); + } as any), new CommandError(`No team found with Group Id ${teamId}`)); }); it('fails when team name does not exist', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq 'Finance'`) { - return { - "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#teams", - "@odata.count": 1, - "value": [ - { - "id": "00000000-0000-0000-0000-000000000000", - "resourceProvisioningOptions": [] - } - ] - }; - } - throw 'Invalid request'; - }); + sinon.stub(teams, 'getTeamByDisplayName').rejects(new Error(`The specified team '${teamName}' does not exist.`)); await assert.rejects(command.action(logger, { options: { - debug: true, - name: 'Finance' + name: teamName } - } as any), new CommandError('The specified team does not exist in the Microsoft Teams')); + } as any), new CommandError(`The specified team '${teamName}' does not exist.`)); }); it('retrieves information about the specified Microsoft Team', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/teams/1caf7dcd-7e83-4c3a-94f7-932a1299c844`) { - - return { - "id": "1caf7dcd-7e83-4c3a-94f7-932a1299c844", - "createdDateTime": "2017-11-29T03:27:05Z", - "displayName": "Finance", - "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", - "classification": null, - "specialization": "none", - "visibility": "Public", - "webUrl": "https://teams.microsoft.com/l/team/19:ASjdflg-xKFnjueOwbm3es6HF2zx3Ki57MyfDFrjeg01%40thread.tacv2/conversations?groupId=1caf7dcd-7e83-4c3a-94f7-932a1299c844&tenantId=dcd219dd-bc68-4b9b-bf0b-4a33a796be35", - "isArchived": false, - "isMembershipLimitedToOwners": false, - "discoverySettings": { - "showInTeamsSearchAndSuggestions": false - }, - "memberSettings": { - "allowCreateUpdateChannels": true, - "allowCreatePrivateChannels": true, - "allowDeleteChannels": true, - "allowAddRemoveApps": true, - "allowCreateUpdateRemoveTabs": true, - "allowCreateUpdateRemoveConnectors": true - }, - "guestSettings": { - "allowCreateUpdateChannels": false, - "allowDeleteChannels": false - }, - "messagingSettings": { - "allowUserEditMessages": true, - "allowUserDeleteMessages": true, - "allowOwnerDeleteMessages": true, - "allowTeamMentions": true, - "allowChannelMentions": true - }, - "funSettings": { - "allowGiphy": true, - "giphyContentRating": "moderate", - "allowStickersAndMemes": true, - "allowCustomMemes": true - } - }; + if (opts.url === `https://graph.microsoft.com/v1.0/teams/${teamId}`) { + return teamResponse; } throw 'Invalid request'; }); await command.action(logger, { options: { id: '1caf7dcd-7e83-4c3a-94f7-932a1299c844' } }); - assert(loggerLogSpy.calledWith({ - "id": "1caf7dcd-7e83-4c3a-94f7-932a1299c844", - "createdDateTime": "2017-11-29T03:27:05Z", - "displayName": "Finance", - "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", - "classification": null, - "specialization": "none", - "visibility": "Public", - "webUrl": "https://teams.microsoft.com/l/team/19:ASjdflg-xKFnjueOwbm3es6HF2zx3Ki57MyfDFrjeg01%40thread.tacv2/conversations?groupId=1caf7dcd-7e83-4c3a-94f7-932a1299c844&tenantId=dcd219dd-bc68-4b9b-bf0b-4a33a796be35", - "isArchived": false, - "isMembershipLimitedToOwners": false, - "discoverySettings": { - "showInTeamsSearchAndSuggestions": false - }, - "memberSettings": { - "allowCreateUpdateChannels": true, - "allowCreatePrivateChannels": true, - "allowDeleteChannels": true, - "allowAddRemoveApps": true, - "allowCreateUpdateRemoveTabs": true, - "allowCreateUpdateRemoveConnectors": true - }, - "guestSettings": { - "allowCreateUpdateChannels": false, - "allowDeleteChannels": false - }, - "messagingSettings": { - "allowUserEditMessages": true, - "allowUserDeleteMessages": true, - "allowOwnerDeleteMessages": true, - "allowTeamMentions": true, - "allowChannelMentions": true - }, - "funSettings": { - "allowGiphy": true, - "giphyContentRating": "moderate", - "allowStickersAndMemes": true, - "allowCustomMemes": true - } - })); + assert(loggerLogSpy.calledWith(teamResponse)); }); it('retrieves information about the specified Microsoft Teams team by name', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - - if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq 'Finance'`) { - return { - "value": [ - { - "id": "1caf7dcd-7e83-4c3a-94f7-932a1299c844", - "resourceProvisioningOptions": ["Team"] - } - ] - }; - } + sinon.stub(teams, 'getTeamByDisplayName').withArgs(teamName).resolves(teamResponse); - if (opts.url === `https://graph.microsoft.com/v1.0/teams/1caf7dcd-7e83-4c3a-94f7-932a1299c844`) { - - return { - "id": "1caf7dcd-7e83-4c3a-94f7-932a1299c844", - "createdDateTime": "2017-11-29T03:27:05Z", - "displayName": "Finance", - "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", - "classification": null, - "specialization": "none", - "visibility": "Public", - "webUrl": "https://teams.microsoft.com/l/team/19:ASjdflg-xKFnjueOwbm3es6HF2zx3Ki57MyfDFrjeg01%40thread.tacv2/conversations?groupId=1caf7dcd-7e83-4c3a-94f7-932a1299c844&tenantId=dcd219dd-bc68-4b9b-bf0b-4a33a796be35", - "isArchived": false, - "isMembershipLimitedToOwners": false, - "discoverySettings": { - "showInTeamsSearchAndSuggestions": false - }, - "memberSettings": { - "allowCreateUpdateChannels": true, - "allowCreatePrivateChannels": true, - "allowDeleteChannels": true, - "allowAddRemoveApps": true, - "allowCreateUpdateRemoveTabs": true, - "allowCreateUpdateRemoveConnectors": true - }, - "guestSettings": { - "allowCreateUpdateChannels": false, - "allowDeleteChannels": false - }, - "messagingSettings": { - "allowUserEditMessages": true, - "allowUserDeleteMessages": true, - "allowOwnerDeleteMessages": true, - "allowTeamMentions": true, - "allowChannelMentions": true - }, - "funSettings": { - "allowGiphy": true, - "giphyContentRating": "moderate", - "allowStickersAndMemes": true, - "allowCustomMemes": true - } - }; - } - - throw 'Invalid request'; - }); - - await command.action(logger, { options: { name: 'Finance' } }); - assert(loggerLogSpy.calledWith({ - "id": "1caf7dcd-7e83-4c3a-94f7-932a1299c844", - "createdDateTime": "2017-11-29T03:27:05Z", - "displayName": "Finance", - "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", - "classification": null, - "specialization": "none", - "visibility": "Public", - "webUrl": "https://teams.microsoft.com/l/team/19:ASjdflg-xKFnjueOwbm3es6HF2zx3Ki57MyfDFrjeg01%40thread.tacv2/conversations?groupId=1caf7dcd-7e83-4c3a-94f7-932a1299c844&tenantId=dcd219dd-bc68-4b9b-bf0b-4a33a796be35", - "isArchived": false, - "isMembershipLimitedToOwners": false, - "discoverySettings": { - "showInTeamsSearchAndSuggestions": false - }, - "memberSettings": { - "allowCreateUpdateChannels": true, - "allowCreatePrivateChannels": true, - "allowDeleteChannels": true, - "allowAddRemoveApps": true, - "allowCreateUpdateRemoveTabs": true, - "allowCreateUpdateRemoveConnectors": true - }, - "guestSettings": { - "allowCreateUpdateChannels": false, - "allowDeleteChannels": false - }, - "messagingSettings": { - "allowUserEditMessages": true, - "allowUserDeleteMessages": true, - "allowOwnerDeleteMessages": true, - "allowTeamMentions": true, - "allowChannelMentions": true - }, - "funSettings": { - "allowGiphy": true, - "giphyContentRating": "moderate", - "allowStickersAndMemes": true, - "allowCustomMemes": true - } - })); + await command.action(logger, { options: { name: teamName } }); + assert(loggerLogSpy.calledWith(teamResponse)); }); }); diff --git a/src/m365/teams/commands/team/team-get.ts b/src/m365/teams/commands/team/team-get.ts index d2c4b1e194e..7f64c5d223c 100644 --- a/src/m365/teams/commands/team/team-get.ts +++ b/src/m365/teams/commands/team/team-get.ts @@ -1,16 +1,12 @@ -import { Group, Team } from '@microsoft/microsoft-graph-types'; +import { Team } from '@microsoft/microsoft-graph-types'; import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; -import request from '../../../../request.js'; -import { entraGroup } from '../../../../utils/entraGroup.js'; +import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; - -interface ExtendedGroup extends Group { - resourceProvisioningOptions: string[]; -} +import { teams } from '../../../../utils/teams.js'; interface CommandArgs { options: Options; @@ -62,7 +58,7 @@ class TeamsTeamGetCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (args.options.id && !validation.isValidGuid(args.options.id as string)) { + if (args.options.id && !validation.isValidGuid(args.options.id)) { return `${args.options.id} is not a valid GUID`; } @@ -75,32 +71,24 @@ class TeamsTeamGetCommand extends GraphCommand { this.optionSets.push({ options: ['id', 'name'] }); } - private async getTeamId(args: CommandArgs): Promise { - if (args.options.id) { - return args.options.id; - } - - const group = await entraGroup.getGroupByDisplayName(args.options.name!); - - if ((group as ExtendedGroup).resourceProvisioningOptions.indexOf('Team') === -1) { - throw 'The specified team does not exist in the Microsoft Teams'; - } - - return group.id!; - } - public async commandAction(logger: Logger, args: CommandArgs): Promise { try { - const teamId: string = await this.getTeamId(args); - const requestOptions: any = { - url: `${this.resource}/v1.0/teams/${formatting.encodeQueryParameter(teamId)}`, - headers: { - accept: 'application/json;odata.metadata=none' - }, - responseType: 'json' - }; - const res = await request.get(requestOptions); - await logger.log(res); + let team: Team; + if (args.options.id) { + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/teams/${formatting.encodeQueryParameter(args.options.id)}`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + team = await request.get(requestOptions); + } + else { + team = await teams.getTeamByDisplayName(args.options.name!); + } + + await logger.log(team); } catch (err: any) { this.handleRejectedODataJsonPromise(err); diff --git a/src/utils/teams.spec.ts b/src/utils/teams.spec.ts index 9c2169211a5..9851a94cd3d 100644 --- a/src/utils/teams.spec.ts +++ b/src/utils/teams.spec.ts @@ -24,8 +24,11 @@ describe('utils/teams', () => { afterEach(() => { sinonUtil.restore([ + cli.handleMultipleResultsFound, request.get, + teams.getTeamByDisplayName, teams.getTeamIdByDisplayName, + teams.getChannelByDisplayName, teams.getChannelIdByDisplayName ]); }); @@ -34,6 +37,60 @@ describe('utils/teams', () => { sinon.restore(); }); + it('correctly gets team by display name', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/teams?$filter=displayName eq '${formatting.encodeQueryParameter(teamName)}'`) { + return { value: [teamResponse] }; + } + + throw 'Invalid Request'; + }); + + const actual = await teams.getTeamByDisplayName(teamName); + assert.strictEqual(actual, teamResponse); + }); + + it('throws error if no team is found when retrieving team by display name', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/teams?$filter=displayName eq '${formatting.encodeQueryParameter(teamName)}'`) { + return { value: [] }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(teams.getTeamByDisplayName(teamName), + new Error(`The specified team '${teamName}' does not exist.`)); + }); + + it('throws error message when multiple teams were found using getTeamByDisplayName', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/teams?$filter=displayName eq '${formatting.encodeQueryParameter(teamName)}'`) { + return { value: [teamResponse, { id: 'df20c966-aa55-4810-a086-7e20001e0788', displayName: teamName }] }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(teams.getTeamByDisplayName(teamName), + new Error(`Multiple teams with name '${teamName}' found. Found: ${teamId}, df20c966-aa55-4810-a086-7e20001e0788.`)); + }); + + it('handles selecting single result when multiple teams with the specified name found using getTeamByDisplayName and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/teams?$filter=displayName eq '${formatting.encodeQueryParameter(teamName)}'`) { + return { value: [teamResponse, { id: 'df20c966-aa55-4810-a086-7e20001e0788', displayName: teamName }] }; + } + + throw 'Invalid Request'; + }); + + sinon.stub(cli, 'handleMultipleResultsFound').resolves(teamResponse); + + const actual = await teams.getTeamByDisplayName(teamName); + assert.deepStrictEqual(actual, teamResponse); + }); + it('correctly gets team id by display name', async () => { sinon.stub(request, 'get').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/teams?$filter=displayName eq '${formatting.encodeQueryParameter(teamName)}'&$select=id`) { @@ -88,6 +145,32 @@ describe('utils/teams', () => { assert.deepStrictEqual(actual, teamId); }); + it('correctly retrieves channel by displayName', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/teams/${teamId}/channels?$filter=displayName eq '${formatting.encodeQueryParameter(channelName)}'`) { + return { value: [channelResponse] }; + } + + throw 'Invalid Request'; + }); + + const actual = await teams.getChannelByDisplayName(teamId, channelName); + assert.strictEqual(actual, channelResponse); + }); + + it('throws error if no channel with the specified name is found by name', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/teams/${teamId}/channels?$filter=displayName eq '${formatting.encodeQueryParameter(channelName)}'`) { + return { value: [] }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(teams.getChannelByDisplayName(teamId, channelName), + new Error(`The channel '${channelName}' does not exist in the Microsoft Teams team with ID '${teamId}'.`)); + }); + it('correctly retrieves channel id by displayName', async () => { sinon.stub(request, 'get').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/teams/${teamId}/channels?$filter=displayName eq '${formatting.encodeQueryParameter(channelName)}'&$select=id`) { diff --git a/src/utils/teams.ts b/src/utils/teams.ts index ce45796442b..f1db712af05 100644 --- a/src/utils/teams.ts +++ b/src/utils/teams.ts @@ -7,6 +7,28 @@ import { cli } from '../cli/cli.js'; const graphResource = 'https://graph.microsoft.com'; export const teams = { + /** + * Retrieve a team by its name. + * @param displayName Name of the team to retrieve. + * @throws Error if the team cannot be found. + * @throws Error when multiple teams with the same name and prompting is disabled. + * @returns The Teams team. + */ + async getTeamByDisplayName(displayName: string): Promise { + const teams = await odata.getAllItems(`${graphResource}/v1.0/teams?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`); + + if (!teams.length) { + throw Error(`The specified team '${displayName}' does not exist.`); + } + + if (teams.length === 1) { + return teams[0]; + } + + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', teams); + const result = await cli.handleMultipleResultsFound(`Multiple teams with name '${displayName}' found.`, resultAsKeyValuePair); + return result; + }, /** * Retrieve the id of a team by its name. @@ -22,13 +44,40 @@ export const teams = { throw Error(`The specified team '${displayName}' does not exist.`); } - if (teams.length > 1) { - const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', teams); - const result = await cli.handleMultipleResultsFound(`Multiple teams with name '${displayName}' found.`, resultAsKeyValuePair); - return result.id!; + if (teams.length === 1) { + return teams[0].id!; + } + + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', teams); + const result = await cli.handleMultipleResultsFound(`Multiple teams with name '${displayName}' found.`, resultAsKeyValuePair); + return result.id!; + }, + + /** + * Retrieves a channel by its name in a Microsoft Teams team. + * @param teamId The ID of the team. + * @param name The name of the channel. + * @throws Throws an error if the specified channel does not exist in the team. + * @returns The Teams channel. + */ + async getChannelByDisplayName(teamId: string, name: string): Promise { + const channelRequestOptions: CliRequestOptions = { + url: `${graphResource}/v1.0/teams/${teamId}/channels?$filter=displayName eq '${formatting.encodeQueryParameter(name)}'`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + const response = await odata.getAllItems(channelRequestOptions); + // Only one channel can have the same name in a team + const channelItem = response[0]; + + if (!channelItem) { + throw Error(`The channel '${name}' does not exist in the Microsoft Teams team with ID '${teamId}'.`); } - return teams[0].id!; + return channelItem; }, /** From 603ad9eb73a2bbf573366d54fa38e68176952b7d Mon Sep 17 00:00:00 2001 From: mkm17 Date: Sun, 9 Jun 2024 23:30:09 +0200 Subject: [PATCH 34/43] Adds command 'spo tenant site membership list'. Closes #5980 --- .eslintrc.cjs | 1 + .../tenant/tenant-site-membership-list.mdx | 122 ++++++ docs/src/config/sidebars.ts | 5 + src/m365/spo/commands.ts | 1 + .../tenant-site-membership-list.spec.ts | 362 ++++++++++++++++++ .../tenant/tenant-site-membership-list.ts | 166 ++++++++ src/utils/spo.spec.ts | 34 ++ src/utils/spo.ts | 150 ++++++++ 8 files changed, 841 insertions(+) create mode 100644 docs/docs/cmd/spo/tenant/tenant-site-membership-list.mdx create mode 100644 src/m365/spo/commands/tenant/tenant-site-membership-list.spec.ts create mode 100644 src/m365/spo/commands/tenant/tenant-site-membership-list.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d70e3f6dcc7..6a4f107a168 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -68,6 +68,7 @@ const dictionary = [ 'logout', 'management', 'member', + 'membership', 'messaging', 'model', 'multitenant', diff --git a/docs/docs/cmd/spo/tenant/tenant-site-membership-list.mdx b/docs/docs/cmd/spo/tenant/tenant-site-membership-list.mdx new file mode 100644 index 00000000000..e6fd09b1791 --- /dev/null +++ b/docs/docs/cmd/spo/tenant/tenant-site-membership-list.mdx @@ -0,0 +1,122 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# spo tenant site membership list + +Retrieves information about default site groups' membership + +## Usage + +```sh +m365 spo tenant site membership list [options] +``` + +## Options + +```md definition-list +`-u, --siteUrl ` +: The URL of the site. + +`-r, --role [role]` +: Filter the results to include only users with the specified roles: `Owner`, `Member`, or `Visitor`. +``` + + + +## Remarks + +:::info + +To use this command, you must be a Global or SharePoint administrator. + +::: + +For other scenarios, refer to the [spo web get --withGroups](../web/web-get.mdx) and [spo group member list](../group/group-member-list.mdx) commands. + +## Examples + +Retrieve information about default site groups' owners, members, and visitors of the site. + +```sh +m365 spo tenant site membership list --siteUrl https://contoso.sharepoint.com +``` + +Retrieve information about site owners. + +```sh +m365 spo tenant site membership list --siteUrl https://contoso.sharepoint.com --role Owner +``` + +## Response + + + + + ```json + { + "AssociatedOwnerGroup": [ + { + "email": "jdoe@contoso.onmicrosoft.com", + "loginName": "i:0#.f|membership|jdoe@contoso.onmicrosoft.com", + "name": "John Doe", + "userPrincipalName": "jdoe@contoso.onmicrosoft.com" + } + ], + "AssociatedMemberGroup": [ + { + "email": "avance@contoso.onmicrosoft.com", + "loginName": "i:0#.f|membership|avance@contoso.onmicrosoft.com", + "name": "Adele Vance", + "userPrincipalName": "avance@contoso.onmicrosoft.com" + } + ], + "AssociatedVisitorGroup": [ + { + "email": "", + "loginName": "c:0-.f|rolemanager|spo-grid-all-users/dc109ffd-4298-487e-9cbc-6b9b1a2cd3e2", + "name": "Everyone except external users", + "userPrincipalName": null + } + ] + } + ``` + + + + ```text + email name userPrincipalName associatedGroupType + -------------------------------- ------------------------------ -------------------------------- ------------------- + jdoe@contoso.onmicrosoft.com John Doe jdoe@contoso.onmicrosoft.com Owner + ``` + + + + + ```csv + email,loginName,name,userPrincipalName,associatedGroupType + jdoe@contoso.onmicrosoft.com,i:0#.f|membership|jdoe@contoso.onmicrosoft.com,John Doe,jdoe@contoso.onmicrosoft.com,Owner + ``` + + + + + ```md + # spo tenant site membership list --siteUrl "https://contoso.sharepoint.com/sites/AudienceTest" + + Date: 11/08/2024 + + ## John Doe + + Property | Value + ---------|------- + email | jdoe@contoso.onmicrosoft.com + loginName | i:0#.f\|membership\|jdoe@contoso.onmicrosoft.com + name | John Doe + userPrincipalName | jdoe@contoso.onmicrosoft.com + associatedGroupType | Owner + + ``` + + + diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index 0370e28df09..7f920aa4997 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -3752,6 +3752,11 @@ const sidebars: SidebarsConfig = { type: 'doc', label: 'tenant site unarchive', id: 'cmd/spo/tenant/tenant-site-unarchive' + }, + { + type: 'doc', + label: 'tenant site membership list', + id: 'cmd/spo/tenant/tenant-site-membership-list' } ] }, diff --git a/src/m365/spo/commands.ts b/src/m365/spo/commands.ts index 5b03d224c85..e7323a44bb6 100644 --- a/src/m365/spo/commands.ts +++ b/src/m365/spo/commands.ts @@ -322,6 +322,7 @@ export default { TENANT_SETTINGS_LIST: `${prefix} tenant settings list`, TENANT_SETTINGS_SET: `${prefix} tenant settings set`, TENANT_SITE_ARCHIVE: `${prefix} tenant site archive`, + TENANT_SITE_MEMBERSHIP_LIST: `${prefix} tenant site membership list`, TENANT_SITE_RENAME: `${prefix} tenant site rename`, TENANT_SITE_UNARCHIVE: `${prefix} tenant site unarchive`, TERM_ADD: `${prefix} term add`, diff --git a/src/m365/spo/commands/tenant/tenant-site-membership-list.spec.ts b/src/m365/spo/commands/tenant/tenant-site-membership-list.spec.ts new file mode 100644 index 00000000000..2fb8101c573 --- /dev/null +++ b/src/m365/spo/commands/tenant/tenant-site-membership-list.spec.ts @@ -0,0 +1,362 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import { Logger } from '../../../../cli/Logger.js'; +import request from '../../../../request.js'; +import { telemetry } from '../../../../telemetry.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import { spo } from '../../../../utils/spo.js'; +import commands from '../../commands.js'; +import command from './tenant-site-membership-list.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandError } from '../../../../Command.js'; + +describe(commands.TENANT_SITE_MEMBERSHIP_LIST, () => { + let log: any[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + const ownerMembershipList = [ + { + email: 'owner1Email@email.com', + loginName: 'i:0#.f|membership|owner1loginName@email.com', + name: 'owner1DisplayName', + userPrincipalName: 'owner1loginName' + }, + { + email: 'owner2Email@email.com', + loginName: 'i:0#.f|membership|owner2loginName@email.com', + name: 'owner2DisplayName', + userPrincipalName: 'owner2loginName' + } + ]; + const membersMembershipList = [ + { + email: 'member1Email@email.com', + loginName: 'i:0#.f|membership|member1loginName@email.com', + name: 'member1DisplayName', + userPrincipalName: 'member1loginName' + }, + { + email: 'member2Email@email.com', + loginName: 'i:0#.f|membership|member2loginName@email.com', + name: 'member2DisplayName', + userPrincipalName: 'member2loginName' + } + ]; + const visitorsMembershipList = [ + { + email: 'visitor1Email@email.com', + loginName: 'i:0#.f|membership|visitor1loginName@email.com', + name: 'visitor1DisplayName', + userPrincipalName: 'visitor1loginName' + }, + { + email: 'visitor2Email@email.com', + loginName: 'i:0#.f|membership|visitor2loginName@email.com', + name: 'visitor2DisplayName', + userPrincipalName: 'visitor2loginName' + } + ]; + const ownerMembershipListCSVOutput = [ + { + email: 'owner1Email@email.com', + loginName: 'i:0#.f|membership|owner1loginName@email.com', + name: 'owner1DisplayName', + userPrincipalName: 'owner1loginName', + associatedGroupType: 'Owner' + }, + { + email: 'owner2Email@email.com', + loginName: 'i:0#.f|membership|owner2loginName@email.com', + name: 'owner2DisplayName', + userPrincipalName: 'owner2loginName', + associatedGroupType: 'Owner' + } + ]; + const membersMembershipListCSVOutput = [ + { + email: 'member1Email@email.com', + loginName: 'i:0#.f|membership|member1loginName@email.com', + name: 'member1DisplayName', + userPrincipalName: 'member1loginName', + associatedGroupType: 'Member' + }, + { + email: 'member2Email@email.com', + loginName: 'i:0#.f|membership|member2loginName@email.com', + name: 'member2DisplayName', + userPrincipalName: 'member2loginName', + associatedGroupType: 'Member' + } + ]; + const visitorsMembershipListCSVOutput = [ + { + email: 'visitor1Email@email.com', + loginName: 'i:0#.f|membership|visitor1loginName@email.com', + name: 'visitor1DisplayName', + userPrincipalName: 'visitor1loginName', + associatedGroupType: 'Visitor' + }, + { + email: 'visitor2Email@email.com', + loginName: 'i:0#.f|membership|visitor2loginName@email.com', + name: 'visitor2DisplayName', + userPrincipalName: 'visitor2loginName', + associatedGroupType: 'Visitor' + } + ]; + const adminUrl = 'https://contoso-admin.sharepoint.com'; + const siteUrl = 'https://contoso.sharepoint.com/sites/site'; + const siteId = '00000000-0000-0000-0000-000000000010'; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + sinon.stub(spo, 'getSpoAdminUrl').resolves(adminUrl); + sinon.stub(spo, 'getSiteAdminPropertiesByUrl').resolves({ SiteId: siteId } as any); + auth.connection.active = true; + auth.connection.spoUrl = 'https://contoso.sharepoint.com'; + commandInfo = cli.getCommandInfo(command); + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.get, + request.post + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + auth.connection.spoUrl = undefined; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.TENANT_SITE_MEMBERSHIP_LIST); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('passes validation if the role option is a valid role', async () => { + const actual = await command.validate({ options: { siteUrl: 'https://contoso.sharepoint.com', role: 'Owner' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('fails validation if the siteUrl option is not a valid SharePoint URL', async () => { + const actual = await command.validate({ options: { siteUrl: 'foo' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if the role option is not a valid role', async () => { + const actual = await command.validate({ options: { siteUrl: 'https://contoso.sharepoint.com', role: 'foo' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('lists all site membership groups', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[0,1,2]`) { + return { value: [{ userGroup: ownerMembershipList }, { userGroup: membersMembershipList }, { userGroup: visitorsMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, output: 'json' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], { + AssociatedOwnerGroup: ownerMembershipList, + AssociatedMemberGroup: membersMembershipList, + AssociatedVisitorGroup: visitorsMembershipList + }); + }); + + it('lists all site membership groups - just Owners group', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[0]`) { + return { value: [{ userGroup: ownerMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, role: "Owner", output: 'json' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], { + AssociatedOwnerGroup: ownerMembershipList + }); + }); + + it('lists all site membership groups - just Members group', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[1]`) { + return { value: [{ userGroup: membersMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, role: "Member", output: 'json' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], { + AssociatedMemberGroup: membersMembershipList + }); + }); + + it('lists all site membership groups - just Visitors group', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[2]`) { + return { value: [{ userGroup: visitorsMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, role: "Visitor", output: 'json' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], { + AssociatedVisitorGroup: visitorsMembershipList + }); + }); + + it('lists all site membership groups - csv output', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[0,1,2]`) { + return { value: [{ userGroup: ownerMembershipList }, { userGroup: membersMembershipList }, { userGroup: visitorsMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, output: 'csv' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], [ + ...ownerMembershipListCSVOutput, + ...membersMembershipListCSVOutput, + ...visitorsMembershipListCSVOutput + ]); + }); + + it('lists all site membership groups - text output', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[0,1,2]`) { + return { value: [{ userGroup: ownerMembershipList }, { userGroup: membersMembershipList }, { userGroup: visitorsMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, output: 'text' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], [ + ...ownerMembershipListCSVOutput, + ...membersMembershipListCSVOutput, + ...visitorsMembershipListCSVOutput + ]); + }); + + it('lists all site membership groups - markdown output', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[0,1,2]`) { + return { value: [{ userGroup: ownerMembershipList }, { userGroup: membersMembershipList }, { userGroup: visitorsMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, output: 'md' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], [ + ...ownerMembershipListCSVOutput, + ...membersMembershipListCSVOutput, + ...visitorsMembershipListCSVOutput + ]); + }); + + it('lists all site membership groups - text output when outputs are empty', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[0,1,2]`) { + return { value: [{ userGroup: [] }, { userGroup: [] }, { userGroup: [] }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, output: 'text' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], []); + }); + + it('lists all site membership groups - just Owners group - csv output', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[0]`) { + return { value: [{ userGroup: ownerMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, role: "Owner", output: 'csv' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], [ + ...ownerMembershipListCSVOutput + ]); + }); + + it('lists all site membership groups - just Members group - csv output', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[1]`) { + return { value: [{ userGroup: membersMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, role: "Member", output: 'csv' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], [ + ...membersMembershipListCSVOutput + ]); + }); + + it('lists all site membership groups - just Visitors group - csv output', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `${adminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${siteId}'&userGroupIds=[2]`) { + return { value: [{ userGroup: visitorsMembershipList }] }; + }; + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: siteUrl, role: "Visitor", output: 'csv' } }); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], [ + ...visitorsMembershipListCSVOutput + ]); + }); + + it('correctly handles error when site is not found for specified site URL', async () => { + sinon.stub(request, 'get').rejects({ + error: { + 'odata.error': { + code: "-1, Microsoft.Online.SharePoint.Common.SpoNoSiteException", message: { lang: "en-US", value: `Cannot get site ${siteUrl}.` } + } + } + }); + + await assert.rejects(command.action(logger, { options: { siteUrl: siteUrl, verbose: true } }), new CommandError(`Cannot get site ${siteUrl}.`)); + }); +}); \ No newline at end of file diff --git a/src/m365/spo/commands/tenant/tenant-site-membership-list.ts b/src/m365/spo/commands/tenant/tenant-site-membership-list.ts new file mode 100644 index 00000000000..0007609cc45 --- /dev/null +++ b/src/m365/spo/commands/tenant/tenant-site-membership-list.ts @@ -0,0 +1,166 @@ +import GlobalOptions from '../../../../GlobalOptions.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { odata } from '../../../../utils/odata.js'; +import { spo } from '../../../../utils/spo.js'; +import { validation } from '../../../../utils/validation.js'; +import SpoCommand from '../../../base/SpoCommand.js'; +import commands from '../../commands.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + siteUrl: string; + role?: string; +} + +interface IMembershipResult { + userGroup: IUserInfo[]; +} + +interface IMembershipOutput { + AssociatedOwnerGroup?: IUserInfo[]; + AssociatedMemberGroup?: IUserInfo[]; + AssociatedVisitorGroup?: IUserInfo[]; +} + +interface IUserInfo { + email: string; + loginName: string; + name: string; + userPrincipalName: string; + associatedGroupType?: string; +} + +class SpoTenantSiteMembershipListCommand extends SpoCommand { + public static readonly RoleNames: string[] = ['Owner', 'Member', 'Visitor']; + + public get name(): string { + return commands.TENANT_SITE_MEMBERSHIP_LIST; + } + + public get description(): string { + return `Retrieves information about default site groups' membership`; + } + + public defaultProperties(): string[] | undefined { + return ['email', 'name', 'userPrincipalName', 'associatedGroupType']; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + this.#initTypes(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + role: typeof args.options.role !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-u, --siteUrl ' + }, + { + option: '-r, --role [role]', + autocomplete: SpoTenantSiteMembershipListCommand.RoleNames + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + if (args.options.role && !SpoTenantSiteMembershipListCommand.RoleNames.some(roleName => roleName.toLocaleLowerCase() === args.options.role!.toLocaleLowerCase())) { + return `'${args.options.role}' is not a valid value for option 'role'. Valid values are: ${SpoTenantSiteMembershipListCommand.RoleNames.join(', ')}`; + } + + return validation.isValidSharePointUrl(args.options.siteUrl); + } + ); + } + + #initTypes(): void { + this.types.string.push('role', 'siteUrl'); + }; + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + try { + const spoAdminUrl: string = await spo.getSpoAdminUrl(logger, this.verbose); + const roleIds: string = this.getRoleIds(args.options.role); + const tenantSiteProperties = await spo.getSiteAdminPropertiesByUrl(args.options.siteUrl, false, logger, this.verbose); + + const response = await odata.getAllItems(`${spoAdminUrl}/_api/SPO.Tenant/sites/GetSiteUserGroups?siteId='${tenantSiteProperties.SiteId}'&userGroupIds=[${roleIds}]`); + const result = args.options.output === 'json' ? this.mapResult(response, args.options.role) : this.mapListResult(response, args.options.role); + + await logger.log(result); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } + + private getRoleIds(role?: string): string { + switch (role?.toLowerCase()) { + case 'owner': + return '0'; + case 'member': + return '1'; + case 'visitor': + return '2'; + default: + return '0,1,2'; + } + } + + private mapResult(response: IMembershipResult[], role?: string): IMembershipOutput { + switch (role?.toLowerCase()) { + case 'owner': + return { AssociatedOwnerGroup: response[0].userGroup }; + case 'member': + return { AssociatedMemberGroup: response[0].userGroup }; + case 'visitor': + return { AssociatedVisitorGroup: response[0].userGroup }; + default: + return { + AssociatedOwnerGroup: response[0].userGroup, + AssociatedMemberGroup: response[1].userGroup, + AssociatedVisitorGroup: response[2].userGroup + }; + } + } + + private mapListResult(response: IMembershipResult[], role?: string): IUserInfo[] { + const mapGroup = (groupIndex: number, groupType: string): IUserInfo[] => + response[groupIndex].userGroup.map(user => ({ + ...user, + associatedGroupType: groupType + })); + + switch (role?.toLowerCase()) { + case 'owner': + return mapGroup(0, 'Owner'); + case 'member': + return mapGroup(0, 'Member'); + case 'visitor': + return mapGroup(0, 'Visitor'); + default: + return [ + ...mapGroup(0, 'Owner'), + ...mapGroup(1, 'Member'), + ...mapGroup(2, 'Visitor') + ]; + } + } +} + +export default new SpoTenantSiteMembershipListCommand(); \ No newline at end of file diff --git a/src/utils/spo.spec.ts b/src/utils/spo.spec.ts index 37debe16ffb..f9b7b914885 100644 --- a/src/utils/spo.spec.ts +++ b/src/utils/spo.spec.ts @@ -3123,4 +3123,38 @@ describe('utils/spo', () => { await assert.rejects(spo.getCopyJobResult('https://contoso.sharepoint.com/sites/sales', copyJobInfo), new Error('A file or folder with the name Company.png already exists at the destination.')); }); + + it(`Gets site properties without included details as admin using provided url`, async () => { + const siteId = 'b2307a39-e878-458b-bc90-03bc578531d6'; + const siteProperties = { SiteId: siteId }; + sinon.stub(spo, 'getSpoAdminUrl').resolves('https://contoso-admin.sharepoint.com'); + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_api/SPO.Tenant/GetSitePropertiesByUrl`) { + return siteProperties; + }; + + throw 'Invalid request'; + }); + + await spo.getSiteAdminPropertiesByUrl('https://contoso.sharepoint.com/sites/sales', false, logger, true); + + assert.deepStrictEqual(postStub.firstCall.args[0].data, { url: 'https://contoso.sharepoint.com/sites/sales', includeDetail: false }); + }); + + it(`Gets site properties with included details as admin using provided url`, async () => { + const siteId = 'b2307a39-e878-458b-bc90-03bc578531d6'; + const siteProperties = { SiteId: siteId }; + sinon.stub(spo, 'getSpoAdminUrl').resolves('https://contoso-admin.sharepoint.com'); + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso-admin.sharepoint.com/_api/SPO.Tenant/GetSitePropertiesByUrl`) { + return siteProperties; + }; + + throw 'Invalid request'; + }); + + await spo.getSiteAdminPropertiesByUrl('https://contoso.sharepoint.com/sites/sales', true, logger, true); + + assert.deepStrictEqual(postStub.firstCall.args[0].data, { url: 'https://contoso.sharepoint.com/sites/sales', includeDetail: true }); + }); }); \ No newline at end of file diff --git a/src/utils/spo.ts b/src/utils/spo.ts index 2384c73b1dd..9256d3cce4e 100644 --- a/src/utils/spo.ts +++ b/src/utils/spo.ts @@ -141,6 +141,124 @@ interface CopyJobObjectInfo { // Wrapping this into a settings object so we can alter the values in tests const pollingInterval = 3_000; +interface TenantSiteProperties { + AllowDownloadingNonWebViewableFiles: boolean; + AllowEditing: boolean; + AllowSelfServiceUpgrade: boolean; + AnonymousLinkExpirationInDays: number; + ApplyToExistingDocumentLibraries: boolean; + ApplyToNewDocumentLibraries: boolean; + ArchivedBy: string; + ArchivedTime: string; + ArchiveStatus: string; + AuthContextStrength: any; + AuthenticationContextLimitedAccess: boolean; + AuthenticationContextName: any; + AverageResourceUsage: number; + BlockDownloadLinksFileType: number; + BlockDownloadMicrosoft365GroupIds: any; + BlockDownloadPolicy: boolean; + BlockDownloadPolicyFileTypeIds: any; + BlockGuestsAsSiteAdmin: number; + BonusDiskQuota: string; + ClearRestrictedAccessControl: boolean; + CommentsOnSitePagesDisabled: boolean; + CompatibilityLevel: number; + ConditionalAccessPolicy: number; + CurrentResourceUsage: number; + DefaultLinkPermission: number; + DefaultLinkToExistingAccess: boolean; + DefaultLinkToExistingAccessReset: boolean; + DefaultShareLinkRole: number; + DefaultShareLinkScope: number; + DefaultSharingLinkType: number; + DenyAddAndCustomizePages: number; + Description: string; + DisableAppViews: number; + DisableCompanyWideSharingLinks: number; + DisableFlows: number; + EnableAutoExpirationVersionTrim: boolean; + ExcludeBlockDownloadPolicySiteOwners: boolean; + ExcludeBlockDownloadSharePointGroups: any[]; + ExcludedBlockDownloadGroupIds: any[]; + ExpireVersionsAfterDays: number; + ExternalUserExpirationInDays: number; + GroupId: string; + GroupOwnerLoginName: string; + HasHolds: boolean; + HubSiteId: string; + IBMode: string; + IBSegments: any[]; + IBSegmentsToAdd: any; + IBSegmentsToRemove: any; + InheritVersionPolicyFromTenant: boolean; + IsGroupOwnerSiteAdmin: boolean; + IsHubSite: boolean; + IsTeamsChannelConnected: boolean; + IsTeamsConnected: boolean; + LastContentModifiedDate: string; + Lcid: string; + LimitedAccessFileType: number; + ListsShowHeaderAndNavigation: boolean; + LockIssue: any; + LockReason: number; + LockState: string; + LoopDefaultSharingLinkRole: number; + LoopDefaultSharingLinkScope: number; + MajorVersionLimit: number; + MajorWithMinorVersionsLimit: number; + MediaTranscription: number; + OverrideBlockUserInfoVisibility: number; + OverrideSharingCapability: boolean; + OverrideTenantAnonymousLinkExpirationPolicy: boolean; + OverrideTenantExternalUserExpirationPolicy: boolean; + Owner: string; + OwnerEmail: string; + OwnerLoginName: string; + OwnerName: string; + PWAEnabled: number; + ReadOnlyAccessPolicy: boolean; + ReadOnlyForBlockDownloadPolicy: boolean; + ReadOnlyForUnmanagedDevices: boolean; + RelatedGroupId: string; + RequestFilesLinkEnabled: boolean; + RequestFilesLinkExpirationInDays: number; + RestrictContentOrgWideSearch: boolean; + RestrictedAccessControl: boolean; + RestrictedAccessControlGroups: any[]; + RestrictedAccessControlGroupsToAdd: any; + RestrictedAccessControlGroupsToRemove: any; + RestrictedToRegion: number; + SandboxedCodeActivationCapability: number; + SensitivityLabel: string; + SensitivityLabel2: any; + SetOwnerWithoutUpdatingSecondaryAdmin: boolean; + SharingAllowedDomainList: string; + SharingBlockedDomainList: string; + SharingCapability: number; + SharingDomainRestrictionMode: number; + SharingLockDownCanBeCleared: boolean; + SharingLockDownEnabled: boolean; + ShowPeoplePickerSuggestionsForGuestUsers: boolean; + SiteDefinedSharingCapability: number; + SiteId: string; + SocialBarOnSitePagesDisabled: boolean; + Status: string; + StorageMaximumLevel: string; + StorageQuotaType: any; + StorageUsage: string; + StorageWarningLevel: string; + TeamsChannelType: number; + Template: string; + TimeZoneId: number; + Title: string; + TitleTranslations: Array<{ LCID: number; Value: string }>; + Url: string; + UserCodeMaximumLevel: number; + UserCodeWarningLevel: number; + WebsCount: number; +} + export const spo = { async getRequestDigest(siteUrl: string): Promise { const requestOptions: CliRequestOptions = { @@ -2104,5 +2222,37 @@ export const spo = { const responseContent = await request.get<{ LoginName: string }>(requestOptions); return responseContent?.LoginName; + }, + + /** + * Retrieves the site admin properties for a given site URL. + * @param adminUrl The SharePoint admin url. + * @param siteUrl URL of the site for which to retrieve properties. + * @param includeDetail Set to true to include detailed properties. + * @param logger The logger object. + * @param verbose Set for verbose logging. + * @returns Tenant Site properties. + */ + async getSiteAdminPropertiesByUrl(siteUrl: string, includeDetail: boolean, logger: Logger, verbose?: boolean): Promise { + if (verbose) { + await logger.logToStderr(`Getting site admin properties for URL: ${siteUrl}...`); + } + + const adminUrl: string = await spo.getSpoAdminUrl(logger, !!verbose); + + const requestOptions: CliRequestOptions = { + url: `${adminUrl}/_api/SPO.Tenant/GetSitePropertiesByUrl`, + headers: { + accept: 'application/json;odata=nometadata', + 'content-type': 'application/json;charset=utf-8' + }, + data: { + url: siteUrl, + includeDetail: includeDetail + }, + responseType: 'json' + }; + + return request.post(requestOptions); } }; \ No newline at end of file From fc660ae63e8071dd6d14fededc5c08d45b859f91 Mon Sep 17 00:00:00 2001 From: waldekmastykarz Date: Tue, 8 Oct 2024 09:05:05 +0200 Subject: [PATCH 35/43] Updates dependencies --- npm-shrinkwrap.json | 2448 +++++++++++++++++++++++++++++++++------- package.json | 28 +- src/appInsights.ts | 10 +- src/telemetryRunner.ts | 4 +- 4 files changed, 2081 insertions(+), 409 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c71b97372a2..6768b6439ee 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -9,17 +9,17 @@ "version": "10.0.0", "license": "MIT", "dependencies": { - "@azure/msal-common": "^14.14.2", - "@azure/msal-node": "^2.13.1", - "@inquirer/confirm": "^3.2.0", - "@inquirer/input": "^2.3.0", - "@inquirer/select": "^2.5.0", - "@xmldom/xmldom": "^0.9.2", - "adaptive-expressions": "^4.23.0", + "@azure/msal-common": "14.15", + "@azure/msal-node": "2.15", + "@inquirer/confirm": "^5.0.0", + "@inquirer/input": "^4.0.0", + "@inquirer/select": "^4.0.0", + "@xmldom/xmldom": "^0.9.3", + "adaptive-expressions": "^4.23.1", "adaptivecards": "^3.0.4", "adaptivecards-templating": "^2.3.1", "adm-zip": "^0.5.16", - "applicationinsights": "^2.9.6", + "applicationinsights": "^3.3.0", "axios": "^1.7.7", "chalk": "^5.3.0", "clipboardy": "^4.0.0", @@ -33,8 +33,8 @@ "open": "^10.1.0", "semver": "^7.6.3", "strip-json-comments": "^5.0.1", - "typescript": "^5.5.4", - "update-notifier": "^7.3.0", + "typescript": "^5.6.2", + "update-notifier": "^7.3.1", "uuid": "^10.0.0", "yaml": "^2.5.1", "yargs-parser": "^21.1.1", @@ -47,14 +47,14 @@ "microsoft365": "dist/index.js" }, "devDependencies": { - "@actions/core": "^1.10.1", + "@actions/core": "^1.11.1", "@microsoft/microsoft-graph-types": "^2.40.0", "@types/adm-zip": "^0.5.5", "@types/jmespath": "^0.15.2", "@types/json-schema": "^7.0.15", "@types/json-to-ast": "^2.1.4", - "@types/mocha": "^10.0.7", - "@types/node": "^20.16.5", + "@types/mocha": "^10.0.9", + "@types/node": "^20.16.11", "@types/node-forge": "^1.3.11", "@types/omelette": "^0.4.4", "@types/semver": "^7.5.8", @@ -70,7 +70,7 @@ "eslint-plugin-mocha": "^10.5.0", "mocha": "^10.7.3", "rimraf": "^6.0.1", - "sinon": "^18.0.0", + "sinon": "^19.0.2", "source-map-support": "^0.5.21" } }, @@ -89,22 +89,24 @@ } }, "node_modules/@actions/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", - "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "dev": true, + "license": "MIT", "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" } }, - "node_modules/@actions/core/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "dev": true, - "bin": { - "uuid": "dist/bin/uuid" + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" } }, "node_modules/@actions/http-client": { @@ -117,6 +119,13 @@ "undici": "^5.25.4" } }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@azure/abort-controller": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", @@ -141,6 +150,24 @@ "node": ">=18.0.0" } }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@azure/core-rest-pipeline": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.3.tgz", @@ -182,6 +209,137 @@ "node": ">=18.0.0" } }, + "node_modules/@azure/functions": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.5.1.tgz", + "integrity": "sha512-ikiw1IrM2W9NlQM3XazcX+4Sq3XAjZi4eeG22B5InKC2x5i7MatGF2S/Gn1ACZ+fEInwu+Ru9J8DlnBv1/hIvg==", + "license": "MIT", + "dependencies": { + "cookie": "^0.6.0", + "long": "^4.0.0", + "undici": "^5.13.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@azure/functions-old": { + "name": "@azure/functions", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-3.5.1.tgz", + "integrity": "sha512-6UltvJiuVpvHSwLcK/Zc6NfUwlkDLOFFx97BHCJzlWNsfiWwzwmTsxJXg4kE/LemKTHxPpfoPE+kOJ8hAdiKFQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "long": "^4.0.0", + "uuid": "^8.3.0" + } + }, + "node_modules/@azure/functions-old/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@azure/identity": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.4.1.tgz", + "integrity": "sha512-DwnG4cKFEM7S3T+9u05NstXU/HN0dk45kPOinUyNKsn5VWwpXd9sbPKEg6kgJzGbm1lMuhx9o31PVbCtM5sfBA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.14.0", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/identity/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@azure/identity/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@azure/identity/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/identity/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/identity/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@azure/logger": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", @@ -193,20 +351,129 @@ "node": ">=14.0.0" } }, + "node_modules/@azure/monitor-opentelemetry": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@azure/monitor-opentelemetry/-/monitor-opentelemetry-1.7.1.tgz", + "integrity": "sha512-zBOlaFsmYer6XlEwFMbOUd75nt00Y81zNt/zwKC8TK5dVZYvIWmsem8QvHdS+WuAYn018ToyAN2CEBWl0eTkfg==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/logger": "^1.0.0", + "@azure/monitor-opentelemetry-exporter": "1.0.0-beta.26", + "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", + "@microsoft/applicationinsights-web-snippet": "^1.2.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.53.0", + "@opentelemetry/core": "^1.26.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/instrumentation-bunyan": "^0.41.0", + "@opentelemetry/instrumentation-http": "^0.53.0", + "@opentelemetry/instrumentation-mongodb": "^0.47.0", + "@opentelemetry/instrumentation-mysql": "^0.41.0", + "@opentelemetry/instrumentation-pg": "^0.44.0", + "@opentelemetry/instrumentation-redis": "^0.42.0", + "@opentelemetry/instrumentation-redis-4": "^0.42.0", + "@opentelemetry/instrumentation-winston": "^0.40.0", + "@opentelemetry/resource-detector-azure": "^0.2.11", + "@opentelemetry/resources": "^1.26.0", + "@opentelemetry/sdk-logs": "^0.53.0", + "@opentelemetry/sdk-metrics": "^1.26.0", + "@opentelemetry/sdk-node": "^0.53.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@opentelemetry/sdk-trace-node": "^1.26.0", + "@opentelemetry/semantic-conventions": "^1.26.0", + "@opentelemetry/winston-transport": "^0.6.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/monitor-opentelemetry-exporter": { + "version": "1.0.0-beta.26", + "resolved": "https://registry.npmjs.org/@azure/monitor-opentelemetry-exporter/-/monitor-opentelemetry-exporter-1.0.0-beta.26.tgz", + "integrity": "sha512-ZGyf+6qOAxh1gSAuuT1Li0uY3Y6JXFDoE5sGCgojvPwcgUqnJ28YFgFwePSS7JI3+N4C8NvE8OW3q4Mm9+pKZw==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.53.0", + "@opentelemetry/core": "^1.26.0", + "@opentelemetry/resources": "^1.26.0", + "@opentelemetry/sdk-logs": "^0.53.0", + "@opentelemetry/sdk-metrics": "^1.26.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@opentelemetry/semantic-conventions": "^1.26.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/monitor-opentelemetry/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@azure/monitor-opentelemetry/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.25.0.tgz", + "integrity": "sha512-a0Y7pmSy8SC1s9bvwr+REvyAA1nQcITlZvkElM2gNUPYFTTNUTEdcpg73TmawNucyMdZ9xb/GFcuhrLOqYAzwg==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "14.15.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@azure/msal-common": { - "version": "14.14.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.14.2.tgz", - "integrity": "sha512-XV0P5kSNwDwCA/SjIxTe9mEAsKB0NqGNSuaVrkCCE2lAyBr/D6YtD80Vkdp4tjWnPFwjzkwldjr1xU/facOJog==", + "version": "14.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz", + "integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==", + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.13.1.tgz", - "integrity": "sha512-sijfzPNorKt6+9g1/miHwhj6Iapff4mPQx1azmmZExgzUROqWTM1o3ACyxDja0g47VpowFy/sxTM/WsuCyXTiw==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.15.0.tgz", + "integrity": "sha512-gVPW8YLz92ZeCibQH2QUw96odJoiM3k/ZPH3f2HxptozmH6+OnyyvKXo/Egg39HAM230akarQKHf0W74UHlh0Q==", + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.14.2", + "@azure/msal-common": "14.15.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -244,6 +511,15 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -273,6 +549,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -296,6 +573,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -308,6 +586,7 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -316,17 +595,123 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", - "dev": true, "engines": { "node": ">=14" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.0.tgz", + "integrity": "sha512-eWdP97A6xKtZXVP/ze9y8zYRB2t6ugQAuLXFuZXAsyqmyltaAjl4yPkmIfc0wuTFJMOUF1AdvIFQCL7fMtaX6g==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/@grpc/proto-loader/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/proto-loader/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@grpc/proto-loader/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -354,34 +739,33 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@inquirer/confirm": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.2.0.tgz", - "integrity": "sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.0.tgz", + "integrity": "sha512-6QEzj6bZg8atviRIL+pR0tODC854cYSjvZxkyCarr8DVaOJPEyuGys7GmEG3W0Rb8kKSQec7P6okt0sJvNneFw==", + "license": "MIT", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3" + "@inquirer/core": "^10.0.0", + "@inquirer/type": "^3.0.0" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/core": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.1.0.tgz", - "integrity": "sha512-RZVfH//2ytTjmaBIzeKT1zefcQZzuruwkpTwwbe/i2jTl4o9M+iML5ChULzz6iw1Ok8iUBBsRCjY2IEbD8Ft4w==", - "dependencies": { - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.3", - "@types/mute-stream": "^0.0.4", - "@types/node": "^22.5.2", - "@types/wrap-ansi": "^3.0.0", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.0.0.tgz", + "integrity": "sha512-7dwoKCGvgZGHWTZfOj2KLmbIAIdiXP9NTrwGaTO/XDfKMEmyBahZpnombiG6JDHmiOrmK3GLEJRXrWExXCDLmQ==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.7", + "@inquirer/type": "^3.0.0", "ansi-escapes": "^4.3.2", - "cli-spinners": "^2.9.2", "cli-width": "^4.1.0", - "mute-stream": "^1.0.0", + "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", @@ -391,42 +775,37 @@ "node": ">=18" } }, - "node_modules/@inquirer/core/node_modules/@types/node": { - "version": "22.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", - "dependencies": { - "undici-types": "~6.19.2" - } - }, "node_modules/@inquirer/figures": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.5.tgz", - "integrity": "sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", + "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@inquirer/input": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", - "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.0.0.tgz", + "integrity": "sha512-LD7MNzaX+q2OpU4Fn0i/SedhnnBCAnEzRr6L0MP6ohofFFlx9kp5EXX7flbRZlUnh8icOwC3NFmXTyP76hvo0g==", + "license": "MIT", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/type": "^1.5.3" + "@inquirer/core": "^10.0.0", + "@inquirer/type": "^3.0.0" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/select": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", - "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.0.tgz", + "integrity": "sha512-XTN4AIFusWbNCBU1Xm2YDxbtH94e/FOrC27U3QargSsoDT1mRm+aLfqE+oOZnUuxwtTnInRT8UHRU3MVOu52wg==", + "license": "MIT", "dependencies": { - "@inquirer/core": "^9.1.0", - "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.3", + "@inquirer/core": "^10.0.0", + "@inquirer/figures": "^1.0.7", + "@inquirer/type": "^3.0.0", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -435,14 +814,15 @@ } }, "node_modules/@inquirer/type": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.3.tgz", - "integrity": "sha512-xUQ14WQGR/HK5ei+2CvgcwoH9fQ4PgPGmVFSN0pc1+fVyDL3MREhyAY7nxEErSu6CkllBM3D7e3e+kOvtu+eIg==", - "dependencies": { - "mute-stream": "^1.0.0" - }, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.0.tgz", + "integrity": "sha512-YYykfbw/lefC7yKj7nanzQXILM7r3suIvyFlCcMskc99axmsSewXWkAfXKwMbgxL76iAFVmRwmYdwNZNc8gjog==", + "license": "MIT", "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" } }, "node_modules/@isaacs/cliui": { @@ -552,10 +932,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@microsoft/applicationinsights-web-snippet": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz", - "integrity": "sha512-2IHAOaLauc8qaAitvWS+U931T+ze+7MNWrDHY47IENP5y2UA0vqJDu67kWZDdpCN1fFC77sfgfB+HV7SrKshnQ==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.2.1.tgz", + "integrity": "sha512-+Cy9zFqdQgdAbMK1dpm7B+3DUnrByai0Tq6XG9v737HJpW6G1EiNNbTuFeXdPWyGaq6FIx9jxm/SUcxA6/Rxxg==", + "license": "MIT" }, "node_modules/@microsoft/microsoft-graph-types": { "version": "2.40.0", @@ -564,9 +955,10 @@ "dev": true }, "node_modules/@microsoft/recognizers-text-data-types-timex-expression": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-data-types-timex-expression/-/recognizers-text-data-types-timex-expression-1.3.0.tgz", - "integrity": "sha512-REHUXmMUI1jL3b9v+aSdzKxLxRdejsfg9McYRxY3LW0Gu4UbwD7Q+K6mtSo40cwg8uh6fiV9GY8hDuKXHH6dVA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@microsoft/recognizers-text-data-types-timex-expression/-/recognizers-text-data-types-timex-expression-1.3.1.tgz", + "integrity": "sha512-jarJIFIJZBqeofy3hh0vdQo1yOmTM+jCjj6/zmo9JunsQ6LO750eZHCg9eLptQhsvq321XCt5xdRNLCwU8YeNA==", + "license": "MIT", "engines": { "node": ">=10.3.0" } @@ -607,25 +999,205 @@ } }, "node_modules/@opentelemetry/api": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", - "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", "engines": { "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz", + "integrity": "sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/core": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.21.0.tgz", - "integrity": "sha512-KP+OIweb3wYoP7qTYL/j5IpOlu52uxBv5M4+QhSmmUfLyTgu1OIS71msK3chFo1D6Y61BIH3wMiMYRCxJCQctA==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-grpc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.53.0.tgz", + "integrity": "sha512-x5ygAQgWAQOI+UOhyV3z9eW7QU2dCfnfOuIBiyYmC2AWr74f6x/3JBnP27IAcEx6aihpqBYWKnpoUTztkVPAZw==", + "license": "Apache-2.0", "dependencies": { - "@opentelemetry/semantic-conventions": "1.21.0" + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/sdk-logs": "0.53.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.53.0.tgz", + "integrity": "sha512-cSRKgD/n8rb+Yd+Cif6EnHEL/VZg1o8lEcEwFji1lwene6BdH51Zh3feAD9p2TyVoBKrl6Q9Zm2WltSp2k9gWQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/sdk-logs": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-logs-otlp-proto": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.53.0.tgz", + "integrity": "sha512-jhEcVL1deeWNmTUP05UZMriZPSWUBcfg94ng7JuBb1q2NExgnADQFl1VQQ+xo62/JepK+MxQe4xAwlsDQFbISA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.53.0.tgz", + "integrity": "sha512-nvZtOk23pZOrTW10Za2WPd9pk4tWDvL6ALlHRFfInpcTjtOgCrv+fQDxpzosa5PeXvYeFFUO5aYCTnwiCX4Dzg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-metrics": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.53.0.tgz", + "integrity": "sha512-m6KSh6OBDwfDjpzPVbuJbMgMbkoZfpxYH2r262KckgX9cMYvooWXEKzlJYsNDC6ADr28A1rtRoUVRwNfIN4tUg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.53.0.tgz", + "integrity": "sha512-m7F5ZTq+V9mKGWYpX8EnZ7NjoqAU7VemQ1E2HAG+W/u0wpY1x0OmbxAXfGKFHCspdJk8UKlwPGrpcB8nay3P8A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.53.0.tgz", + "integrity": "sha512-T/bdXslwRKj23S96qbvGtaYOdfyew3TjPEKOk5mHjkCmkVl1O9C/YMdejwSsdLdOq2YW30KjR9kVi0YMxZushQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.26.0.tgz", + "integrity": "sha512-PW5R34n3SJHO4t0UetyHKiXL6LixIqWN6lWncg3eRXhKuT30x+b7m5sDJS0kEWRfHeS+kG7uCw2vBzmB2lk3Dw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" } }, "node_modules/@opentelemetry/instrumentation": { @@ -633,54 +1205,705 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", "dependencies": { - "@types/shimmer": "^1.0.2", - "import-in-the-middle": "1.4.2", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.1", - "shimmer": "^1.2.1" + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.41.0.tgz", + "integrity": "sha512-NoQS+gcwQ7pzb2PZFyra6bAxDAVXBMmpKxBblEuXJWirGrAksQllg9XTdmqhrwT/KxUYrbVca/lMams7e51ysg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.53.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@types/bunyan": "1.8.9" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.53.0.tgz", + "integrity": "sha512-H74ErMeDuZfj7KgYCTOFGWF5W9AfaPnqLQQxeFq85+D29wwV2yqHbz2IKLYpkOh7EI6QwDEl7rZCIxjJLyc/CQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/instrumentation": "0.53.0", + "@opentelemetry/semantic-conventions": "1.27.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.47.0.tgz", + "integrity": "sha512-yqyXRx2SulEURjgOQyJzhCECSh5i1uM49NUaq9TqLd6fA7g26OahyJfsr9NE38HFqGRHpi4loyrnfYGdrsoVjQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/sdk-metrics": "^1.9.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.41.0.tgz", + "integrity": "sha512-jnvrV6BsQWyHS2qb2fkfbfSb1R/lmYwqEZITwufuRl37apTopswu9izc0b1CYRp/34tUG/4k/V39PND6eyiNvw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mysql": "2.15.26" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.44.0.tgz", + "integrity": "sha512-oTWVyzKqXud1BYEGX1loo2o4k4vaU1elr3vPO8NZolrBtFvQ34nx4HgUaexUDuEog00qQt+MLR5gws/p+JXMLQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.40.1", + "@types/pg": "8.6.1", + "@types/pg-pool": "2.0.6" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/instrumentation-redis": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.42.0.tgz", + "integrity": "sha512-jZBoqve0rEC51q0HuhjtZVq1DtUvJHzEJ3YKGvzGar2MU1J4Yt5+pQAQYh1W4jSoDyKeaI4hyeUdWM5N0c2lqA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis-4": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.42.0.tgz", + "integrity": "sha512-NaD+t2JNcOzX/Qa7kMy68JbmoVIV37fT/fJYzLKu2Wwd+0NCxt+K2OOsOakA8GVg8lSpFdbx4V/suzZZ2Pvdjg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis-4/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis-4/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/instrumentation-redis/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/instrumentation-winston": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.40.0.tgz", + "integrity": "sha512-eMk2tKl86YJ8/yHvtDbyhrE35/R0InhO9zuHTflPx8T0+IvKVUhPV71MsJr32sImftqeOww92QHt4Jd+a5db4g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.53.0", + "@opentelemetry/instrumentation": "^0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-winston/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", + "integrity": "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-transformer": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.53.0.tgz", + "integrity": "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.1", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/otlp-exporter-base": "0.53.0", + "@opentelemetry/otlp-transformer": "0.53.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.53.0.tgz", + "integrity": "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "protobufjs": "^7.3.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.26.0.tgz", + "integrity": "sha512-vvVkQLQ/lGGyEy9GT8uFnI047pajSOVnZI2poJqVGD3nJ+B9sFGdlHNnQKophE3lHfnIH0pw2ubrCTjZCgIj+Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.26.0.tgz", + "integrity": "sha512-DelFGkCdaxA1C/QA0Xilszfr0t4YbGd3DjxiCDPh34lfnFr+VkkrjV9S8ZTJvAzfdKERXhfOxIKBoGPJwoSz7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", + "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/resource-detector-azure": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.2.11.tgz", + "integrity": "sha512-XepvQfTXWyHAoAziCfXGwYbSZL0LHtFk5iuKKN2VE2vzcoiw5Tepi0Qafuwb7CCtpQRReao4H7E29MFbCmh47g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.25.1", + "@opentelemetry/resources": "^1.10.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", + "integrity": "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.53.0.tgz", + "integrity": "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.26.0.tgz", + "integrity": "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.53.0.tgz", + "integrity": "sha512-0hsxfq3BKy05xGktwG8YdGdxV978++x40EAKyKr1CaHZRh8uqVlXnclnl7OMi9xLMJEcXUw7lGhiRlArFcovyg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/exporter-logs-otlp-grpc": "0.53.0", + "@opentelemetry/exporter-logs-otlp-http": "0.53.0", + "@opentelemetry/exporter-logs-otlp-proto": "0.53.0", + "@opentelemetry/exporter-trace-otlp-grpc": "0.53.0", + "@opentelemetry/exporter-trace-otlp-http": "0.53.0", + "@opentelemetry/exporter-trace-otlp-proto": "0.53.0", + "@opentelemetry/exporter-zipkin": "1.26.0", + "@opentelemetry/instrumentation": "0.53.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/sdk-logs": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "@opentelemetry/sdk-trace-node": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/instrumentation": { + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/sdk-node/node_modules/import-in-the-middle": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.2.tgz", + "integrity": "sha512-gK6Rr6EykBcc6cVWRSBR5TWf8nn6hZMYSRYqCcHa0l0d1fPK7JSYo6+Mlmck76jIX9aL/IZ71c06U2VpFwl1zA==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.8.2", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.26.0.tgz", + "integrity": "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.26.0", + "@opentelemetry/resources": "1.26.0", + "@opentelemetry/semantic-conventions": "1.27.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/resources": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.21.0.tgz", - "integrity": "sha512-1Z86FUxPKL6zWVy2LdhueEGl9AHDJcx+bvHStxomruz6Whd02mE3lNUMjVJ+FGRoktx/xYQcxccYb03DiUP6Yw==", + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.26.0.tgz", + "integrity": "sha512-Fj5IVKrj0yeUwlewCRwzOVcr5avTuNnMHWf7GPc1t6WaT78J6CJyF3saZ/0RkZfdeNO8IcBl/bNcWMVZBMRW8Q==", + "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/semantic-conventions": "1.21.0" + "@opentelemetry/context-async-hooks": "1.26.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/propagator-b3": "1.26.0", + "@opentelemetry/propagator-jaeger": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "semver": "^7.5.2" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.21.0.tgz", - "integrity": "sha512-yrElGX5Fv0umzp8Nxpta/XqU71+jCAyaLk34GmBzNcrW43nqbrqvdPs4gj4MVy/HcTjr6hifCDCYA3rMkajxxA==", + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", + "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==", + "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "1.21.0", - "@opentelemetry/resources": "1.21.0", - "@opentelemetry/semantic-conventions": "1.21.0" + "@opentelemetry/core": "^1.1.0" }, "engines": { "node": ">=14" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.8.0" + "@opentelemetry/api": "^1.1.0" } }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.21.0.tgz", - "integrity": "sha512-lkC8kZYntxVKr7b8xmjCVUgE0a8xgDakPyDo9uSWavXPyYqLgYYGdEd2j8NxihRyb6UwpX3G/hFUF4/9q2V+/g==", + "node_modules/@opentelemetry/winston-transport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/winston-transport/-/winston-transport-0.6.0.tgz", + "integrity": "sha512-paYrOThvm8cjSB3fQYdRzx+RVTcnTfzqInUH68SEX8frBLyn3FS0KDJJQc3Q3bamfec2J9KEkubHcAzq6OkEVw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.53.0", + "winston-transport": "4.*" + }, "engines": { "node": ">=14" } @@ -732,49 +1955,118 @@ "node": ">=12" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.2.tgz", + "integrity": "sha512-4Bb+oqXZTSTZ1q27Izly9lv8B9dlV61CROxPiVtywwzv5SnytJqhvYe6FclHYuXml4cd1VHPo1zd5PmTeJozvA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" } }, "node_modules/@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^2.0.0", + "@sinonjs/commons": "^3.0.1", "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" } }, - "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, - "dependencies": { - "type-detect": "4.0.8" + "license": "MIT", + "engines": { + "node": ">=4" } }, "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" }, "node_modules/@types/adm-zip": { "version": "0.5.5", @@ -795,6 +2087,15 @@ "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==" }, + "node_modules/@types/bunyan": { + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.9.tgz", + "integrity": "sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/configstore": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/configstore/-/configstore-6.0.2.tgz", @@ -845,25 +2146,26 @@ "integrity": "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==" }, "node_modules/@types/mocha": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz", - "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==", + "version": "10.0.9", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.9.tgz", + "integrity": "sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==", "dev": true, "license": "MIT" }, - "node_modules/@types/mute-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "node_modules/@types/mysql": { + "version": "2.15.26", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", + "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.16.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", + "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", + "license": "MIT", "dependencies": { "undici-types": "~6.19.2" } @@ -883,6 +2185,26 @@ "integrity": "sha512-aEZ+4w3lvomNt6hX+H+VZey8a3fXa/jL6zZdu0vHrc7aGIe2yIybHQgIl7eXcMPchVaPekebQQ2Con7wGgB3mQ==", "dev": true }, + "node_modules/@types/pg": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", + "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -890,9 +2212,10 @@ "dev": true }, "node_modules/@types/shimmer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz", - "integrity": "sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT" }, "node_modules/@types/sinon": { "version": "17.0.3", @@ -909,6 +2232,12 @@ "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", "dev": true }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/update-notifier": { "version": "6.0.8", "resolved": "https://registry.npmjs.org/@types/update-notifier/-/update-notifier-6.0.8.tgz", @@ -925,12 +2254,6 @@ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "dev": true }, - "node_modules/@types/wrap-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", - "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", - "license": "MIT" - }, "node_modules/@types/xmldom": { "version": "0.1.34", "resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.34.tgz", @@ -947,6 +2270,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.18.0", @@ -975,17 +2299,17 @@ } } }, - "node_modules/@typescript-eslint/parser": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", - "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0", - "debug": "^4.3.4" + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1003,14 +2327,17 @@ } } }, - "node_modules/@typescript-eslint/scope-manager": { + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", - "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, + "license": "MIT", "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/visitor-keys": "7.18.0" + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1018,18 +2345,23 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" } }, - "node_modules/@typescript-eslint/type-utils": { + "node_modules/@typescript-eslint/parser": { "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", - "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0", - "@typescript-eslint/utils": "7.18.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1047,11 +2379,30 @@ } } }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/types": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -1065,6 +2416,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", @@ -1093,6 +2445,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1102,6 +2455,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1112,33 +2466,12 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/utils": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", - "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.18.0", - "@typescript-eslint/types": "7.18.0", - "@typescript-eslint/typescript-estree": "7.18.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" @@ -1155,14 +2488,28 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/@xmldom/xmldom": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.2.tgz", - "integrity": "sha512-afP3lpLtalPxgNGU4bxlsru4wSDsZwdSFKnHs6PR0q3KIEWWcAlBqAdx4aWlVtP1gV1FBWlJ3d0MgaRRdj/ucA==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.3.tgz", + "integrity": "sha512-W7fOe0N+t2eyL9sjDE+7bBNo/NZg6U6aU0Rp8wwQV8TRkzLnX13SvROoyJMAH0Kcm9G1DX9b1XI4LxwKxowsXw==", + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": ">=14.6" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" } }, "node_modules/acorn": { @@ -1184,39 +2531,50 @@ "acorn": "^8" } }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/adaptive-expressions": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/adaptive-expressions/-/adaptive-expressions-4.23.0.tgz", - "integrity": "sha512-OvbrD0hlmxyyaTJMpGmFIcfZG0b7GvQTucnQ3apXD6LzI3gQWVfur5eFADFzUOa78/fDjh81ZcKuGFcPt283pg==", - "dependencies": { - "@microsoft/recognizers-text-data-types-timex-expression": "1.3.0", - "@types/atob-lite": "^2.0.0", - "@types/btoa-lite": "^1.0.0", - "@types/lodash.isequal": "^4.5.5", - "@types/lru-cache": "^5.1.0", - "@types/xmldom": "^0.1.30", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/adaptive-expressions/-/adaptive-expressions-4.23.1.tgz", + "integrity": "sha512-wQVhfmF8I0CI+8Bgcoc0sl/oLRek31XzjYq03QjdHyPjIeTmtofgf5qmJDdGQt6nz6fP9Jd0FlnesqasdUEjFA==", + "license": "MIT", + "dependencies": { + "@microsoft/recognizers-text-data-types-timex-expression": "~1.3.1", + "@types/atob-lite": "^2.0.2", + "@types/btoa-lite": "^1.0.2", + "@types/lodash.isequal": "^4.5.8", + "@types/lru-cache": "^5.1.1", + "@types/xmldom": "^0.1.34", "@xmldom/xmldom": "^0.8.6", - "antlr4ts": "0.5.0-alpha.3", + "antlr4ts": "0.5.0-alpha.4", "atob-lite": "^2.0.0", - "big-integer": "^1.6.48", + "big-integer": "^1.6.52", "btoa-lite": "^1.0.0", - "d3-format": "^1.4.4", - "dayjs": "^1.10.3", + "d3-format": "^2.0.0", + "dayjs": "^1.11.13", "fast-xml-parser": "^4.4.1", "jspath": "^0.4.0", "lodash.isequal": "^4.5.0", "lru-cache": "^5.1.1", - "uuid": "^8.3.2", - "xpath": "^0.0.32" + "uuid": "^10.0.0", + "xpath": "^0.0.34" } }, "node_modules/adaptive-expressions/node_modules/@xmldom/xmldom": { @@ -1227,14 +2585,6 @@ "node": ">=10.0.0" } }, - "node_modules/adaptive-expressions/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/adaptivecards": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/adaptivecards/-/adaptivecards-3.0.4.tgz", @@ -1276,6 +2626,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1361,9 +2712,10 @@ } }, "node_modules/antlr4ts": { - "version": "0.5.0-alpha.3", - "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.3.tgz", - "integrity": "sha512-La89tKkGcHFIVuruv4Bm1esc3zLmES2NOTEwwNS1pudz+zx/0FNqQeUu9p48i9/QHKPVqjN87LB+q3buTg7oDQ==" + "version": "0.5.0-alpha.4", + "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", + "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", + "license": "BSD-3-Clause" }, "node_modules/anymatch": { "version": "3.1.3", @@ -1379,33 +2731,38 @@ } }, "node_modules/applicationinsights": { - "version": "2.9.6", - "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-2.9.6.tgz", - "integrity": "sha512-BLeBYJUZaKmnzqG/6Q/IFSCqpiVECjSTIvwozLex/1ZZpSxOjPiBxGMev+iIBfNZ2pc7vvnV7DuPOtsoG2DJeQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-3.3.0.tgz", + "integrity": "sha512-sRpob0J0HTSZyMGZCpnmHYszNmOzu4WTQf/UgxTpCqblWncw/zrrGADwV0Qw0WCllUChQ7wshk26LRzZUYt6Vg==", + "license": "MIT", "dependencies": { - "@azure/core-auth": "1.7.2", - "@azure/core-rest-pipeline": "1.16.3", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.0.0", + "@azure/core-rest-pipeline": "^1.9.2", + "@azure/functions": "^4.5.0", + "@azure/functions-old": "npm:@azure/functions@3.5.1", + "@azure/identity": "^4.2.1", + "@azure/monitor-opentelemetry": "^1.7.1", + "@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.26", "@azure/opentelemetry-instrumentation-azure-sdk": "^1.0.0-beta.5", - "@microsoft/applicationinsights-web-snippet": "1.0.1", - "@opentelemetry/api": "^1.7.0", - "@opentelemetry/core": "^1.19.0", - "@opentelemetry/sdk-trace-base": "^1.19.0", - "@opentelemetry/semantic-conventions": "^1.19.0", - "cls-hooked": "^4.2.2", - "continuation-local-storage": "^3.2.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.53.0", + "@opentelemetry/core": "^1.26.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.53.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.53.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.53.0", + "@opentelemetry/otlp-exporter-base": "^0.53.0", + "@opentelemetry/resources": "^1.26.0", + "@opentelemetry/sdk-logs": "^0.53.0", + "@opentelemetry/sdk-metrics": "^1.26.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@opentelemetry/sdk-trace-node": "^1.26.0", + "@opentelemetry/semantic-conventions": "^1.26.0", "diagnostic-channel": "1.1.1", "diagnostic-channel-publishers": "1.0.8" }, "engines": { "node": ">=8.0.0" - }, - "peerDependencies": { - "applicationinsights-native-metrics": "*" - }, - "peerDependenciesMeta": { - "applicationinsights-native-metrics": { - "optional": true - } } }, "node_modules/argparse": { @@ -1419,41 +2776,11 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/async-hook-jl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", - "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", - "dependencies": { - "stack-chain": "^1.3.7" - }, - "engines": { - "node": "^4.7 || >=6.9 || >=7.3" - } - }, - "node_modules/async-listener": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz", - "integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==", - "dependencies": { - "semver": "^5.3.0", - "shimmer": "^1.1.0" - }, - "engines": { - "node": "<=0.11.8 || >0.11.10" - } - }, - "node_modules/async-listener/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1489,10 +2816,31 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", "engines": { "node": ">=0.6" } @@ -1601,6 +2949,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1630,6 +2979,30 @@ "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==" }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1762,6 +3135,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1838,18 +3212,7 @@ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1937,27 +3300,6 @@ "node": ">=0.8" } }, - "node_modules/cls-hooked": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", - "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", - "dependencies": { - "async-hook-jl": "^1.7.6", - "emitter-listener": "^1.0.1", - "semver": "^5.4.1" - }, - "engines": { - "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" - } - }, - "node_modules/cls-hooked/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/code-error-fragment": { "version": "0.0.230", "resolved": "https://registry.npmjs.org/code-error-fragment/-/code-error-fragment-0.0.230.tgz", @@ -1997,7 +3339,8 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/config-chain": { "version": "1.1.13", @@ -2030,21 +3373,21 @@ "url": "https://github.com/yeoman/configstore?sponsor=1" } }, - "node_modules/continuation-local-storage": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", - "integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==", - "dependencies": { - "async-listener": "^0.6.0", - "emitter-listener": "^1.1.1" - } - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2064,14 +3407,16 @@ "integrity": "sha512-+9lpZfwpLntpTIEpFbwQyWuW/hmI/eHuJZD1XzeZpfZTqkf1fyvBbBLXTJJMsBuuS11uTShMqPwzx4A6ffXgRQ==" }, "node_modules/d3-format": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", - "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz", + "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==", + "license": "BSD-3-Clause" }, "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" }, "node_modules/debug": { "version": "4.3.5", @@ -2204,6 +3549,7 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -2216,6 +3562,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -2282,14 +3629,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/emitter-listener": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", - "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", - "dependencies": { - "shimmer": "^1.2.0" - } - }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2300,7 +3639,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -2332,7 +3670,9 @@ "version": "8.57.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2409,6 +3749,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2480,6 +3821,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -2509,6 +3851,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2530,10 +3873,29 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -2571,7 +3933,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -2607,7 +3970,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -2645,11 +4009,18 @@ "reusify": "^1.0.4" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -2700,6 +4071,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -2715,6 +4087,7 @@ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -2729,7 +4102,8 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.6", @@ -2811,7 +4185,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -2833,6 +4206,7 @@ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2906,6 +4280,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -3004,6 +4379,38 @@ "node": ">=16.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -3019,6 +4426,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3097,6 +4505,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3223,6 +4646,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3370,13 +4794,15 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -3429,7 +4855,8 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jwa": { "version": "1.4.1", @@ -3455,6 +4882,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -3512,11 +4940,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.includes": { "version": "4.3.0", @@ -3596,6 +5031,29 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/logform": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", + "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3635,9 +5093,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -3683,6 +5141,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3837,12 +5296,12 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/natural-compare": { @@ -3852,16 +5311,17 @@ "dev": true }, "node_modules/nise": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", - "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/text-encoding": "^0.7.2", + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", "just-extend": "^6.2.0", - "path-to-regexp": "^6.2.1" + "path-to-regexp": "^8.1.0" } }, "node_modules/node-forge": { @@ -4029,6 +5489,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -4050,6 +5511,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4090,20 +5552,56 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", - "dev": true + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4116,6 +5614,45 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4125,11 +5662,50 @@ "node": ">= 0.8.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "license": "Apache-2.0" + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -4140,6 +5716,7 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -4221,6 +5798,22 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4262,7 +5855,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4301,6 +5893,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4459,6 +6052,21 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -4516,28 +6124,40 @@ } }, "node_modules/sinon": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", - "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/samsam": "^8.0.0", - "diff": "^5.2.0", - "nise": "^6.0.0", - "supports-color": "^7" + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/sinon" } }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4567,10 +6187,24 @@ "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==", "peer": true }, - "node_modules/stack-chain": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", - "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==" + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } }, "node_modules/string-width": { "version": "5.1.2", @@ -4843,11 +6477,21 @@ "node": ">=8.0" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -4886,6 +6530,7 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4903,9 +6548,10 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4918,7 +6564,6 @@ "version": "5.28.2", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==", - "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -4932,11 +6577,12 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/update-notifier": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.0.tgz", - "integrity": "sha512-nA5Zoy3rahYd/Lx1s6jZYHfrKKYOgw0kThkLdwgJtXEFsXqEbMnwdVNPT9D+HELlEXqTR7Iq8rjg/NjenGLIvg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.3.1.tgz", + "integrity": "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==", + "license": "BSD-2-Clause", "dependencies": { - "boxen": "^8.0.0", + "boxen": "^8.0.1", "chalk": "^5.3.0", "configstore": "^7.0.0", "is-in-ci": "^1.0.0", @@ -5089,6 +6735,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -5162,6 +6809,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/winston-transport": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.8.0.tgz", + "integrity": "sha512-qxSTKswC6llEMZKgCQdaWgDuMJQnhuvF5f2Nk3SNXc4byfQ+voo2mX1Px9dkNOuR8p0KAjfPG29PuYUSIb+vSA==", + "license": "MIT", + "dependencies": { + "logform": "^2.6.1", + "readable-stream": "^4.5.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/workerpool": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", @@ -5259,18 +6920,27 @@ } }, "node_modules/xpath": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz", - "integrity": "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==", + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==", + "license": "MIT", "engines": { "node": ">=0.6.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } diff --git a/package.json b/package.json index d9bd4a4df2b..7133ed14288 100644 --- a/package.json +++ b/package.json @@ -252,17 +252,17 @@ "Zachariassen Laksafoss, Trygvi " ], "dependencies": { - "@azure/msal-common": "^14.14.2", - "@azure/msal-node": "^2.13.1", - "@inquirer/confirm": "^3.2.0", - "@inquirer/input": "^2.3.0", - "@inquirer/select": "^2.5.0", - "@xmldom/xmldom": "^0.9.2", - "adaptive-expressions": "^4.23.0", + "@azure/msal-common": "14.15", + "@azure/msal-node": "2.15", + "@inquirer/confirm": "^5.0.0", + "@inquirer/input": "^4.0.0", + "@inquirer/select": "^4.0.0", + "@xmldom/xmldom": "^0.9.3", + "adaptive-expressions": "^4.23.1", "adaptivecards": "^3.0.4", "adaptivecards-templating": "^2.3.1", "adm-zip": "^0.5.16", - "applicationinsights": "^2.9.6", + "applicationinsights": "^3.3.0", "axios": "^1.7.7", "chalk": "^5.3.0", "clipboardy": "^4.0.0", @@ -276,22 +276,22 @@ "open": "^10.1.0", "semver": "^7.6.3", "strip-json-comments": "^5.0.1", - "typescript": "^5.5.4", - "update-notifier": "^7.3.0", + "typescript": "^5.6.2", + "update-notifier": "^7.3.1", "uuid": "^10.0.0", "yaml": "^2.5.1", "yargs-parser": "^21.1.1", "zod": "^3.23.8" }, "devDependencies": { - "@actions/core": "^1.10.1", + "@actions/core": "^1.11.1", "@microsoft/microsoft-graph-types": "^2.40.0", "@types/adm-zip": "^0.5.5", "@types/jmespath": "^0.15.2", "@types/json-schema": "^7.0.15", "@types/json-to-ast": "^2.1.4", - "@types/mocha": "^10.0.7", - "@types/node": "^20.16.5", + "@types/mocha": "^10.0.9", + "@types/node": "^20.16.11", "@types/node-forge": "^1.3.11", "@types/omelette": "^0.4.4", "@types/semver": "^7.5.8", @@ -307,7 +307,7 @@ "eslint-plugin-mocha": "^10.5.0", "mocha": "^10.7.3", "rimraf": "^6.0.1", - "sinon": "^18.0.0", + "sinon": "^19.0.2", "source-map-support": "^0.5.21" } } diff --git a/src/appInsights.ts b/src/appInsights.ts index 8e6d0f23f9f..98b74d915e6 100644 --- a/src/appInsights.ts +++ b/src/appInsights.ts @@ -6,13 +6,15 @@ process.env.APPLICATION_INSIGHTS_NO_STATSBEAT = 'true'; import * as appInsights from 'applicationinsights'; import crypto from 'crypto'; import fs from 'fs'; +import os from 'os'; import path from 'path'; import url from 'url'; import { app } from './utils/app.js'; const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); -const config = appInsights.setup('6b908c80-d09f-4cf6-8274-e54349a0149a'); -config.setInternalLogging(false, false); +appInsights + .setup('6b908c80-d09f-4cf6-8274-e54349a0149a') + .setInternalLogging(false, false); // append -dev to the version number when ran locally // to distinguish production and dev version of the CLI // in the telemetry @@ -29,8 +31,8 @@ appInsightsClient.commonProperties = { appInsightsClient.config.proxyHttpUrl = process.env.HTTP_PROXY ?? ''; appInsightsClient.config.proxyHttpsUrl = process.env.HTTPS_PROXY ?? ''; -appInsightsClient.context.tags['ai.cloud.roleInstance'] = crypto.createHash('sha256').update(appInsightsClient.context.tags['ai.cloud.roleInstance']).digest('hex'); -delete appInsightsClient.context.tags['ai.cloud.role']; +appInsightsClient.context.tags[appInsightsClient.context.keys.cloudRoleInstance] = crypto.createHash('sha256').update(os.hostname()).digest('hex'); +delete appInsightsClient.context.tags[appInsightsClient.context.keys.cloudRole]; delete appInsightsClient.context.tags['ai.cloud.roleName']; export default appInsightsClient; \ No newline at end of file diff --git a/src/telemetryRunner.ts b/src/telemetryRunner.ts index 11454a37412..1d974b5525c 100644 --- a/src/telemetryRunner.ts +++ b/src/telemetryRunner.ts @@ -11,7 +11,7 @@ try { const { commandName, properties, exception, shell, session } = data; appInsights.commonProperties.shell = shell; - appInsights.context.tags['ai.session.id'] = session; + appInsights.context.tags[appInsights.context.keys.sessionId] = session; if (exception) { appInsights.trackException({ @@ -24,6 +24,6 @@ try { properties }); } - appInsights.flush(); + await appInsights.flush(); } catch { } \ No newline at end of file From db579ab2208f7165e62327b2a8b3639e51b41dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kornet?= Date: Mon, 22 Jul 2024 10:47:27 +0200 Subject: [PATCH 36/43] Adds command 'spp model list'. Closes #6103 --- docs/docs/cmd/spp/model/model-list.mdx | 140 +++++++++++++ docs/src/config/sidebars.ts | 9 + src/m365/spp/commands.ts | 3 +- .../spp/commands/model/model-list.spec.ts | 185 ++++++++++++++++++ src/m365/spp/commands/model/model-list.ts | 77 ++++++++ src/utils/spp.spec.ts | 48 +++++ src/utils/spp.ts | 24 +++ 7 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 docs/docs/cmd/spp/model/model-list.mdx create mode 100644 src/m365/spp/commands/model/model-list.spec.ts create mode 100644 src/m365/spp/commands/model/model-list.ts create mode 100644 src/utils/spp.spec.ts create mode 100644 src/utils/spp.ts diff --git a/docs/docs/cmd/spp/model/model-list.mdx b/docs/docs/cmd/spp/model/model-list.mdx new file mode 100644 index 00000000000..e7cd2ca30d0 --- /dev/null +++ b/docs/docs/cmd/spp/model/model-list.mdx @@ -0,0 +1,140 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# spp model list + +Retrieves the list of unstructured document processing models + +## Usage + +```sh +m365 spp model list [options] +``` + +## Options + +```md definition-list +`-u, --siteUrl ` +: The URL of the content center site. +``` + + + +## Examples + +Retrieve a list of SharePoint Premium unstructured document processing models on the content center site. + +```sh +m365 spp model list --siteUrl https://contoso.sharepoint.com/sites/ContentCenter +``` + +## Response + + + + + ```json + [ + { + "AIBuilderHybridModelType": "Freeform document processing", + "AzureCognitivePrebuiltModelName": null, + "BaseContentTypeName": null, + "ConfidenceScore": "{\r\\\n \"ConfidenceScoresPerLibrary\": {\r\\\n \"64e2de73-dfbe-4085-a49f-8347ddfc7e3a\": 0.86973333358764682\r\\\n }\r\\\n}", + "ContentTypeGroup": "Intelligent Document Content Types", + "ContentTypeId": "0x01010027D65A01A9F279408035C63ED6682D3A", + "ContentTypeName": "UnorderedModel", + "Created": "2024-05-22T20:11:59Z", + "CreatedBy": "i:0#.f|membership|user@contoso.onmicrosoft.com", + "DriveId": "b!xlpC8UN4X0WDi7Xz7yRQP_JZHRltjGlEpsj90HfZPCiSNv6idu5tRoCxMjOJSIyo", + "Explanations": "{\r\\\n \"Classifier\": [],\r\\\n \"Extractors\": {}\r\\\n}", + "ID": 1, + "LastTrained": "2024-05-23T17:35:53Z", + "ListID": "a2fe3692-ee76-466d-80b1-323389488ca8", + "ModelSettings": "{\r\\\n \"ModelTypeSpecificSettings\": {\r\\\n \"kind\": \"aiBuilderHybridSettings\",\r\\\n \"AIBHybridModelType\": \"CustomDocument\",\r\\\n \"AIBHybridModelHostName\": \"make.powerapps.com\",\r\\\n \"IsTestEnvironment\": false,\r\\\n \"AIBHybridModelId\": \"16cff4de-bac7-4849-957e-54bdee08e11e\",\r\\\n \"AIBHybridModelEnvironment\": \"Default-4d704217-2c7f-4072-8799-820f988299d7\",\r\\\n \"AIBHybridModelStatus\": \"publisheddraft\",\r\\\n \"DataverseInstanceUrl\": \"https://org.crm4.dynamics.com/\"\r\\\n },\r\\\n \"LastPublishedTime\": \"2024-05-23T10:36:36Z\",\r\\\n \"ClassifiedItemsCount\": \"3\"\r\\\n}", + "ModelType": 8, + "Modified": "2024-05-24T11:57:59Z", + "ModifiedBy": "i:0#.w|sharepoint\\system", + "ObjectId": "01RS33TEFSBQKR7PVA25C2USVWUH2YFFRV", + "PublicationType": 0, + "Schemas": "{\"Version\":2,\"Extractors\":{\"FirstName\":{\"id\":\"labels/FirstName\",\"concepts\":{},\"table\":null,\"detectionOnly\":false},\"LastName\":{\"id\":\"labels/LastName\",\"concepts\":{},\"table\":null,\"detectionOnly\":false},\"Hobbies\":{\"id\":\"labels/Hobbies\",\"concepts\":{},\"table\":null,\"detectionOnly\":false},\"Email\":{\"id\":\"labels/Email\",\"concepts\":{},\"table\":null,\"detectionOnly\":false},\"PhoneNumber\":{\"id\":\"labels/PhoneNumber\",\"concepts\":{},\"table\":null,\"detectionOnly\":false},\"City\":{\"id\":\"labels/City\",\"concepts\":{},\"table\":null,\"detectionOnly\":false},\"JobTitle\":{\"id\":\"labels/JobTitle\",\"concepts\":{},\"table\":null,\"detectionOnly\":false},\"Education_aibtable\":{\"id\":\"tables/Education\",\"concepts\":{},\"table\":{\"name\":\"Education\",\"columns\":[{\"name\":\"SchoolName\",\"id\":null},{\"name\":\"SpecializationName\",\"id\":null},{\"name\":\"StartDate\",\"id\":null},{\"name\":\"EndDate\",\"id\":null}]},\"detectionOnly\":false},\"WorkExperiance_aibtable\":{\"id\":\"tables/WorkExperiance\",\"concepts\":{},\"table\":{\"name\":\"WorkExperiance\",\"columns\":[{\"name\":\"CompanyName\",\"id\":null},{\"name\":\"StartDate\",\"id\":null},{\"name\":\"EndDate\",\"id\":null},{\"name\":\"Position\",\"id\":null},{\"name\":\"TechnologiesKeywords\",\"id\":null}]},\"detectionOnly\":false}},\"Classifiers\":null,\"accuracy\":null,\"relationships\":null,\"TemplatedModel\":null}", + "SourceSiteUrl": "https://contoso.sharepoint.com/sites/ContentCenter", + "SourceUrl": null, + "SourceWebServerRelativeUrl": "/sites/ContentCenter", + "UniqueId": "1f150cb2-a0be-45d7-aa4a-b6a1f5829635" + } + ] + ``` + + + + + ```text + AIBuilderHybridModelType ContentTypeName LastTrained UniqueId + ------------------------------ ----------------- -------------------- ------------------------------------ + Freeform document processing UnorderedModel 2024-05-23T17:35:53Z 1f150cb2-a0be-45d7-aa4a-b6a1f5829635 + ``` + + + + + ```csv + AIBuilderHybridModelType,AzureCognitivePrebuiltModelName,BaseContentTypeName,ConfidenceScore,ContentTypeGroup,ContentTypeId,ContentTypeName,Created,CreatedBy,DriveId,Explanations,ID,LastTrained,ListID,ModelSettings,ModelType,Modified,ModifiedBy,ObjectId,PublicationType,Schemas,SourceSiteUrl,SourceUrl,SourceWebServerRelativeUrl,UniqueId + Freeform document processing,,,"{ + ""ConfidenceScoresPerLibrary"": { + ""64e2de73-dfbe-4085-a49f-8347ddfc7e3a"": 0.86973333358764682 + } + }",Intelligent Document Content Types,0x01010027D65A01A9F279408035C63ED6682D3A,UnorderedModel,2024-05-22T20:11:59Z,i:0#.f|membership|user@contoso.onmicrosoft.com,b!xlpC8UN4X0WDi7Xz7yRQP_JZHRltjGlEpsj90HfZPCiSNv6idu5tRoCxMjOJSIyo,"{ + ""Classifier"": [], + ""Extractors"": {} + }",1,2024-05-23T17:35:53Z,a2fe3692-ee76-466d-80b1-323389488ca8,"{ + ""ModelTypeSpecificSettings"": { + ""kind"": ""aiBuilderHybridSettings"", + ""AIBHybridModelType"": ""CustomDocument"", + ""AIBHybridModelHostName"": ""make.powerapps.com"", + ""IsTestEnvironment"": false, + ""AIBHybridModelId"": ""16cff4de-bac7-4849-957e-54bdee08e11e"", + ""AIBHybridModelEnvironment"": ""Default-4d704217-2c7f-4072-8799-820f988299d7"", + ""AIBHybridModelStatus"": ""publisheddraft"", + ""DataverseInstanceUrl"": ""https://org.crm4.dynamics.com/"" + }, + ""LastPublishedTime"": ""2024-05-23T10:36:36Z"", + ""ClassifiedItemsCount"": ""3"" + }",8,2024-05-24T11:57:59Z,i:0#.w|sharepoint\system,01RS33TEFSBQKR7PVA25C2USVWUH2YFFRV,0,"Model Schema",https://contoso.sharepoint.com/sites/ContentCenter,,/sites/ContentCenter,1f150cb2-a0be-45d7-aa4a-b6a1f5829635 + ``` + + + + + ```md + # spp model list --siteUrl "https://contoso.sharepoint.com/sites/ContentCenter" + + Date: 7/22/2024 + + ## 1 + + Property | Value + ---------|------- + AIBuilderHybridModelType | Freeform document processing + ContentTypeGroup | Intelligent Document Content Types + ContentTypeId | 0x01010027D65A01A9F279408035C63ED6682D3A + ContentTypeName | UnorderedModel + Created | 2024-05-22T20:11:59Z + CreatedBy | i:0#.f\|membership\|user@contoso.onmicrosoft.com + DriveId | b!xlpC8UN4X0WDi7Xz7yRQP\_JZHRltjGlEpsj90HfZPCiSNv6idu5tRoCxMjOJSIyo + ID | 1 + LastTrained | 2024-05-23T17:35:53Z + ListID | a2fe3692-ee76-466d-80b1-323389488ca8 + ModelType | 8 + Modified | 2024-05-24T11:57:59Z + ModifiedBy | i:0#.w\|sharepoint\system + ObjectId | 01RS33TEFSBQKR7PVA25C2USVWUH2YFFRV + PublicationType | 0 + Schemas | {"Version":2,"Extractors":{"FirstName":{"id":"labels/FirstName","concepts":{},"table":null,"detectionOnly":false},"LastName":{"id":"labels/LastName","concepts":{},"table":null,"detectionOnly":false},"Hobbies":{"id":"labels/Hobbies","concepts":{},"table":null,"detectionOnly":false},"Email":{"id":"labels/Email","concepts":{},"table":null,"detectionOnly":false},"PhoneNumber":{"id":"labels/PhoneNumber","concepts":{},"table":null,"detectionOnly":false},"City":{"id":"labels/City","concepts":{},"table":null,"detectionOnly":false},"JobTitle":{"id":"labels/JobTitle","concepts":{},"table":null,"detectionOnly":false},"Education\_aibtable":{"id":"tables/Education","concepts":{},"table":{"name":"Education","columns":[{"name":"SchoolName","id":null},{"name":"SpecializationName","id":null},{"name":"StartDate","id":null},{"name":"EndDate","id":null}]},"detectionOnly":false},"WorkExperiance\_aibtable":{"id":"tables/WorkExperiance","concepts":{},"table":{"name":"WorkExperiance","columns":[{"name":"CompanyName","id":null},{"name":"StartDate","id":null},{"name":"EndDate","id":null},{"name":"Position","id":null},{"name":"TechnologiesKeywords","id":null}]},"detectionOnly":false}},"Classifiers":null,"accuracy":null,"relationships":null,"TemplatedModel":null} + SourceSiteUrl | https://contoso.sharepoint.com/sites/ContentCenter + SourceWebServerRelativeUrl | /sites/ContentCenter + UniqueId | 1f150cb2-a0be-45d7-aa4a-b6a1f5829635 + ``` + + + diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index 7f920aa4997..c995ffe5bdb 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -3957,6 +3957,15 @@ const sidebars: SidebarsConfig = { id: 'cmd/spp/contentcenter/contentcenter-list' } ] + }, + { + model: [ + { + type: 'doc', + label: 'model list', + id: 'cmd/spp/model/model-list' + } + ] } ] }, diff --git a/src/m365/spp/commands.ts b/src/m365/spp/commands.ts index d3309979bb6..97f9c21b5bb 100644 --- a/src/m365/spp/commands.ts +++ b/src/m365/spp/commands.ts @@ -1,5 +1,6 @@ const prefix: string = 'spp'; export default { - CONTENTCENTER_LIST: `${prefix} contentcenter list` + CONTENTCENTER_LIST: `${prefix} contentcenter list`, + MODEL_LIST: `${prefix} model list` }; \ No newline at end of file diff --git a/src/m365/spp/commands/model/model-list.spec.ts b/src/m365/spp/commands/model/model-list.spec.ts new file mode 100644 index 00000000000..f74759628d9 --- /dev/null +++ b/src/m365/spp/commands/model/model-list.spec.ts @@ -0,0 +1,185 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { CommandError } from '../../../../Command.js'; +import request from '../../../../request.js'; +import { telemetry } from '../../../../telemetry.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import commands from '../../commands.js'; +import command from './model-list.js'; + +describe(commands.MODEL_LIST, () => { + let log: string[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + const models = [ + { + "AIBuilderHybridModelType": null, + "AzureCognitivePrebuiltModelName": null, + "BaseContentTypeName": null, + "ConfidenceScore": "{\"trainingStatus\":{\"kind\":\"original\",\"ClassifierStatus\":{\"TrainingStatus\":\"success\",\"TimeStamp\":1716547860981},\"ExtractorsStatus\":[{\"TimeStamp\":1716547860173,\"ExtractorName\":\"Name\",\"TrainingStatus\":\"success\"}]},\"modelAccuracy\":{\"Classifier\":1,\"Extractors\":{\"Name\":0.333333343}},\"perSampleAccuracy\":{\"1\":{\"Classifier\":1,\"Extractors\":{\"Name\":1}},\"2\":{\"Classifier\":1,\"Extractors\":{\"Name\":0}},\"3\":{\"Classifier\":1,\"Extractors\":{\"Name\":0}},\"4\":{\"Classifier\":1,\"Extractors\":{\"Name\":0}},\"5\":{\"Classifier\":1,\"Extractors\":{\"Name\":0}},\"6\":{\"Classifier\":1,\"Extractors\":{\"Name\":1}}},\"perSamplePrediction\":{\"1\":{\"Extractors\":{\"Name\":[\"Michał\"]}},\"2\":{\"Extractors\":{\"Name\":[]}},\"3\":{\"Extractors\":{\"Name\":[]}},\"4\":{\"Extractors\":{\"Name\":[]}},\"5\":{\"Extractors\":{\"Name\":[]}},\"6\":{\"Extractors\":{\"Name\":[]}}},\"trainingFailures\":{}}", + "ContentTypeGroup": "Intelligent Document Content Types", + "ContentTypeId": "0x010100A5C3671D1FB1A64D9F280C628D041692", + "ContentTypeName": "TeachingModel", + "Created": "2024-05-23T16:51:29Z", + "CreatedBy": "i:0#.f|membership|user@contoso.onmicrosoft.com", + "DriveId": "b!qTVLltt1P02LUgejOy6O_1amoFeu1EJBlawH83UtYbQs_H3KVKAcQpuQOpNLl646", + "Explanations": "{\"Classifier\":[{\"id\":\"950f39cd-5e72-4442-ae8a-ea4bae32962c\",\"kind\":\"regexFeature\",\"name\":\"Email address\",\"active\":true,\"pattern\":\"[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,6}\"},{\"id\":\"d3f2940d-1df1-4ba8-975a-db3a4d626d5c\",\"kind\":\"dictionaryFeature\",\"name\":\"FirstName\",\"active\":true,\"nGrams\":[\"Michał\"],\"caseSensitive\":false,\"ignoreDigitIdentity\":false,\"ignoreLetterIdentity\":false},{\"id\":\"077966e1-73be-44c9-855f-a4eade6a280b\",\"kind\":\"modelFeature\",\"name\":\"Name\",\"active\":true,\"modelReference\":\"Name\",\"conceptId\":\"309b64e5-acd5-4538-a5b6-c6bfcdc1ffbf\"}],\"Extractors\":{\"Name\":[{\"id\":\"69e412bc-e5bc-4657-b378-34a01966bb92\",\"kind\":\"dictionaryFeature\",\"name\":\"Before label\",\"active\":true,\"nGrams\":[\"Test\",\"'Surname\",\"Test (\"],\"caseSensitive\":false,\"ignoreDigitIdentity\":false,\"ignoreLetterIdentity\":false}]}}", + "ID": 1, + "LastTrained": "2024-05-24T17:51:16Z", + "ListID": "ca7dfc2c-a054-421c-9b90-3a934b97ae3a", + "ModelSettings": "{\"ModelOrigin\":{\"LibraryId\":\"8a6027ab-c584-4394-ba9c-3dc4dd152b65\",\"Published\":false,\"SelecteFileUniqueIds\":[]}}", + "ModelType": 2, + "Modified": "2024-05-24T17:51:02Z", + "ModifiedBy": "i:0#.f|membership|user@contoso.onmicrosoft.com", + "ObjectId": "01HQDCWVGIEBDRN3RVK5A3UJW3M4TCMT45", + "PublicationType": 0, + "Schemas": "{\"Version\":2,\"Extractors\":{\"Name\":{\"concepts\":{\"309b64e5-acd5-4538-a5b6-c6bfcdc1ffbf\":{\"name\":\"Name\"}},\"relationships\":[],\"id\":\"Name\"}}}", + "SourceSiteUrl": "https://contoso.sharepoint.com/sites/SyntexTest", + "SourceUrl": null, + "SourceWebServerRelativeUrl": "/sites/SyntexTest", + "UniqueId": "164720c8-35ee-4157-ba26-db6726264f9d" + } + ]; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.get + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.MODEL_LIST); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('defines correct properties for the default output', () => { + assert.deepStrictEqual(command.defaultProperties(), ['AIBuilderHybridModelType', 'ContentTypeName', 'LastTrained', 'UniqueId']); + }); + + it('passes validation when required parameters are valid', async () => { + const actual = await command.validate({ options: { siteUrl: 'https://contoso.sharepoint.com/sites/sales' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('fails validation when siteUrl is not valid', async () => { + const actual = await command.validate({ options: { siteUrl: 'invalidUrl' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('correctly handles site is not Content Site', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/web?$select=WebTemplateConfiguration`) { + return { + WebTemplateConfiguration: 'SITEPAGEPUBLISHING#0' + }; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { verbose: true, siteUrl: 'https://contoso.sharepoint.com/sites/portal' } }), + new CommandError('https://contoso.sharepoint.com/sites/portal is not a content site.')); + }); + + + it('correctly handles an access denied error', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/web?$select=WebTemplateConfiguration`) { + throw { + error: { + "odata.error": { + message: { + lang: "en-US", + value: "Attempted to perform an unauthorized operation." + } + } + } + }; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { verbose: true, siteUrl: 'https://contoso.sharepoint.com/sites/portal' } }), + new CommandError('Attempted to perform an unauthorized operation.')); + }); + + + it('retrieves all site models', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/web?$select=WebTemplateConfiguration`) { + return { + WebTemplateConfiguration: 'CONTENTCTR#0' + }; + } + + if (opts.url === 'https://contoso.sharepoint.com/sites/portal/_api/machinelearning/models') { + return { value: models }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal' } }); + assert(loggerLogSpy.calledOnceWithExactly(models)); + }); + + it('gets correct model list when the site URL has a trailing slash', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/sites/portal/_api/web?$select=WebTemplateConfiguration`) { + return { + WebTemplateConfiguration: 'CONTENTCTR#0' + }; + } + + if (opts.url === 'https://contoso.sharepoint.com/sites/portal/_api/machinelearning/models') { + return { value: models }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { siteUrl: 'https://contoso.sharepoint.com/sites/portal/' } }); + assert(loggerLogSpy.calledOnceWithExactly(models)); + }); +}); diff --git a/src/m365/spp/commands/model/model-list.ts b/src/m365/spp/commands/model/model-list.ts new file mode 100644 index 00000000000..164de544f6f --- /dev/null +++ b/src/m365/spp/commands/model/model-list.ts @@ -0,0 +1,77 @@ +import { Logger } from '../../../../cli/Logger.js'; +import GlobalOptions from '../../../../GlobalOptions.js'; +import { odata } from '../../../../utils/odata.js'; +import { spp } from '../../../../utils/spp.js'; +import { urlUtil } from '../../../../utils/urlUtil.js'; +import { validation } from '../../../../utils/validation.js'; +import SpoCommand from '../../../base/SpoCommand.js'; +import commands from '../../commands.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + siteUrl: string; +} + +class SppModelListCommand extends SpoCommand { + public get name(): string { + return commands.MODEL_LIST; + } + + public get description(): string { + return 'Retrieves the list of unstructured document processing models'; + } + + public defaultProperties(): string[] | undefined { + return ['AIBuilderHybridModelType', 'ContentTypeName', 'LastTrained', 'UniqueId']; + } + + constructor() { + super(); + + this.#initOptions(); + this.#initTypes(); + this.#initValidators(); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-u, --siteUrl ' + } + ); + } + + #initTypes(): void { + this.types.string.push('siteUrl'); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + return validation.isValidSharePointUrl(args.options.siteUrl); + } + ); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + try { + if (this.verbose) { + await logger.log(`Retrieving models from ${args.options.siteUrl}...`); + } + + const siteUrl = urlUtil.removeTrailingSlashes(args.options.siteUrl); + await spp.assertSiteIsContentCenter(siteUrl); + + const result = await odata.getAllItems(`${siteUrl}/_api/machinelearning/models`); + await logger.log(result); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new SppModelListCommand(); \ No newline at end of file diff --git a/src/utils/spp.spec.ts b/src/utils/spp.spec.ts new file mode 100644 index 00000000000..cbd5e924d40 --- /dev/null +++ b/src/utils/spp.spec.ts @@ -0,0 +1,48 @@ + +import assert from 'assert'; +import sinon from 'sinon'; +import { spp } from './spp.js'; +import { sinonUtil } from './sinonUtil.js'; +import request from '../request.js'; + +describe('utils/spp', () => { + const siteUrl = 'https://contoso.sharepoint.com'; + afterEach(() => { + sinonUtil.restore([ + request.get + ]); + }); + + after(() => { + sinon.restore(); + }); + + it('calls api correctly and throw an error when site is not a content center using assertSiteIsContentCenter', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/_api/web?$select=WebTemplateConfiguration`) { + return { + WebTemplateConfiguration: 'SITEPAGEPUBLISHING#0' + }; + } + + throw 'Invalid request'; + }); + + await assert.rejects(spp.assertSiteIsContentCenter(siteUrl), Error('https://contoso.sharepoint.com is not a content site.')); + }); + + it('calls api correctly and does not throw an error when site is a content center using assertSiteIsContentCenter', async () => { + const stubGet = sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/_api/web?$select=WebTemplateConfiguration`) { + return { + WebTemplateConfiguration: 'CONTENTCTR#0' + }; + } + + throw 'Invalid request'; + }); + + await spp.assertSiteIsContentCenter(siteUrl); + assert(stubGet.calledOnce); + }); +}); \ No newline at end of file diff --git a/src/utils/spp.ts b/src/utils/spp.ts new file mode 100644 index 00000000000..83544d92284 --- /dev/null +++ b/src/utils/spp.ts @@ -0,0 +1,24 @@ +import request, { CliRequestOptions } from '../request.js'; + +export const spp = { + /** + * Asserts whether the specified site is a content center. + * @param siteUrl The URL of the site to check. + * @throws Error when the site is not a content center. + */ + async assertSiteIsContentCenter(siteUrl: string): Promise { + const requestOptions: CliRequestOptions = { + url: `${siteUrl}/_api/web?$select=WebTemplateConfiguration`, + headers: { + accept: 'application/json;odata=nometadata' + }, + responseType: 'json' + }; + + const response = await request.get<{ WebTemplateConfiguration: string }>(requestOptions); + + if (response.WebTemplateConfiguration !== 'CONTENTCTR#0') { + throw Error(`${siteUrl} is not a content site.`); + } + } +}; \ No newline at end of file From d6dcd68624acd9e00f3decdde9531069e92b6b81 Mon Sep 17 00:00:00 2001 From: Shantha Kumar T Date: Sun, 22 Sep 2024 15:44:12 +0530 Subject: [PATCH 37/43] Enhances 'spo list remove' with recycle option. Closes #6270 --- docs/docs/cmd/spo/list/list-remove.mdx | 11 +++- .../spo/commands/list/list-remove.spec.ts | 58 +++++++++++-------- src/m365/spo/commands/list/list-remove.ts | 21 ++++++- 3 files changed, 63 insertions(+), 27 deletions(-) diff --git a/docs/docs/cmd/spo/list/list-remove.mdx b/docs/docs/cmd/spo/list/list-remove.mdx index a0f6b2336c4..0ff33655876 100644 --- a/docs/docs/cmd/spo/list/list-remove.mdx +++ b/docs/docs/cmd/spo/list/list-remove.mdx @@ -9,7 +9,7 @@ Removes the specified list ```sh m365 spo list remove [options] ``` - + ## Options ```md definition-list @@ -22,6 +22,9 @@ m365 spo list remove [options] `-t, --title [title]` : Title of the list to remove. Specify either `id` or `title` but not both. +`--recycle` +: Instead of permanently deleting, send the list to the recycle bin. + `-f, --force` : Don't prompt for confirming removing the list. ``` @@ -42,6 +45,12 @@ Remove the list with a specific title located in a specific site. m365 spo list remove --webUrl https://contoso.sharepoint.com/sites/project-x --title 'List 1' ``` +Remove a list specified by id by sending it to the recycle bin instead of permanently removing it + +```sh +m365 spo list remove --webUrl https://contoso.sharepoint.com/sites/project-x --id 0cd891ef-afce-4e55-b836-fce03286cccf --recycle +``` + ## Response The command won't return a response on success. diff --git a/src/m365/spo/commands/list/list-remove.spec.ts b/src/m365/spo/commands/list/list-remove.spec.ts index 93c196a505c..eccdbb9fa65 100644 --- a/src/m365/spo/commands/list/list-remove.spec.ts +++ b/src/m365/spo/commands/list/list-remove.spec.ts @@ -15,6 +15,10 @@ import command from './list-remove.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.LIST_REMOVE, () => { + const listId = 'b2307a39-e878-458b-bc90-03bc578531d6'; + const webUrl = 'https://contoso.sharepoint.com'; + const listTitle = 'Documents'; + let log: any[]; let logger: Logger; let commandInfo: CommandInfo; @@ -44,9 +48,9 @@ describe(commands.LIST_REMOVE, () => { } }; requests = []; - sinon.stub(cli, 'promptForConfirmation').callsFake(() => { + sinon.stub(cli, 'promptForConfirmation').callsFake(async () => { promptIssued = true; - return Promise.resolve(false); + return false; }); promptIssued = false; }); @@ -95,14 +99,9 @@ describe(commands.LIST_REMOVE, () => { sinon.stub(request, 'post').callsFake(async (opts) => { requests.push(opts); - if ((opts.url as string).indexOf(`/_api/web/lists(guid'`) > -1) { - if (opts.headers && - opts.headers.accept && - (opts.headers.accept as string).indexOf('application/json') === 0) { - return; - } + if (opts.url === `${webUrl}/_api/web/lists(guid'${listId}')`) { + return; } - throw 'Invalid request'; }); @@ -111,9 +110,7 @@ describe(commands.LIST_REMOVE, () => { await command.action(logger, { options: { id: 'b2307a39-e878-458b-bc90-03bc578531d6', webUrl: 'https://contoso.sharepoint.com' } }); let correctRequestIssued = false; requests.forEach(r => { - if (r.url.indexOf(`/_api/web/lists(guid'`) > -1 && - r.headers.accept && - r.headers.accept.indexOf('application/json') === 0) { + if (r.url === `${webUrl}/_api/web/lists(guid'${listId}')`) { correctRequestIssued = true; } }); @@ -123,13 +120,8 @@ describe(commands.LIST_REMOVE, () => { it('removes the list when prompt confirmed by option', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { requests.push(opts); - - if ((opts.url as string).indexOf(`/_api/web/lists(guid'`) > -1) { - if (opts.headers && - opts.headers.accept && - (opts.headers.accept as string).indexOf('application/json') === 0) { - return; - } + if (opts.url === `${webUrl}/_api/web/lists(guid'${listId}')`) { + return; } throw 'Invalid request'; @@ -138,15 +130,34 @@ describe(commands.LIST_REMOVE, () => { await command.action(logger, { options: { id: 'b2307a39-e878-458b-bc90-03bc578531d6', webUrl: 'https://contoso.sharepoint.com', force: true } }); let correctRequestIssued = false; requests.forEach(r => { - if (r.url.indexOf(`/_api/web/lists(guid'`) > -1 && - r.headers.accept && - r.headers.accept.indexOf('application/json') === 0) { + if (r.url === `${webUrl}/_api/web/lists(guid'${listId}')`) { correctRequestIssued = true; } }); assert(correctRequestIssued); }); + it('uses correct API url when recycle option is passed', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `${webUrl}/_api/web/lists/GetByTitle('${listTitle}')/recycle`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + title: 'Documents', + recycle: true, + webUrl: 'https://contoso.sharepoint.com', + force: true + } + }); + + assert(postStub.calledOnce); + }); + it('command correctly handles list get reject request', async () => { const error = { error: { @@ -159,7 +170,8 @@ describe(commands.LIST_REMOVE, () => { } }; sinon.stub(request, 'post').callsFake(async (opts) => { - if ((opts.url as string).indexOf('/_api/web/lists/GetByTitle(') > -1) { + + if (opts.url === `${webUrl}/_api/web/lists/GetByTitle('${listTitle}')`) { throw error; } diff --git a/src/m365/spo/commands/list/list-remove.ts b/src/m365/spo/commands/list/list-remove.ts index 1c45a3185a8..44d77aeb348 100644 --- a/src/m365/spo/commands/list/list-remove.ts +++ b/src/m365/spo/commands/list/list-remove.ts @@ -15,6 +15,7 @@ interface Options extends GlobalOptions { webUrl: string; id?: string; title?: string; + recycle?: boolean; force?: boolean; } @@ -32,6 +33,7 @@ class SpoListRemoveCommand extends SpoCommand { this.#initTelemetry(); this.#initOptions(); + this.#initTypes(); this.#initValidators(); this.#initOptionSets(); } @@ -39,9 +41,10 @@ class SpoListRemoveCommand extends SpoCommand { #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { - id: (!(!args.options.id)).toString(), - title: (!(!args.options.title)).toString(), - force: (!(!args.options.force)).toString() + id: typeof args.options.id !== 'undefined', + title: typeof args.options.title !== 'undefined', + force: !!args.options.force, + recycle: !!args.options.recycle }); }); } @@ -57,6 +60,9 @@ class SpoListRemoveCommand extends SpoCommand { { option: '-t, --title [title]' }, + { + option: '--recycle' + }, { option: '-f, --force' } @@ -85,6 +91,11 @@ class SpoListRemoveCommand extends SpoCommand { this.optionSets.push({ options: ['id', 'title'] }); } + #initTypes(): void { + this.types.string.push('id', 'title', 'webUrl'); + this.types.boolean.push('force', 'recycle'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { const removeList = async (): Promise => { if (this.verbose) { @@ -100,6 +111,10 @@ class SpoListRemoveCommand extends SpoCommand { requestUrl = `${args.options.webUrl}/_api/web/lists/GetByTitle('${formatting.encodeQueryParameter(args.options.title as string)}')`; } + if (args.options.recycle) { + requestUrl += `/recycle`; + } + const requestOptions: CliRequestOptions = { url: requestUrl, method: 'POST', From cff20c1c3bac9538e05c4b3fbd8cc2cc15294748 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 11 Oct 2024 10:49:54 +0200 Subject: [PATCH 38/43] Removes duplicate drive util. Closes #6393 --- src/m365/file/commands/file-move.ts | 8 ++-- src/utils/driveUtil.ts | 63 ----------------------------- 2 files changed, 4 insertions(+), 67 deletions(-) delete mode 100644 src/utils/driveUtil.ts diff --git a/src/m365/file/commands/file-move.ts b/src/m365/file/commands/file-move.ts index 1c3ab9688d5..0b7b535c4fe 100644 --- a/src/m365/file/commands/file-move.ts +++ b/src/m365/file/commands/file-move.ts @@ -7,7 +7,7 @@ import commands from '../commands.js'; import request, { CliRequestOptions } from '../../../request.js'; import { spo } from '../../../utils/spo.js'; import { urlUtil } from '../../../utils/urlUtil.js'; -import { driveUtil } from '../../../utils/driveUtil.js'; +import { drive } from '../../../utils/drive.js'; import { validation } from '../../../utils/validation.js'; interface CommandArgs { @@ -115,9 +115,9 @@ class FileMoveCommand extends GraphCommand { private async getDriveIdAndItemId(webUrl: string, folderUrl: string, sourceUrl: string, logger: Logger, verbose?: boolean): Promise<{ driveId: string, itemId: string }> { const siteId: string = await spo.getSiteId(webUrl, logger, verbose); - const drive: Drive = await driveUtil.getDriveByUrl(siteId, new URL(folderUrl)); - const itemId: string = await driveUtil.getDriveItemId(drive, new URL(folderUrl)); - return { driveId: drive.id as string, itemId }; + const driveDetails: Drive = await drive.getDriveByUrl(siteId, new URL(folderUrl), logger, verbose); + const itemId: string = await drive.getDriveItemId(driveDetails, new URL(folderUrl), logger, verbose); + return { driveId: driveDetails.id!, itemId }; } private getRequestOptions(sourceDriveId: string, sourceItemId: string, targetDriveId: string, targetItemId: string, newName: string | undefined, sourcePath: string, nameConflictBehavior: string | undefined): CliRequestOptions { diff --git a/src/utils/driveUtil.ts b/src/utils/driveUtil.ts deleted file mode 100644 index ff63bf207d5..00000000000 --- a/src/utils/driveUtil.ts +++ /dev/null @@ -1,63 +0,0 @@ - -import { Drive, DriveItem } from '@microsoft/microsoft-graph-types'; -import request, { CliRequestOptions } from "../request.js"; - -export const driveUtil = { - /** - * Retrieves the Drive associated with the specified site and URL. - * @param siteId Site ID - * @param url Drive URL - * @returns The Drive associated with the drive URL. - */ - async getDriveByUrl(siteId: string, url: URL): Promise { - const requestOptions: CliRequestOptions = { - url: `https://graph.microsoft.com/v1.0/sites/${siteId}/drives?$select=webUrl,id`, - headers: { - accept: 'application/json;odata.metadata=none' - }, - responseType: 'json' - }; - - const drives = await request.get<{ value: Drive[] }>(requestOptions); - - const lowerCaseFolderUrl: string = url.href.toLowerCase(); - - const drive: Drive | undefined = drives.value - .sort((a, b) => (b.webUrl as string).localeCompare(a.webUrl as string)) - .find((d: Drive) => { - const driveUrl: string = (d.webUrl as string).toLowerCase(); - - return lowerCaseFolderUrl.startsWith(driveUrl) && - (driveUrl.length === lowerCaseFolderUrl.length || - lowerCaseFolderUrl[driveUrl.length] === '/'); - }); - - if (!drive) { - throw `Drive '${url.href}' not found`; - } - - return drive; - }, - - /** - * Retrieves the ID of a drive item (file, folder, etc.) associated with the given drive and item URL. - * @param drive The Drive object containing the item - * @param itemUrl Item URL - * @returns Drive item ID - */ - async getDriveItemId(drive: Drive, itemUrl: URL): Promise { - const relativeItemUrl: string = itemUrl.href.replace(new RegExp(`${drive.webUrl}`, 'i'), '').replace(/\/+$/, ''); - - const requestOptions: CliRequestOptions = { - url: `https://graph.microsoft.com/v1.0/drives/${drive.id}/root${relativeItemUrl ? `:${relativeItemUrl}` : ''}?$select=id`, - headers: { - accept: 'application/json;odata.metadata=none' - }, - responseType: 'json' - }; - - const driveItem = await request.get(requestOptions); - - return driveItem?.id as string; - } -}; \ No newline at end of file From ca0430e15cee4e9070a11afacfa7b36810bd7658 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 14 Oct 2024 12:36:37 +0200 Subject: [PATCH 39/43] Adds bypassSharedLock option to 'spo file remove' command. Closes #6313 --- docs/docs/cmd/spo/file/file-remove.mdx | 17 +++++++++++++---- src/m365/spo/commands/file/file-remove.spec.ts | 13 +++++++++++++ src/m365/spo/commands/file/file-remove.ts | 11 ++++++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/docs/docs/cmd/spo/file/file-remove.mdx b/docs/docs/cmd/spo/file/file-remove.mdx index 6d266fab518..d22ccf18cfd 100644 --- a/docs/docs/cmd/spo/file/file-remove.mdx +++ b/docs/docs/cmd/spo/file/file-remove.mdx @@ -31,6 +31,9 @@ m365 spo page template remove `--recycle` : Recycle the file instead of actually deleting it. +`--bypassSharedLock` +: Remove the file even if it is locked for shared use. + `-f, --force` : Don't prompt for confirming removing the file. ``` @@ -39,22 +42,28 @@ m365 spo page template remove ## Examples -Remove file by ID. +Remove a file by ID. ```sh m365 spo file remove --webUrl https://contoso.sharepoint.com/sites/project-x --id 0cd891ef-afce-4e55-b836-fce03286cccf ``` -Remove the file by site-relative URL. +Remove a file by site-relative URL. ```sh m365 spo file remove --webUrl https://contoso.sharepoint.com/sites/project-x --url "/Shared Documents/Test.docx" ``` -Move the file by server-relative URL to the recycle bin. +Remove a file by server-relative URL to the recycle bin. + +```sh +m365 spo file remove --webUrl https://contoso.sharepoint.com/sites/project-x --url "/sites/project-x/Shared Documents/Test.docx" --recycle +``` + +Remove a file that is locked ```sh -m365 spo file remove --webUrl https://contoso.sharepoint.com/sites/project-x --url "/sites/project-x/SharedDocuments/Test.docx" --recycle +m365 spo file remove --webUrl https://contoso.sharepoint.com/sites/project-x --url "/Shared Documents/Test.docx" --bypassSharedLock ``` Remove a page template by site-relative URL. diff --git a/src/m365/spo/commands/file/file-remove.spec.ts b/src/m365/spo/commands/file/file-remove.spec.ts index 98eba7652b0..68ffb3423f7 100644 --- a/src/m365/spo/commands/file/file-remove.spec.ts +++ b/src/m365/spo/commands/file/file-remove.spec.ts @@ -552,6 +552,19 @@ describe(commands.FILE_REMOVE, () => { assert(correctRequestIssued); }); + it('correctly bypasses a shared lock', async () => { + const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://contoso.sharepoint.com/_api/web/GetFileByServerRelativePath(DecodedUrl='%2F0cd891ef-afce-4e55-b836-fce03286cccf')`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { webUrl: 'https://contoso.sharepoint.com', url: '0cd891ef-afce-4e55-b836-fce03286cccf', force: true, bypassSharedLock: true } }); + assert.deepStrictEqual(postStub.firstCall.args[0].headers?.Prefer, 'bypass-shared-lock'); + }); + it('command correctly handles file remove reject request', async () => { const err = 'An error has occurred'; const error = { diff --git a/src/m365/spo/commands/file/file-remove.ts b/src/m365/spo/commands/file/file-remove.ts index 5a1c883f4b4..a0147782536 100644 --- a/src/m365/spo/commands/file/file-remove.ts +++ b/src/m365/spo/commands/file/file-remove.ts @@ -17,6 +17,7 @@ export interface Options extends GlobalOptions { id?: string; url?: string; recycle?: boolean; + bypassSharedLock?: boolean; force?: boolean; } @@ -49,6 +50,7 @@ class SpoFileRemoveCommand extends SpoCommand { id: typeof args.options.id !== 'undefined', url: typeof args.options.url !== 'undefined', recycle: !!args.options.recycle, + bypassSharedLock: !!args.options.bypassSharedLock, force: !!args.options.force }); }); @@ -68,6 +70,9 @@ class SpoFileRemoveCommand extends SpoCommand { { option: '--recycle' }, + { + option: '--bypassSharedLock' + }, { option: '-f, --force' } @@ -98,7 +103,7 @@ class SpoFileRemoveCommand extends SpoCommand { #initTypes(): void { this.types.string.push('webUrl', 'id', 'url'); - this.types.boolean.push('recycle', 'force'); + this.types.boolean.push('recycle', 'bypassSharedLock', 'force'); } protected getExcludedOptionsWithUrls(): string[] | undefined { @@ -136,6 +141,10 @@ class SpoFileRemoveCommand extends SpoCommand { responseType: 'json' }; + if (args.options.bypassSharedLock) { + requestOptions.headers!.Prefer = 'bypass-shared-lock'; + } + try { await request.post(requestOptions); } From 878ed5f35e1092d4dc541abbe336aae9e0c0b66b Mon Sep 17 00:00:00 2001 From: Jwaegebaert <38426621+Jwaegebaert@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:12:00 +0200 Subject: [PATCH 40/43] Updates docs dependencies --- docs/package-lock.json | 349 +++++++++++++++++++++++++++++++++++++++-- docs/package.json | 5 +- 2 files changed, 337 insertions(+), 17 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 10d79e52642..d629dab2640 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -21,15 +21,14 @@ "prism-react-renderer": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "sass": "^1.78.0", "unist-util-visit": "^5.0.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.5.2", "@docusaurus/tsconfig": "^3.5.2", - "asciinema-player": "^3.8.0", + "asciinema-player": "^3.8.1", "docusaurus-node-polyfills": "^1.0.0", - "typescript": "^5.5.4" + "typescript": "^5.6.3" }, "engines": { "node": ">=18.0" @@ -3073,6 +3072,279 @@ "node": ">= 8" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "peer": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -4225,9 +4497,9 @@ } }, "node_modules/asciinema-player": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.8.0.tgz", - "integrity": "sha512-yFoAcjFK9WJ0D+aagkT0YXOWRbyXoOe/TQHq07oQP6prItXQkWn46fdvUb6zqJu2AywmY8VjBEwZ6ciL8IbezQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.8.1.tgz", + "integrity": "sha512-NkpbFg81Y6iJFpDRndakLCQ0G26XSpvuT3vJTFjMRgHb26lqHgRNY9gun54e5MehZ4fEDNYkMZv+z6MfZ8c2aA==", "dev": true, "dependencies": { "@babel/runtime": "^7.21.0", @@ -6508,6 +6780,18 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "peer": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -8714,7 +8998,8 @@ "node_modules/immutable": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==" + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "peer": true }, "node_modules/import-fresh": { "version": "3.3.0", @@ -12314,6 +12599,12 @@ "tslib": "^2.0.3" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "peer": true + }, "node_modules/node-emoji": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", @@ -14696,11 +14987,13 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz", - "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", + "version": "1.79.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz", + "integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==", + "peer": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -14792,6 +15085,34 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "peer": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -15924,9 +16245,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/docs/package.json b/docs/package.json index 6022848048d..d1c744725c6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -28,15 +28,14 @@ "prism-react-renderer": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "sass": "^1.78.0", "unist-util-visit": "^5.0.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.5.2", "@docusaurus/tsconfig": "^3.5.2", - "asciinema-player": "^3.8.0", + "asciinema-player": "^3.8.1", "docusaurus-node-polyfills": "^1.0.0", - "typescript": "^5.5.4" + "typescript": "^5.6.3" }, "browserslist": { "production": [ From 4ca7c62a8b94bd9f793a22cc90cad3b3bc24e2d0 Mon Sep 17 00:00:00 2001 From: Milan Holemans <11723921+milanholemans@users.noreply.github.com> Date: Fri, 18 Oct 2024 00:09:26 +0200 Subject: [PATCH 41/43] Updates release notes --- docs/docs/about/release-notes.mdx | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/docs/about/release-notes.mdx b/docs/docs/about/release-notes.mdx index 0bfb5b76831..c6bb619a544 100644 --- a/docs/docs/about/release-notes.mdx +++ b/docs/docs/about/release-notes.mdx @@ -6,16 +6,27 @@ sidebar_position: 3 ## v10.0.0 (beta) +### New commands + +**SharePoint:** + +- [spo tenant site membership list](../cmd/spo/tenant/tenant-site-membership-list.mdx) - retrieves information about default site groups' membership [#5980](https://github.com/pnp/cli-microsoft365/issues/5980) + +**SharePoint Premium:** + +- [spp model list](../cmd/spp/model/model-list.mdx) - retrieves the list of unstructured document processing models [#6103](https://github.com/pnp/cli-microsoft365/issues/6103) + ### Changes -- extended `m365 login` with `--ensure` [#5217](https://github.com/pnp/cli-microsoft365/issues/5217) +- extended [login](../cmd/login.mdx) command with `--ensure` option [#5217](https://github.com/pnp/cli-microsoft365/issues/5217) - updated docs thumbnail [#6302](https://github.com/pnp/cli-microsoft365/issues/6302) -- fixed casing and autocomplete for enums based on zod +- fixed casing and autocomplete for enums based on zod [#6373](https://github.com/pnp/cli-microsoft365/pull/6373) - updated docs contribution guide with Zod [#6322](https://github.com/pnp/cli-microsoft365/issues/6322) -- removed obsolete example [#6272](https://github.com/pnp/cli-microsoft365/issues/6272) -- fixed 'm365 setup' app registration to use 'CLI for M365' [#6367](https://github.com/pnp/cli-microsoft365/issues/6367) -- fixed example in 'spo site sharingpermission set' -- updated 'entra m365group user remove' command [#6058](https://github.com/pnp/cli-microsoft365/issues/6058) +- removed obsolete docs example [#6272](https://github.com/pnp/cli-microsoft365/issues/6272) +- fixed [setup](../cmd/setup.mdx) command's app registration name [#6367](https://github.com/pnp/cli-microsoft365/issues/6367) +- added ability to specify multiple users for command [entra m365group user remove](../cmd/entra/m365group/m365group-user-remove.mdx) command [#6058](https://github.com/pnp/cli-microsoft365/issues/6058) +- enhanced [spo list remove](../cmd/spo/list/list-remove.mdx) command with `--recylce` flag [#6270](https://github.com/pnp/cli-microsoft365/issues/6270) +- enhanced [spo file remove](../cmd/spo/file/file-remove.mdx) command with `--bypassSharedLock` flag [#6313](https://github.com/pnp/cli-microsoft365/issues/6313) ### ⚠️ Breaking changes @@ -46,10 +57,10 @@ sidebar_position: 3 - updated [cli doctor](../cmd/cli/cli-doctor.mdx) command output [#5923](https://github.com/pnp/cli-microsoft365/issues/5923) - removed duplicate properties from [teams tab list](../cmd/teams/tab/tab-list.mdx) command [#5900](https://github.com/pnp/cli-microsoft365/issues/5900) - renamed `entra group user ` to `entra group member `. [#6396](https://github.com/pnp/cli-microsoft365/issues/6396) -- updated setting users in `entra m365group set` [#6061](https://github.com/pnp/cli-microsoft365/issues/6061) +- updated setting users in [entra m365group set](../cmd/entra/m365group/m365group-set.mdx) [#6061](https://github.com/pnp/cli-microsoft365/issues/6061) - removed aad options and aliasses [#5823](https://github.com/pnp/cli-microsoft365/issues/5823), [#5676](https://github.com/pnp/cli-microsoft365/issues/5676) -- removed deprecated option `username` from `entra m365group user set` command [#6224](https://github.com/pnp/cli-microsoft365/issues/6224) -- updated endpoint for `spo folder move` command [#6154](https://github.com/pnp/cli-microsoft365/issues/6154) +- removed deprecated option `username` from [entra m365group user set](../cmd/entra/m365group/m365group-user-set.mdx) command [#6224](https://github.com/pnp/cli-microsoft365/issues/6224) +- updated endpoint for [spo folder move](../cmd/spo/folder/folder-move.mdx) command [#6154](https://github.com/pnp/cli-microsoft365/issues/6154) ## [v9.1.0](https://github.com/pnp/cli-microsoft365/releases/tag/v9.1.0) From 42d0be72089b6e052e2956a9cc006f6476a8d801 Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Tue, 22 Oct 2024 08:41:27 +0200 Subject: [PATCH 42/43] change email address --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7133ed14288..ee9cbcfef27 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,7 @@ "Levert, Sebastien ", "Lingstuyl, Martin ", "Macháček, Martin ", - "Maestrini Tobias ", + "Maestrini, Tobias ", "Maillot, Michaël ", "Mastykarz, Waldek ", "McDonnell, Kevin ", From f0afacf968309dec0ab1d155cc1617a91708ae5f Mon Sep 17 00:00:00 2001 From: Tobias Maestrini Date: Tue, 22 Oct 2024 08:46:45 +0200 Subject: [PATCH 43/43] fix test definition to cover new implementation to retrieve the site's standard list --- src/m365/spo/commands/list/list-get.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/m365/spo/commands/list/list-get.spec.ts b/src/m365/spo/commands/list/list-get.spec.ts index bc87a8fa190..4dec616c95c 100644 --- a/src/m365/spo/commands/list/list-get.spec.ts +++ b/src/m365/spo/commands/list/list-get.spec.ts @@ -1291,7 +1291,6 @@ describe(commands.LIST_GET, () => { await command.action(logger, { options: { - title: 'Documents', webUrl: 'https://contoso.sharepoint.com' } });