<# .SYNOPSIS Сверяет манифесты provisioning.json с живыми списками сайта. Только чтение. .DESCRIPTION Манифесты снимались с живых списков (Export-ListSchema.ps1), но колонки потом добавляют руками и кодом — манифест отстаёт, и пересоздание списков по нему ломает функции. Запускать перед Provision-Lists.ps1 на рабочем сайте и после ручных правок списков. Показывает по каждому списку: - в манифесте, НЕТ в живом — колонку создаст Provision-Lists.ps1; + в живом, НЕТ в манифесте — манифест отстал: при пересоздании колонки не будет; ~ тип / варианты / лукап — колонка есть, но отличается. .PARAMETER SiteUrl Сайт со списками, например https://tnsukz.sharepoint.com/sites/sed .PARAMETER ClientId Client Id приложения с доступом к сайту (app-only). .PARAMETER Tenant Тенант, например tnsukz.onmicrosoft.com .PARAMETER Thumbprint Отпечаток сертификата в хранилище CurrentUser\My. Либо он, либо CertificatePath. .PARAMETER CertificatePath Путь к файлу сертификата .pfx — если сертификат не установлен в хранилище. .PARAMETER CertificatePassword Пароль к .pfx из CertificatePath. .PARAMETER ManifestsDir Папка с манифестами. По умолчанию — provisioning/manifests рядом со скриптом. .PARAMETER Only Сверять только манифесты указанных форм (по полю form). По умолчанию — все. .EXAMPLE ./Compare-Manifests.ps1 -SiteUrl "https://tnsukz.sharepoint.com/sites/sed" ` -ClientId "" -Tenant "tnsukz.onmicrosoft.com" -CertificatePath "" -CertificatePassword "<пароль>" #> param( [Parameter(Mandatory = $true)][string]$SiteUrl, [Parameter(Mandatory = $true)][string]$ClientId, [Parameter(Mandatory = $true)][string]$Tenant, [string]$Thumbprint, [string]$CertificatePath, [string]$CertificatePassword, [string]$ManifestsDir = "$PSScriptRoot/manifests", [string[]]$Only ) $ErrorActionPreference = 'Stop' # Служебные поля SharePoint и Syntex: в манифестах их нет и быть не должно — в «+ в живом» не показываем. $SYSTEM_FIELDS = @( 'Title', 'ContentType', 'Attachments', 'Edit', 'LinkTitle', 'LinkTitleNoMenu', 'DocIcon', 'ItemChildCount', 'FolderChildCount', 'AppAuthor', 'AppEditor', 'ComplianceAssetId', '_ColorTag', '_ComplianceFlags', '_ComplianceTag', '_ComplianceTagWrittenTime', '_ComplianceTagUserId', '_IsRecord', 'FileLeafRef', '_DisplayName', 'SelectTitle', 'FileRef', '_UIVersionString', 'ParentVersionString', 'ParentLeafName', 'CheckoutUser', 'MediaServiceImageTags', '_ExtendedDescription', '_ModelId', '_dlc_DocId', '_dlc_DocIdUrl' ) $SYNTEX_FIELD_PREFIX = 'Prime' # Тип живой колонки → тип манифеста (Currency/MultiChoice/UserMulti/LookupMulti сводятся к базовому). $TYPE_MAP = @{ 'Text' = 'Text'; 'Note' = 'Note'; 'Number' = 'Number'; 'Currency' = 'Number'; 'Choice' = 'Choice'; 'MultiChoice' = 'Choice'; 'DateTime' = 'DateTime'; 'Boolean' = 'Boolean'; 'User' = 'User'; 'UserMulti' = 'User'; 'Lookup' = 'Lookup'; 'LookupMulti' = 'Lookup' } $listTitleById = @{} function Get-ListTitleById { param([string]$Id) if (-not $listTitleById.ContainsKey($Id)) { $listTitleById[$Id] = (Get-PnPList -Identity $Id -ErrorAction SilentlyContinue).Title } return $listTitleById[$Id] } function Compare-Column { param($Col, $Field) $diffs = @() $liveType = $TYPE_MAP[$Field.TypeAsString] if (-not $liveType) { $liveType = $Field.TypeAsString } if ($liveType -ne $Col.type) { $diffs += " ~ тип $($Col.internalName): манифест $($Col.type), живой $($Field.TypeAsString)" } if ($Col.type -eq 'Choice') { $liveChoices = @($Field.Choices) $manifestChoices = @($Col.choices) $onlyLive = @($liveChoices | Where-Object { $manifestChoices -notcontains $_ }) $onlyManifest = @($manifestChoices | Where-Object { $liveChoices -notcontains $_ }) if ($onlyLive.Count -gt 0) { $diffs += " ~ варианты $($Col.internalName): только в живом — $($onlyLive -join ', ')" } if ($onlyManifest.Count -gt 0) { $diffs += " ~ варианты $($Col.internalName): только в манифесте — $($onlyManifest -join ', ')" } } if ($Col.type -eq 'Lookup') { $schema = [xml]$Field.SchemaXml $target = Get-ListTitleById ($schema.Field.List -replace '[{}]', '') if ($target -ne $Col.lookupList) { $diffs += " ~ лукап $($Col.internalName): манифест → «$($Col.lookupList)», живой → «$target»" } if ($schema.Field.ShowField -and $schema.Field.ShowField -ne $Col.lookupShowField) { $diffs += " ~ лукап $($Col.internalName): показ манифест $($Col.lookupShowField), живой $($schema.Field.ShowField)" } } return $diffs } if ($Thumbprint) { Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Tenant $Tenant -Thumbprint $Thumbprint } elseif ($CertificatePath) { $securePassword = ConvertTo-SecureString $CertificatePassword -AsPlainText -Force Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Tenant $Tenant -CertificatePath $CertificatePath -CertificatePassword $securePassword } else { throw "Укажите -Thumbprint или -CertificatePath" } $totalDiffs = 0 foreach ($file in Get-ChildItem -Path $ManifestsDir -Filter '*.provisioning.json') { $manifest = Get-Content -Path $file.FullName -Raw -Encoding UTF8 | ConvertFrom-Json if ($Only -and ($Only -notcontains $manifest.form)) { continue } Write-Host "`n######## $($manifest.form)" -ForegroundColor Cyan foreach ($ls in $manifest.lists) { if (-not (Get-PnPList -Identity $ls.title -ErrorAction SilentlyContinue)) { Write-Host "== «$($ls.title)»: списка НЕТ на сайте" -ForegroundColor Yellow $totalDiffs++ continue } $allFields = Get-PnPField -List $ls.title $diffs = @() foreach ($col in $ls.columns) { $field = $allFields | Where-Object { $_.InternalName -eq $col.internalName } | Select-Object -First 1 if (-not $field) { $diffs += " - в манифесте, НЕТ в живом: $($col.internalName) [$($col.type)]"; continue } $diffs += Compare-Column -Col $col -Field $field } $customFields = $allFields | Where-Object { -not $_.Hidden -and -not $_.ReadOnlyField -and -not $_.FromBaseType -and $SYSTEM_FIELDS -notcontains $_.InternalName -and -not $_.InternalName.StartsWith($SYNTEX_FIELD_PREFIX) } foreach ($field in $customFields) { if (-not ($ls.columns | Where-Object { $_.internalName -eq $field.InternalName })) { $diffs += " + в живом, НЕТ в манифесте: $($field.InternalName) «$($field.Title)» [$($field.TypeAsString)]" } } if ($diffs.Count -eq 0) { Write-Host "== «$($ls.title)»: совпадает" -ForegroundColor DarkGray } else { Write-Host "== «$($ls.title)»:" -ForegroundColor White $diffs | ForEach-Object { Write-Host $_ } $totalDiffs += $diffs.Count } } } Write-Host "`nРасхождений: $totalDiffs" -ForegroundColor Cyan Disconnect-PnPOnline