Compare commits

..

2 Commits

Author SHA1 Message Date
Тимур Городецкий efbe732995 fix: манифесты — колонки, которые пишет код, но не было в схеме (#2)
Без них списки, пересозданные по манифестам, ломали бы шаг 1 СТ
(requestPositionId), сохранение офферов (offerFileId), добавление поставщика
из базы (контакты в «Поставщики») и позиции ОИД (RequestCode, RequestUrl).
Названия и типы — как в живых списках /sites/sed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 14:33:05 +05:00
Тимур Городецкий 40839cc8d8 feat: Compare-Manifests.ps1 — сверка манифестов с живыми списками (#2)
Манифесты отстают от живых списков, когда колонки добавляют руками или кодом,
и пересоздание по ним ломает функции. Скрипт только читает и показывает, чего
нет в манифесте, чего нет на сайте и где отличаются тип, варианты или лукап.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 14:29:24 +05:00
4 changed files with 228 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
<#
.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 "<id>" -Tenant "tnsukz.onmicrosoft.com" -CertificatePath "<pfx>" -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
+21
View File
@@ -103,6 +103,27 @@ cd provisioning
Лог: `[+]` создано, `[=]` уже было, `[~]` обновлено, `[!]` предупреждение. Лог: `[+]` создано, `[=]` уже было, `[~]` обновлено, `[!]` предупреждение.
## Сверка манифестов с живыми списками
Манифест отстаёт, когда колонки добавляют руками или кодом. Перед провижинингом на рабочем
сайте и после ручных правок списков — сверка (только чтение):
```powershell
./Compare-Manifests.ps1 `
-SiteUrl "https://tnsukz.sharepoint.com/sites/sed" `
-ClientId "<client-id>" `
-Tenant "tnsukz.onmicrosoft.com" `
-CertificatePath "<путь к .pfx>" -CertificatePassword "<пароль>"
```
`-` есть в манифесте, нет в живом (создаст Provision-Lists.ps1), `+` есть в живом, нет в
манифесте (при пересоздании пропадёт — дописать в манифест), `~` отличаются тип, варианты или лукап.
## Добавить новый список на рабочий сайт
Полный прогон Provision-Lists.ps1 синхронизирует варианты Choice у ВСЕХ живых списков с
манифестами. Новый список добавляй точечно — `-Lists "Название"`: остальные списки не трогаются.
## Формат манифеста ## Формат манифеста
```jsonc ```jsonc
@@ -120,6 +120,11 @@
"title": "Количество", "title": "Количество",
"type": "Number", "type": "Number",
"decimals": 3 "decimals": 3
},
{
"internalName": "requestPositionId",
"title": "requestPositionId",
"type": "Text"
} }
] ]
}, },
@@ -200,6 +205,11 @@
"internalName": "deliveryDistanceKm", "internalName": "deliveryDistanceKm",
"title": "deliveryDistanceKm", "title": "deliveryDistanceKm",
"type": "Number" "type": "Number"
},
{
"internalName": "offerFileId",
"title": "offerFileId",
"type": "Number"
} }
] ]
}, },
@@ -465,6 +475,31 @@
"internalName": "adataCheckedAt", "internalName": "adataCheckedAt",
"title": "Дата проверки Adata", "title": "Дата проверки Adata",
"type": "DateTime" "type": "DateTime"
},
{
"internalName": "address",
"title": "Адрес",
"type": "Note"
},
{
"internalName": "contactPerson",
"title": "Контактное лицо",
"type": "Note"
},
{
"internalName": "contactInfo",
"title": "Контактная информация",
"type": "Note"
},
{
"internalName": "email",
"title": "Email",
"type": "Text"
},
{
"internalName": "website",
"title": "Сайт",
"type": "Text"
} }
] ]
}, },
@@ -101,6 +101,16 @@
"title": "Сумма из сметы", "title": "Сумма из сметы",
"type": "Number", "type": "Number",
"decimals": 2 "decimals": 2
},
{
"internalName": "RequestCode",
"title": "Номер из заявки",
"type": "Text"
},
{
"internalName": "RequestUrl",
"title": "Ссылка на заявку",
"type": "Text"
} }
] ]
}, },