mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-09-07 18:50:53 +03:00
Compare commits
39
Commits
@@ -34,6 +34,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -Confi
|
||||
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
|
||||
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
|
||||
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
|
||||
| `sort-childObjects` | вид, напр. `Catalog` (batch `;;`), либо пусто | Упорядочить ChildObjects по имени внутри вида. Без значения — все виды, кроме четырёх (см. reference) |
|
||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||
|
||||
@@ -39,6 +39,20 @@
|
||||
|
||||
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||
|
||||
## sort-childObjects
|
||||
|
||||
Упорядочивает объекты в `<ChildObjects>` по имени **внутри вида**. Значение — имя вида (`Catalog`, `Role`, …), batch через `;;`. Без значения обрабатываются все виды, какие есть в файле.
|
||||
|
||||
```
|
||||
-Operation sort-childObjects — все виды, кроме перечисленных ниже
|
||||
-Operation sort-childObjects -Value "Catalog" — только справочники
|
||||
-Operation sort-childObjects -Value "Catalog ;; Role"
|
||||
```
|
||||
|
||||
Не сортируются, пока вид не назван явно: `CommonAttribute`, `Subsystem`, `CommandGroup`, `Language`.
|
||||
|
||||
Вызов без значения дополнительно ставит группы видов в канонический порядок; вызов с явным видом трогает только имена внутри него.
|
||||
|
||||
## add-defaultRole / remove-defaultRole / set-defaultRoles
|
||||
|
||||
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# cf-edit v1.23 — Edit 1C configuration root (Configuration.xml)
|
||||
# cf-edit v1.29 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
||||
[string]$DefinitionFile,
|
||||
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
|
||||
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page","sort-childObjects")]
|
||||
[string]$Operation,
|
||||
[string]$Value,
|
||||
[switch]$NoValidate
|
||||
@@ -277,27 +277,27 @@ foreach ($child in $script:propsEl.ChildNodes) {
|
||||
}
|
||||
Info "Configuration: $($script:objName)"
|
||||
|
||||
# --- Canonical type order for ChildObjects (44 types) ---
|
||||
# --- Canonical type order for ChildObjects (46 types) ---
|
||||
$script:typeOrder = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
# --- Type → on-disk directory name (plural) ---
|
||||
$script:typeToDir = @{
|
||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
||||
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
||||
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "PaletteColor"="PaletteColors"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
||||
@@ -306,7 +306,7 @@ $script:typeToDir = @{
|
||||
"Report"="Reports"; "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
|
||||
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"; "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "IntegrationService"="IntegrationServices"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
}
|
||||
|
||||
# --- XML manipulation helpers (from subsystem-edit pattern) ---
|
||||
@@ -376,6 +376,21 @@ function Import-Fragment([string]$xmlString) {
|
||||
}
|
||||
|
||||
# --- Parse batch value (split by ;;) ---
|
||||
|
||||
# Имя вида из пользовательского ввода → каноническое имя или $null.
|
||||
# Ввод прощающий: регистр не важен, принимается имя каталога выгрузки (Catalogs → Catalog)
|
||||
# и русское имя вида в единственном и множественном числе.
|
||||
function Resolve-TypeName([string]$token) {
|
||||
$key = "$token".Trim()
|
||||
if (-not $key) { return $null }
|
||||
foreach ($canon in $script:typeOrder) { if ($canon -eq $key) { return $canon } }
|
||||
$byDir = $script:dirToType[$key.ToLowerInvariant()]
|
||||
if ($byDir) { return $byDir }
|
||||
$ru = $script:ruTypeMap[$key.ToLowerInvariant()]
|
||||
if ($ru) { return $ru }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Parse-BatchValue([string]$val) {
|
||||
$items = @()
|
||||
foreach ($part in $val.Split(";;")) {
|
||||
@@ -441,6 +456,220 @@ function Do-ModifyProperty([string]$batchVal) {
|
||||
}
|
||||
|
||||
# --- Operation: add-childObject ---
|
||||
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
# остаётся рабочим каталогом проекта.
|
||||
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Get-NewObjectPosition([string]$cfgDir) {
|
||||
try {
|
||||
if (-not $cfgDir) { $cfgDir = "." }
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||
if (-not $pj) { return "end" }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc -and $db.newObjectPosition) {
|
||||
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
} catch { return "end" }
|
||||
}
|
||||
|
||||
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
# Явно названный вид сортируется в любом случае.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Test-OrderSensitiveType([string]$typeName) {
|
||||
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||
}
|
||||
|
||||
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||
$keys = @("", "")
|
||||
$names = @($a, $b)
|
||||
for ($i = 0; $i -lt 2; $i++) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||
else { [void]$sb.Append('0') }
|
||||
[void]$sb.Append($ch)
|
||||
}
|
||||
$keys[$i] = $sb.ToString()
|
||||
}
|
||||
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||
if ($r -lt 0) { return -1 }
|
||||
if ($r -gt 0) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
# Сортировка имён компаратором Compare-MetadataNames. В py-порту ту же роль играет
|
||||
# functools.cmp_to_key — штатный способ отсортировать компаратором; в PS 5.1 его нет,
|
||||
# поэтому слияние вручную. Порядок обоих портов задаёт один и тот же компаратор.
|
||||
function Sort-MetadataNames([string[]]$names) {
|
||||
# Возврат без запятой-обёртки: приёмная сторона всегда пишет @(...), и одноэлементный
|
||||
# результат остаётся массивом. С `return ,@(...)` @() собрал бы ОДИН объект-массив.
|
||||
if ($names.Count -le 1) { return $names }
|
||||
$mid = [int]($names.Count / 2)
|
||||
$left = @(Sort-MetadataNames $names[0..($mid - 1)])
|
||||
$right = @(Sort-MetadataNames $names[$mid..($names.Count - 1)])
|
||||
$out = New-Object System.Collections.ArrayList
|
||||
$i = 0; $j = 0
|
||||
while ($i -lt $left.Count -and $j -lt $right.Count) {
|
||||
if ((Compare-MetadataNames $left[$i] $right[$j]) -le 0) { [void]$out.Add($left[$i]); $i++ }
|
||||
else { [void]$out.Add($right[$j]); $j++ }
|
||||
}
|
||||
while ($i -lt $left.Count) { [void]$out.Add($left[$i]); $i++ }
|
||||
while ($j -lt $right.Count) { [void]$out.Add($right[$j]); $j++ }
|
||||
return $out.ToArray()
|
||||
}
|
||||
|
||||
# Упорядочить <ChildObjects>: имена внутри вида, а без аргумента — и группы видов.
|
||||
# Виды из Test-OrderSensitiveType по имени не сортируются, пока не названы явно.
|
||||
# Вызов без значения дополнительно ставит группы видов в канонический порядок: платформа
|
||||
# починила бы его только при загрузке-выгрузке, то есть неканоничный файл даёт диф на
|
||||
# ровном месте. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы — отступы и структура файла
|
||||
# остаются как были, в дифе только перестановка строк.
|
||||
function Do-SortChildObjects([string]$batchVal) {
|
||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||
|
||||
# Ввод прощающий: регистр не важен, принимается и имя каталога (Catalogs → Catalog) —
|
||||
# в дереве выгрузки виды видны именно во множественном числе.
|
||||
# Без @(...) на приёме: Parse-BatchValue возвращает ,$items — обёртка, которую @()
|
||||
# собрал бы как ОДИН объект-массив, и вид не нашёлся бы в $script:typeOrder.
|
||||
$tokens = @()
|
||||
if ("$batchVal".Trim()) { $tokens = Parse-BatchValue $batchVal }
|
||||
$requested = @()
|
||||
foreach ($token in $tokens) {
|
||||
$canon = Resolve-TypeName $token
|
||||
if (-not $canon) { Write-Error "Unknown type '$token'. Valid: $($script:typeOrder -join ', ')"; exit 1 }
|
||||
$requested += $canon
|
||||
}
|
||||
|
||||
$groups = New-Object System.Collections.Specialized.OrderedDictionary
|
||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
$ln = $child.get_LocalName()
|
||||
if (-not $groups.Contains($ln)) { $groups[$ln] = New-Object System.Collections.ArrayList }
|
||||
[void]$groups[$ln].Add($child)
|
||||
}
|
||||
|
||||
$targets = if ($requested.Count -gt 0) { $requested } else { @($groups.Keys | Where-Object { -not (Test-OrderSensitiveType $_) }) }
|
||||
foreach ($typeName in $targets) {
|
||||
if (-not $groups.Contains($typeName)) { continue }
|
||||
$els = $groups[$typeName]
|
||||
if ($els.Count -lt 2) { continue }
|
||||
$names = @(foreach ($e in $els) { $e.InnerText })
|
||||
$ordered = @(Sort-MetadataNames $names)
|
||||
$same = $true
|
||||
for ($i = 0; $i -lt $names.Count; $i++) { if ($names[$i] -cne $ordered[$i]) { $same = $false; break } }
|
||||
if ($same) { continue }
|
||||
for ($i = 0; $i -lt $els.Count; $i++) { $els[$i].InnerText = $ordered[$i] }
|
||||
$script:modifyCount++
|
||||
Info "Sorted: $typeName ($($els.Count))"
|
||||
}
|
||||
|
||||
if ($requested.Count -gt 0) { return }
|
||||
|
||||
# Без аргумента приводим в порядок и сами группы видов: собранная навыками конфигурация
|
||||
# может держать их не в каноне, и первая же выгрузка платформы даст диф. Переставляем
|
||||
# содержимое существующих узлов, а не узлы, поэтому отступы и структура файла не меняются —
|
||||
# в дифе только перестановка строк. Имя тега у XmlElement неизменяемо, поэтому там, где вид
|
||||
# меняется, узел заменяется через ReplaceChild: он сохраняет окружающие пробельные узлы.
|
||||
$elems = @()
|
||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element') { $elems += $child }
|
||||
}
|
||||
$tags = @(); $texts = @()
|
||||
foreach ($e in $elems) { $tags += $e.get_LocalName(); $texts += $e.InnerText }
|
||||
$rank = @()
|
||||
for ($i = 0; $i -lt $tags.Count; $i++) {
|
||||
$r = $script:typeOrder.IndexOf($tags[$i])
|
||||
if ($r -lt 0) { $r = $script:typeOrder.Count }
|
||||
$rank += $r
|
||||
}
|
||||
# Порядок стабильный: вторым ключом идёт исходная позиция
|
||||
$order = @(0..($tags.Count - 1) | Sort-Object @{e={$rank[$_]}}, @{e={$_}})
|
||||
$same = $true
|
||||
for ($i = 0; $i -lt $order.Count; $i++) { if ($order[$i] -ne $i) { $same = $false; break } }
|
||||
if ($same) { return }
|
||||
|
||||
for ($i = 0; $i -lt $elems.Count; $i++) {
|
||||
$srcIdx = $order[$i]
|
||||
if ($tags[$i] -ceq $tags[$srcIdx]) {
|
||||
$elems[$i].InnerText = $texts[$srcIdx]
|
||||
continue
|
||||
}
|
||||
$newEl = $script:xmlDoc.CreateElement($tags[$srcIdx], $script:mdNs)
|
||||
$newEl.InnerText = $texts[$srcIdx]
|
||||
[void]$script:childObjsEl.ReplaceChild($newEl, $elems[$i])
|
||||
}
|
||||
$script:modifyCount++
|
||||
Info "Reordered type groups: $($elems.Count) entries"
|
||||
}
|
||||
|
||||
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
# финальный перенос. $null → файл новый (сохранить текущее поведение).
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Detect-XmlStyle([string]$path) {
|
||||
if (-not (Test-Path -LiteralPath $path)) { return $null }
|
||||
$raw = [System.IO.File]::ReadAllBytes($path)
|
||||
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
|
||||
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
|
||||
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
|
||||
$m = [regex]::Match($head, 'encoding="([^"]+)"')
|
||||
return @{
|
||||
bom = $bom
|
||||
crlf = $body.Contains("`r`n")
|
||||
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
|
||||
finalNl = $body.EndsWith("`n")
|
||||
}
|
||||
}
|
||||
|
||||
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
|
||||
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Finalize-XmlText([string]$text, $style) {
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
|
||||
if ($style -and $style.finalNl) { $text += "`n" }
|
||||
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
|
||||
return $text
|
||||
}
|
||||
|
||||
function Do-AddChildObject([string]$batchVal) {
|
||||
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
|
||||
|
||||
@@ -460,6 +689,8 @@ function Do-AddChildObject([string]$batchVal) {
|
||||
exit 1
|
||||
}
|
||||
$typeName = $item.Substring(0, $dotIdx)
|
||||
$canonType = Resolve-TypeName $typeName
|
||||
if ($canonType) { $typeName = $canonType }
|
||||
$objNameVal = $item.Substring($dotIdx + 1)
|
||||
|
||||
# Check type is valid
|
||||
@@ -504,11 +735,11 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
|
||||
continue
|
||||
}
|
||||
|
||||
# Find insertion point: after last element of same type, or after last element of preceding type
|
||||
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition.
|
||||
$byName = (-not (Test-OrderSensitiveType $typeName) -and (Get-NewObjectPosition $script:configDir) -eq "byName")
|
||||
$insertBefore = $null
|
||||
$lastSameType = $null
|
||||
$lastPrecedingType = $null
|
||||
$currentTypeIdx = -1
|
||||
$firstLaterType = $null
|
||||
|
||||
foreach ($child in $script:childObjsEl.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
@@ -516,17 +747,29 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
|
||||
if ($childTypeIdx -lt 0) { continue }
|
||||
|
||||
if ($child.LocalName -eq $typeName) {
|
||||
# Same type — check alphabetical order
|
||||
if ($child.InnerText -gt $objNameVal -and -not $insertBefore) {
|
||||
# Insert before this element (alphabetical)
|
||||
# Внутри вида — по newObjectPosition: end (по умолчанию) кладёт после последнего
|
||||
# объекта того же вида, byName — по имени. Subsystem по имени не упорядочиваем
|
||||
# никогда: порядок подсистем в дереве задаёт порядок разделов в панели.
|
||||
$lastSameType = $child
|
||||
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objNameVal) -gt 0) {
|
||||
$insertBefore = $child
|
||||
}
|
||||
$lastSameType = $child
|
||||
} elseif ($childTypeIdx -lt $typeIdx) {
|
||||
$lastPrecedingType = $child
|
||||
} elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) {
|
||||
# First element of a later type — insert before it
|
||||
$insertBefore = $child
|
||||
} elseif ($childTypeIdx -gt $typeIdx -and -not $firstLaterType) {
|
||||
$firstLaterType = $child
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $insertBefore) {
|
||||
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
|
||||
# то есть перед его следующим соседом. Через $firstLaterType этого не сделать:
|
||||
# если видов старше в файле нет, запись уехала бы в самый конец блока,
|
||||
# за пределы своей группы.
|
||||
if ($lastSameType) {
|
||||
$next = $lastSameType.NextSibling
|
||||
while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling }
|
||||
$insertBefore = $next
|
||||
} else {
|
||||
$insertBefore = $firstLaterType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,6 +801,8 @@ function Do-RemoveChildObject([string]$batchVal) {
|
||||
exit 1
|
||||
}
|
||||
$typeName = $item.Substring(0, $dotIdx)
|
||||
$canonType = Resolve-TypeName $typeName
|
||||
if ($canonType) { $typeName = $canonType }
|
||||
$objNameVal = $item.Substring($dotIdx + 1)
|
||||
|
||||
$found = $false
|
||||
@@ -787,6 +1032,29 @@ $script:ruTypeMap = @{
|
||||
"бот" = "Bot"
|
||||
"планобмена" = "ExchangePlan"
|
||||
"хранилищенастроек" = "SettingsStorage"
|
||||
# Множественное число: в дереве конфигурации виды подписаны именно так.
|
||||
"справочники" = "Catalog"
|
||||
"документы" = "Document"
|
||||
"перечисления" = "Enum"
|
||||
"отчёты" = "Report"
|
||||
"отчеты" = "Report"
|
||||
"обработки" = "DataProcessor"
|
||||
"общиеформы" = "CommonForm"
|
||||
"журналыдокументов" = "DocumentJournal"
|
||||
"планывидовхарактеристик" = "ChartOfCharacteristicTypes"
|
||||
"планысчетов" = "ChartOfAccounts"
|
||||
"планывидоврасчета" = "ChartOfCalculationTypes"
|
||||
"планывидоврасчёта" = "ChartOfCalculationTypes"
|
||||
"регистрысведений" = "InformationRegister"
|
||||
"регистрынакопления" = "AccumulationRegister"
|
||||
"регистрыбухгалтерии" = "AccountingRegister"
|
||||
"регистрырасчета" = "CalculationRegister"
|
||||
"регистрырасчёта" = "CalculationRegister"
|
||||
"бизнеспроцессы" = "BusinessProcess"
|
||||
"задачи" = "Task"
|
||||
"боты" = "Bot"
|
||||
"планыобмена" = "ExchangePlan"
|
||||
"хранилищанастроек" = "SettingsStorage"
|
||||
}
|
||||
# plural folder → singular type
|
||||
$script:dirToType = @{}
|
||||
@@ -1028,11 +1296,16 @@ foreach ($op in $operations) {
|
||||
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
|
||||
"set-panels" { Do-SetPanels $opValue }
|
||||
"set-home-page" { Do-SetHomePage $opValue }
|
||||
"sort-childObjects" { Do-SortChildObjects $opValueStr }
|
||||
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Save ---
|
||||
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||
$xmlStyle = Detect-XmlStyle $resolvedPath
|
||||
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings.Indent = $false
|
||||
@@ -1043,22 +1316,12 @@ $writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$script:xmlDoc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$bytes = $memStream.ToArray()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
$text = Finalize-XmlText $text $xmlStyle
|
||||
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
|
||||
$writeBom = ($null -eq $xmlStyle) -or $xmlStyle.bom
|
||||
[System.IO.File]::WriteAllText($resolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
|
||||
Info "Saved: $resolvedPath"
|
||||
|
||||
# --- Auto-validate ---
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-edit v1.23 — Edit 1C configuration root (Configuration.xml)
|
||||
# cf-edit v1.29 — Edit 1C configuration root (Configuration.xml)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -316,27 +317,27 @@ XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
# Canonical type order for ChildObjects (44 types)
|
||||
# Canonical type order for ChildObjects (46 types)
|
||||
TYPE_ORDER = [
|
||||
"Language", "Subsystem", "StyleItem", "Style",
|
||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
|
||||
"Constant", "CommonForm", "Catalog", "Document",
|
||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
|
||||
"ChartOfCalculationTypes", "CalculationRegister",
|
||||
"BusinessProcess", "Task", "IntegrationService",
|
||||
"BusinessProcess", "Task", "ExternalDataSource", "IntegrationService",
|
||||
]
|
||||
|
||||
# Type → on-disk directory name (plural)
|
||||
TYPE_TO_DIR = {
|
||||
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
||||
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
||||
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
||||
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "PaletteColor": "PaletteColors", "PaletteColor": "PaletteColors", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
||||
@@ -345,7 +346,7 @@ TYPE_TO_DIR = {
|
||||
"Report": "Reports", "DataProcessor": "DataProcessors", "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
|
||||
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", "ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters",
|
||||
"ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters",
|
||||
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "IntegrationService": "IntegrationServices",
|
||||
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "ExternalDataSource": "ExternalDataSources", "IntegrationService": "IntegrationServices",
|
||||
}
|
||||
|
||||
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
|
||||
@@ -353,6 +354,137 @@ SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCat
|
||||
REF_PROPS = ["DefaultLanguage"]
|
||||
|
||||
|
||||
def get_new_object_position(cfg_dir):
|
||||
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
|
||||
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
остаётся рабочим каталогом проекта.
|
||||
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||
if not pj:
|
||||
return "end"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
proj_dir = os.path.dirname(pj)
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src and db.get("newObjectPosition"):
|
||||
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||
return "byName"
|
||||
return "end"
|
||||
except Exception:
|
||||
return "end"
|
||||
|
||||
|
||||
def is_order_sensitive_type(type_name):
|
||||
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
|
||||
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
Явно названный вид сортируется в любом случае.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||
|
||||
|
||||
def compare_metadata_names(a, b):
|
||||
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
|
||||
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
keys = []
|
||||
for name in (a, b):
|
||||
parts = []
|
||||
for ch in name.lower():
|
||||
if ch == "ё":
|
||||
ch = "е"
|
||||
if ch.isdigit():
|
||||
parts.append("1" + ch)
|
||||
elif ch.isalpha():
|
||||
parts.append("2" + ch)
|
||||
else:
|
||||
parts.append("0" + ch)
|
||||
keys.append("".join(parts))
|
||||
if keys[0] != keys[1]:
|
||||
return -1 if keys[0] < keys[1] else 1
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
return 0
|
||||
|
||||
|
||||
RU_TYPE_MAP = {
|
||||
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
|
||||
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
|
||||
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
|
||||
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||
"плансчетов": "ChartOfAccounts",
|
||||
"планвидоврасчета": "ChartOfCalculationTypes",
|
||||
"планвидоврасчёта": "ChartOfCalculationTypes",
|
||||
"регистрсведений": "InformationRegister",
|
||||
"регистрнакопления": "AccumulationRegister",
|
||||
"регистрбухгалтерии": "AccountingRegister",
|
||||
"регистррасчета": "CalculationRegister",
|
||||
"регистррасчёта": "CalculationRegister",
|
||||
"бизнеспроцесс": "BusinessProcess",
|
||||
"бот": "Bot",
|
||||
"задача": "Task", "планобмена": "ExchangePlan",
|
||||
"хранилищенастроек": "SettingsStorage",
|
||||
# Множественное число: в дереве конфигурации виды подписаны именно так.
|
||||
"справочники": "Catalog", "документы": "Document", "перечисления": "Enum",
|
||||
"отчёты": "Report", "отчеты": "Report", "обработки": "DataProcessor",
|
||||
"общиеформы": "CommonForm", "журналыдокументов": "DocumentJournal",
|
||||
"планывидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||
"планысчетов": "ChartOfAccounts",
|
||||
"планывидоврасчета": "ChartOfCalculationTypes",
|
||||
"планывидоврасчёта": "ChartOfCalculationTypes",
|
||||
"регистрысведений": "InformationRegister",
|
||||
"регистрынакопления": "AccumulationRegister",
|
||||
"регистрыбухгалтерии": "AccountingRegister",
|
||||
"регистррасчета": "CalculationRegister", "регистрырасчета": "CalculationRegister",
|
||||
"регистрырасчёта": "CalculationRegister",
|
||||
"бизнеспроцессы": "BusinessProcess",
|
||||
"боты": "Bot",
|
||||
"задачи": "Task", "планыобмена": "ExchangePlan",
|
||||
"хранилищанастроек": "SettingsStorage",
|
||||
}
|
||||
|
||||
|
||||
def resolve_type_name(token):
|
||||
"""Имя вида из пользовательского ввода → каноническое имя или None.
|
||||
|
||||
Ввод прощающий: регистр не важен, принимается имя каталога выгрузки
|
||||
(Catalogs → Catalog) и русское имя вида в единственном и множественном числе.
|
||||
"""
|
||||
key = (token or "").strip().lower()
|
||||
if not key:
|
||||
return None
|
||||
for canon in TYPE_ORDER:
|
||||
if canon.lower() == key:
|
||||
return canon
|
||||
for canon, dir_name in TYPE_TO_DIR.items():
|
||||
if dir_name.lower() == key:
|
||||
return canon
|
||||
return RU_TYPE_MAP.get(key)
|
||||
|
||||
|
||||
def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
|
||||
@@ -498,7 +630,7 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
||||
parser.add_argument("-ConfigPath", "-Path", required=True)
|
||||
parser.add_argument("-DefinitionFile", default=None)
|
||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page", "sort-childObjects"])
|
||||
parser.add_argument("-Value", default=None)
|
||||
parser.add_argument("-NoValidate", action="store_true")
|
||||
args = ci_parse_args(parser)
|
||||
@@ -637,7 +769,7 @@ def main():
|
||||
if dot_idx < 1:
|
||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
type_name = item[:dot_idx]
|
||||
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
|
||||
obj_name_val = item[dot_idx + 1:]
|
||||
|
||||
if type_name not in TYPE_ORDER:
|
||||
@@ -674,8 +806,15 @@ def main():
|
||||
warn(f"Already exists: {type_name}.{obj_name_val}")
|
||||
continue
|
||||
|
||||
# Find insertion point
|
||||
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
|
||||
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
|
||||
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт
|
||||
# порядок разделов в панели, пока их не перечислили в <SubsystemsOrder>.
|
||||
by_name = (not is_order_sensitive_type(type_name)
|
||||
and get_new_object_position(config_dir) == "byName")
|
||||
insert_before = None
|
||||
last_same = None
|
||||
first_later = None
|
||||
for child in child_objs_el:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
@@ -685,10 +824,24 @@ def main():
|
||||
child_type_idx = TYPE_ORDER.index(child_type_name)
|
||||
|
||||
if child_type_name == type_name:
|
||||
if (child.text or "") > obj_name_val and insert_before is None:
|
||||
last_same = child
|
||||
if (by_name and insert_before is None
|
||||
and compare_metadata_names(child.text or "", obj_name_val) > 0):
|
||||
insert_before = child
|
||||
elif child_type_idx > type_idx and insert_before is None:
|
||||
insert_before = child
|
||||
elif child_type_idx > type_idx and first_later is None:
|
||||
first_later = child
|
||||
|
||||
if insert_before is None:
|
||||
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
|
||||
# то есть перед его следующим соседом. Через first_later этого не сделать:
|
||||
# если видов старше в файле нет, запись уехала бы в самый конец блока,
|
||||
# за пределы своей группы.
|
||||
if last_same is not None:
|
||||
siblings = [c for c in child_objs_el if isinstance(c.tag, str)]
|
||||
pos = siblings.index(last_same)
|
||||
insert_before = siblings[pos + 1] if pos + 1 < len(siblings) else None
|
||||
else:
|
||||
insert_before = first_later
|
||||
|
||||
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
|
||||
new_el.text = obj_name_val
|
||||
@@ -701,6 +854,69 @@ def main():
|
||||
add_count += 1
|
||||
info(f"Added: {type_name}.{obj_name_val}")
|
||||
|
||||
def do_sort_child_objects(batch_val):
|
||||
"""Упорядочить <ChildObjects>: имена внутри вида, а без аргумента — и группы видов.
|
||||
|
||||
Виды из is_order_sensitive_type по имени не сортируются, пока не названы явно.
|
||||
Вызов без значения дополнительно ставит группы видов в канонический порядок: платформа
|
||||
починила бы его только при загрузке-выгрузке, то есть неканоничный файл даёт диф на
|
||||
ровном месте. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы — отступы и структура файла
|
||||
остаются как были, в дифе только перестановка строк.
|
||||
"""
|
||||
nonlocal modify_count
|
||||
if child_objs_el is None:
|
||||
print("No <ChildObjects> element found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
requested = []
|
||||
for token in (parse_batch_value(batch_val) if str(batch_val or "").strip() else []):
|
||||
canon = resolve_type_name(token)
|
||||
if canon is None:
|
||||
print(f"Unknown type '{token}'. Valid: {', '.join(TYPE_ORDER)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
requested.append(canon)
|
||||
|
||||
groups = {}
|
||||
for child in child_objs_el:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
groups.setdefault(localname(child), []).append(child)
|
||||
|
||||
targets = requested or [t for t in groups if not is_order_sensitive_type(t)]
|
||||
for type_name in targets:
|
||||
els = groups.get(type_name, [])
|
||||
if len(els) < 2:
|
||||
continue
|
||||
names = [e.text or "" for e in els]
|
||||
ordered = sorted(names, key=functools.cmp_to_key(compare_metadata_names))
|
||||
if names == ordered:
|
||||
continue
|
||||
for el, name in zip(els, ordered):
|
||||
el.text = name
|
||||
modify_count += 1
|
||||
info(f"Sorted: {type_name} ({len(els)})")
|
||||
|
||||
if requested:
|
||||
# Вид назван явно — точечная операция: взаимный порядок групп не трогаем.
|
||||
return
|
||||
|
||||
# Без аргумента приводим в порядок и сами группы видов: собранная навыками
|
||||
# конфигурация может держать их не в каноне, и первая же выгрузка платформы даст
|
||||
# диф. Переставляем содержимое существующих узлов, а не узлы, поэтому отступы и
|
||||
# структура файла не меняются — в дифе только перестановка строк.
|
||||
elems = [c for c in child_objs_el if isinstance(c.tag, str)]
|
||||
pairs = [(localname(c), c.text or "") for c in elems]
|
||||
ranked = sorted(range(len(pairs)),
|
||||
key=lambda i: (TYPE_ORDER.index(pairs[i][0]) if pairs[i][0] in TYPE_ORDER else len(TYPE_ORDER), i))
|
||||
wanted = [pairs[i] for i in ranked]
|
||||
if wanted == pairs:
|
||||
return
|
||||
for el, (tag, text) in zip(elems, wanted):
|
||||
el.tag = f'{{{MD_NS}}}{tag}'
|
||||
el.text = text
|
||||
modify_count += 1
|
||||
info(f"Reordered type groups: {len(elems)} entries")
|
||||
|
||||
def do_remove_child_object(batch_val):
|
||||
nonlocal remove_count
|
||||
if child_objs_el is None:
|
||||
@@ -713,7 +929,7 @@ def main():
|
||||
if dot_idx < 1:
|
||||
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
type_name = item[:dot_idx]
|
||||
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
|
||||
obj_name_val = item[dot_idx + 1:]
|
||||
|
||||
found = False
|
||||
@@ -933,24 +1149,6 @@ def main():
|
||||
info(f"Wrote panel layout: {cai_path}")
|
||||
|
||||
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
|
||||
RU_TYPE_MAP = {
|
||||
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
|
||||
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
|
||||
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
|
||||
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
|
||||
"плансчетов": "ChartOfAccounts",
|
||||
"планвидоврасчета": "ChartOfCalculationTypes",
|
||||
"планвидоврасчёта": "ChartOfCalculationTypes",
|
||||
"регистрсведений": "InformationRegister",
|
||||
"регистрнакопления": "AccumulationRegister",
|
||||
"регистрбухгалтерии": "AccountingRegister",
|
||||
"регистррасчета": "CalculationRegister",
|
||||
"регистррасчёта": "CalculationRegister",
|
||||
"бизнеспроцесс": "BusinessProcess",
|
||||
"бот": "Bot",
|
||||
"задача": "Task", "планобмена": "ExchangePlan",
|
||||
"хранилищенастроек": "SettingsStorage",
|
||||
}
|
||||
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
|
||||
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||
|
||||
@@ -1130,6 +1328,8 @@ def main():
|
||||
do_set_panels(op_value)
|
||||
elif op_key == "set-home-page":
|
||||
do_set_home_page(op_value)
|
||||
elif op_key == "sort-childobjects":
|
||||
do_sort_child_objects(op_value if isinstance(op_value, str) else str(op_value))
|
||||
else:
|
||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# cf-info v1.7 — Compact summary of 1C configuration root
|
||||
# cf-info v1.9 — Compact summary of 1C configuration root
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -86,20 +86,20 @@ function Get-PropML([string]$propName) {
|
||||
return (Get-MLText $n)
|
||||
}
|
||||
|
||||
# --- Type name maps (canonical order, 44 types) ---
|
||||
# --- Type name maps (canonical order, 46 types) ---
|
||||
$typeOrder = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
$typeRuNames = @{
|
||||
@@ -120,7 +120,7 @@ $typeRuNames = @{
|
||||
"ChartOfCharacteristicTypes"="ПВХ"; "ChartOfAccounts"="Планы счетов"
|
||||
"AccountingRegister"="Регистры бухгалтерии"; "ChartOfCalculationTypes"="ПВР"
|
||||
"CalculationRegister"="Регистры расчёта"; "BusinessProcess"="Бизнес-процессы"
|
||||
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
|
||||
"Task"="Задачи"; "ExternalDataSource"="Внешние источники данных"; "IntegrationService"="Сервисы интеграции"
|
||||
}
|
||||
|
||||
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-info v1.7 — Compact summary of 1C configuration root
|
||||
# cf-info v1.9 — Compact summary of 1C configuration root
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -113,20 +113,20 @@ def get_prop_ml(prop_name):
|
||||
n = props_node.find(f"md:{prop_name}", NS)
|
||||
return get_ml_text(n)
|
||||
|
||||
# --- Type name maps (canonical order, 44 types) ---
|
||||
# --- Type name maps (canonical order, 46 types) ---
|
||||
type_order = [
|
||||
"Language", "Subsystem", "StyleItem", "Style",
|
||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
|
||||
"Constant", "CommonForm", "Catalog", "Document",
|
||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
|
||||
"ChartOfCalculationTypes", "CalculationRegister",
|
||||
"BusinessProcess", "Task", "IntegrationService",
|
||||
"BusinessProcess", "Task", "ExternalDataSource", "IntegrationService",
|
||||
]
|
||||
|
||||
type_ru_names = {
|
||||
@@ -147,7 +147,7 @@ type_ru_names = {
|
||||
"ChartOfCharacteristicTypes": "ПВХ", "ChartOfAccounts": "Планы счетов",
|
||||
"AccountingRegister": "Регистры бухгалтерии", "ChartOfCalculationTypes": "ПВР",
|
||||
"CalculationRegister": "Регистры расчёта", "BusinessProcess": "Бизнес-процессы",
|
||||
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
||||
"Task": "Задачи", "ExternalDataSource": "Внешние источники данных", "IntegrationService": "Сервисы интеграции",
|
||||
}
|
||||
|
||||
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# cf-validate v1.8 — Validate 1C configuration root structure
|
||||
# cf-validate v1.10 — Validate 1C configuration root structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -122,16 +122,16 @@ $validClassIds = @(
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
# Type -> directory mapping
|
||||
@@ -140,6 +140,7 @@ $childTypeDirMap = @{
|
||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||
"Bot"="Bots"
|
||||
"PaletteColor"="PaletteColors"
|
||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
@@ -156,7 +157,7 @@ $childTypeDirMap = @{
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||
"CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"IntegrationService"="IntegrationServices"
|
||||
"ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
}
|
||||
|
||||
# Valid enum values for Configuration properties
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cf-validate v1.8 — Validate 1C configuration XML structure
|
||||
# cf-validate v1.10 — Validate 1C configuration XML structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||
import sys, os, argparse, re
|
||||
@@ -59,16 +59,16 @@ VALID_CLASS_IDS = [
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'IntegrationService',
|
||||
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
|
||||
]
|
||||
|
||||
# Type -> directory mapping
|
||||
@@ -76,7 +76,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||
'Bot': 'Bots',
|
||||
'Bot': 'Bots', 'PaletteColor': 'PaletteColors',
|
||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||
@@ -93,6 +93,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
|
||||
'CalculationRegister': 'CalculationRegisters',
|
||||
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
|
||||
'ExternalDataSource': 'ExternalDataSources',
|
||||
'IntegrationService': 'IntegrationServices',
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# cfe-borrow v1.33 — Borrow objects from configuration into extension (CFE)
|
||||
# cfe-borrow v1.37 — Borrow objects from configuration into extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -257,10 +257,10 @@ $childTypeDirMap = @{
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||
"Sequence"="Sequences"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
|
||||
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
|
||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "PaletteColor"="PaletteColors"; "Language"="Languages"
|
||||
}
|
||||
|
||||
# --- 4a. Модули заимствованных объектов ---
|
||||
@@ -310,20 +310,20 @@ $synonymMap = @{
|
||||
"HTTPСервис"="HTTPService"; "СервисИнтеграции"="IntegrationService"
|
||||
}
|
||||
|
||||
# --- 5. Canonical type order (44 types) ---
|
||||
# --- 5. Canonical type order (46 types) ---
|
||||
$script:typeOrder = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
# --- 6. GeneratedType patterns per type ---
|
||||
@@ -455,6 +455,11 @@ $script:generatedTypes = @{
|
||||
"DefinedType" = @(
|
||||
@{ prefix = "DefinedType"; category = "DefinedType" }
|
||||
)
|
||||
"ExternalDataSource" = @(
|
||||
@{ prefix = "ExternalDataSourceManager"; category = "Manager" }
|
||||
@{ prefix = "ExternalDataSourceTablesManager"; category = "TablesManager" }
|
||||
@{ prefix = "ExternalDataSourceCubesManager"; category = "CubesManager" }
|
||||
)
|
||||
"Sequence" = @(
|
||||
@{ prefix = "SequenceRecord"; category = "Record" }
|
||||
@{ prefix = "SequenceManager"; category = "Manager" }
|
||||
@@ -1347,32 +1352,22 @@ function Register-FormInObject {
|
||||
}
|
||||
|
||||
# Save object XML
|
||||
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||
$style2 = Detect-XmlStyle $objFile
|
||||
$settings2 = New-Object System.Xml.XmlWriterSettings
|
||||
$settings2.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings2.Indent = $false
|
||||
$settings2.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
$memStream2 = New-Object System.IO.MemoryStream
|
||||
$writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2)
|
||||
$objDoc.Save($writer2)
|
||||
$writer2.Flush(); $writer2.Close()
|
||||
|
||||
$bytes2 = $memStream2.ToArray()
|
||||
$text2 = [System.Text.Encoding]::UTF8.GetString($memStream2.ToArray())
|
||||
$memStream2.Close()
|
||||
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
|
||||
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
|
||||
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text2 = [regex]::Replace($text2, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
|
||||
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$text2 = ($text2 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
|
||||
$text2 = Finalize-XmlText $text2 $style2
|
||||
$writeBom2 = ($null -eq $style2) -or $style2.bom
|
||||
[System.IO.File]::WriteAllText($objFile, $text2, (New-Object System.Text.UTF8Encoding($writeBom2)))
|
||||
Info " Registered form in: $objFile"
|
||||
}
|
||||
|
||||
@@ -1885,6 +1880,9 @@ function Merge-AttributesIntoObject {
|
||||
}
|
||||
|
||||
# Save via text manipulation to avoid namespace issues with InnerXml
|
||||
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||
$style3 = Detect-XmlStyle $objFile
|
||||
$settings3 = New-Object System.Xml.XmlWriterSettings
|
||||
$settings3.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings3.Indent = $false
|
||||
@@ -1893,28 +1891,15 @@ function Merge-AttributesIntoObject {
|
||||
$writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3)
|
||||
$objDoc.Save($writer3)
|
||||
$writer3.Flush(); $writer3.Close()
|
||||
$bytes3 = $memStream3.ToArray()
|
||||
$text3 = [System.Text.Encoding]::UTF8.GetString($memStream3.ToArray())
|
||||
$memStream3.Close()
|
||||
$text3 = [System.Text.Encoding]::UTF8.GetString($bytes3)
|
||||
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
|
||||
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
|
||||
# Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
|
||||
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
|
||||
# Стоит ДО Finalize-XmlText, чтобы схлопывание пустых тегов накрыло и вставленные реквизиты.
|
||||
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
|
||||
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
# Стоит ПОСЛЕ вставки реквизитов, чтобы накрыть и их.
|
||||
$text3 = [regex]::Replace($text3, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
|
||||
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$text3 = ($text3 -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
|
||||
$text3 = Finalize-XmlText $text3 $style3
|
||||
$writeBom3 = ($null -eq $style3) -or $style3.bom
|
||||
[System.IO.File]::WriteAllText($objFile, $text3, (New-Object System.Text.UTF8Encoding($writeBom3)))
|
||||
Info " Merged $added attribute(s) into: $objFile"
|
||||
}
|
||||
}
|
||||
@@ -2230,6 +2215,127 @@ function Build-BorrowedObjectXml {
|
||||
}
|
||||
|
||||
# --- 13. Helper: add object to extension ChildObjects ---
|
||||
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
|
||||
# финальный перенос. $null → файл новый (сохранить текущее поведение).
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Detect-XmlStyle([string]$path) {
|
||||
if (-not (Test-Path -LiteralPath $path)) { return $null }
|
||||
$raw = [System.IO.File]::ReadAllBytes($path)
|
||||
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
|
||||
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
|
||||
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
|
||||
$m = [regex]::Match($head, 'encoding="([^"]+)"')
|
||||
return @{
|
||||
bom = $bom
|
||||
crlf = $body.Contains("`r`n")
|
||||
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
|
||||
finalNl = $body.EndsWith("`n")
|
||||
}
|
||||
}
|
||||
|
||||
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
|
||||
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Finalize-XmlText([string]$text, $style) {
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
|
||||
if ($style -and $style.finalNl) { $text += "`n" }
|
||||
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
|
||||
return $text
|
||||
}
|
||||
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
# остаётся рабочим каталогом проекта.
|
||||
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Get-NewObjectPosition([string]$cfgDir) {
|
||||
try {
|
||||
if (-not $cfgDir) { $cfgDir = "." }
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||
if (-not $pj) { return "end" }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc -and $db.newObjectPosition) {
|
||||
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
} catch { return "end" }
|
||||
}
|
||||
|
||||
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
# Явно названный вид сортируется в любом случае.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Test-OrderSensitiveType([string]$typeName) {
|
||||
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||
}
|
||||
|
||||
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||
$keys = @("", "")
|
||||
$names = @($a, $b)
|
||||
for ($i = 0; $i -lt 2; $i++) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||
else { [void]$sb.Append('0') }
|
||||
[void]$sb.Append($ch)
|
||||
}
|
||||
$keys[$i] = $sb.ToString()
|
||||
}
|
||||
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||
if ($r -lt 0) { return -1 }
|
||||
if ($r -gt 0) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
function Add-ToChildObjects {
|
||||
param([string]$typeName, [string]$objName)
|
||||
|
||||
@@ -2255,7 +2361,12 @@ function Add-ToChildObjects {
|
||||
}
|
||||
}
|
||||
|
||||
# Find insertion point: after last element of same type, or before first element of later type
|
||||
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition: end
|
||||
# (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени. Так же
|
||||
# заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects не отсортирован.
|
||||
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт порядок
|
||||
# разделов в панели.
|
||||
$byName = (-not (Test-OrderSensitiveType $typeName) -and (Get-NewObjectPosition $extDir) -eq "byName")
|
||||
$insertBefore = $null
|
||||
$lastSameType = $null
|
||||
|
||||
@@ -2265,8 +2376,7 @@ function Add-ToChildObjects {
|
||||
if ($childTypeIdx -lt 0) { continue }
|
||||
|
||||
if ($child.LocalName -eq $typeName) {
|
||||
# Same type -- check alphabetical order
|
||||
if ($child.InnerText -gt $objName -and -not $insertBefore) {
|
||||
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objName) -gt 0) {
|
||||
$insertBefore = $child
|
||||
}
|
||||
$lastSameType = $child
|
||||
@@ -2439,32 +2549,22 @@ while ($true) {
|
||||
}
|
||||
|
||||
# --- 15. Save modified Configuration.xml ---
|
||||
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
|
||||
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
|
||||
$style = Detect-XmlStyle $extResolvedPath
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings.Indent = $false
|
||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$script:xmlDoc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$bytes = $memStream.ToArray()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $extResolvedPath) -and ([System.IO.File]::ReadAllText($extResolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
|
||||
$text = Finalize-XmlText $text $style
|
||||
$writeBom = ($null -eq $style) -or $style.bom
|
||||
[System.IO.File]::WriteAllText($extResolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
|
||||
Info "Saved: $extResolvedPath"
|
||||
|
||||
# --- 16. Summary ---
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-borrow v1.33 — Borrow objects from configuration into extension (CFE)
|
||||
# cfe-borrow v1.37 — Borrow objects from configuration into extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -202,6 +203,97 @@ def decode_numeric_entities(s):
|
||||
return s
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def get_new_object_position(cfg_dir):
|
||||
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
|
||||
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
остаётся рабочим каталогом проекта.
|
||||
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||
if not pj:
|
||||
return "end"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
proj_dir = os.path.dirname(pj)
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src and db.get("newObjectPosition"):
|
||||
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||
return "byName"
|
||||
return "end"
|
||||
except Exception:
|
||||
return "end"
|
||||
|
||||
|
||||
def is_order_sensitive_type(type_name):
|
||||
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
|
||||
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
Явно названный вид сортируется в любом случае.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||
|
||||
|
||||
def compare_metadata_names(a, b):
|
||||
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
|
||||
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
keys = []
|
||||
for name in (a, b):
|
||||
parts = []
|
||||
for ch in name.lower():
|
||||
if ch == "ё":
|
||||
ch = "е"
|
||||
if ch.isdigit():
|
||||
parts.append("1" + ch)
|
||||
elif ch.isalpha():
|
||||
parts.append("2" + ch)
|
||||
else:
|
||||
parts.append("0" + ch)
|
||||
keys.append("".join(parts))
|
||||
if keys[0] != keys[1]:
|
||||
return -1 if keys[0] < keys[1] else 1
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
return 0
|
||||
|
||||
|
||||
def localname(el):
|
||||
return etree.QName(el.tag).localname
|
||||
|
||||
@@ -233,11 +325,11 @@ CHILD_TYPE_DIR_MAP = {
|
||||
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs",
|
||||
"SettingsStorage": "SettingsStorages", "FilterCriterion": "FilterCriteria",
|
||||
"CommandGroup": "CommandGroups", "DocumentNumerator": "DocumentNumerators",
|
||||
"Sequence": "Sequences", "IntegrationService": "IntegrationServices",
|
||||
"Sequence": "Sequences", "ExternalDataSource": "ExternalDataSources", "IntegrationService": "IntegrationServices",
|
||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices",
|
||||
"HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||
"CommonAttribute": "CommonAttributes", "Style": "Styles",
|
||||
"Bot": "Bots", "Language": "Languages",
|
||||
"Bot": "Bots", "PaletteColor": "PaletteColors", "PaletteColor": "PaletteColors", "Language": "Languages",
|
||||
}
|
||||
|
||||
# --- Модули заимствованных объектов ---
|
||||
@@ -307,16 +399,16 @@ SYNONYM_MAP = {
|
||||
TYPE_ORDER = [
|
||||
"Language", "Subsystem", "StyleItem", "Style",
|
||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
||||
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
|
||||
"Constant", "CommonForm", "Catalog", "Document",
|
||||
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
|
||||
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
|
||||
"ChartOfCalculationTypes", "CalculationRegister",
|
||||
"BusinessProcess", "Task", "IntegrationService",
|
||||
"BusinessProcess", "Task", "ExternalDataSource", "IntegrationService",
|
||||
]
|
||||
|
||||
GENERATED_TYPES = {
|
||||
@@ -447,6 +539,11 @@ GENERATED_TYPES = {
|
||||
"DefinedType": [
|
||||
{"prefix": "DefinedType", "category": "DefinedType"},
|
||||
],
|
||||
"ExternalDataSource": [
|
||||
{"prefix": "ExternalDataSourceManager", "category": "Manager"},
|
||||
{"prefix": "ExternalDataSourceTablesManager", "category": "TablesManager"},
|
||||
{"prefix": "ExternalDataSourceCubesManager", "category": "CubesManager"},
|
||||
],
|
||||
"Sequence": [
|
||||
{"prefix": "SequenceRecord", "category": "Record"},
|
||||
{"prefix": "SequenceManager", "category": "Manager"},
|
||||
@@ -1035,6 +1132,13 @@ def main():
|
||||
warn(f"Already in ChildObjects: {type_name}.{obj_name}")
|
||||
return
|
||||
|
||||
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
|
||||
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
|
||||
# Так же, как заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects
|
||||
# не отсортирован. Subsystem по имени не упорядочиваем никогда: порядок подсистем
|
||||
# в дереве задаёт порядок разделов в панели.
|
||||
by_name = (not is_order_sensitive_type(type_name)
|
||||
and get_new_object_position(ext_dir) == "byName")
|
||||
insert_before = None
|
||||
for child in child_objs_el:
|
||||
if not isinstance(child.tag, str):
|
||||
@@ -1045,7 +1149,8 @@ def main():
|
||||
child_type_idx = TYPE_ORDER.index(child_type_name)
|
||||
|
||||
if child_type_name == type_name:
|
||||
if (child.text or "") > obj_name and insert_before is None:
|
||||
if (by_name and insert_before is None
|
||||
and compare_metadata_names(child.text or "", obj_name) > 0):
|
||||
insert_before = child
|
||||
elif child_type_idx > type_idx and insert_before is None:
|
||||
insert_before = child
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# cfe-diff v1.3 — Analyze and compare 1C configuration extension (CFE)
|
||||
# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -49,10 +49,11 @@ $childTypeDirMap = @{
|
||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
|
||||
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
|
||||
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
|
||||
"Sequence"="Sequences"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
|
||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||
"Bot"="Bots"
|
||||
"PaletteColor"="PaletteColors"
|
||||
}
|
||||
|
||||
# --- Parse extension Configuration.xml ---
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-diff v1.3 — Analyze and compare 1C configuration extension (CFE)
|
||||
# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -80,6 +80,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
"CommandGroup": "CommandGroups",
|
||||
"DocumentNumerator": "DocumentNumerators",
|
||||
"Sequence": "Sequences",
|
||||
"ExternalDataSource": "ExternalDataSources",
|
||||
"IntegrationService": "IntegrationServices",
|
||||
"CommonAttribute": "CommonAttributes",
|
||||
"Style": "Styles",
|
||||
@@ -88,6 +89,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
"HTTPService": "HTTPServices",
|
||||
"WSReference": "WSReferences",
|
||||
"Bot": "Bots",
|
||||
"PaletteColor": "PaletteColors",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ allowed-tools:
|
||||
|
||||
Правила:
|
||||
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
|
||||
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
||||
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Дословно — включая комментарии, регистр и пробелы внутри строки (`Х = Х + 1` ≠ `Х=Х+1`); свободны только отступ и пустые строки. Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
|
||||
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
|
||||
|
||||
## Актуализация
|
||||
@@ -107,6 +107,8 @@ allowed-tools:
|
||||
|
||||
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
|
||||
|
||||
`-Check` смотрит исходники. Вердикт платформы — уже после загрузки в базу: `/db-cfe-admin check`.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# cfe-patch-method v2.9 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -362,6 +362,22 @@ function Get-Normalized {
|
||||
return (($line -replace '\s+', ' ').Trim())
|
||||
}
|
||||
|
||||
# Control comparison key, as the platform compares a &ИзменениеИКонтроль copy with the original:
|
||||
# each line trimmed, blank lines dropped, everything else byte-for-byte and case-sensitive
|
||||
# (inner spaces, comments and letter case are significant). Measured on 8.3.24 and 8.3.27.
|
||||
function Get-ControlKey {
|
||||
param($lines)
|
||||
return (@($lines | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) -join "`n")
|
||||
}
|
||||
|
||||
# Parameter count of a signature params text. The platform compares only the number of
|
||||
# parameters of a &ИзменениеИКонтроль copy (names and default values it ignores).
|
||||
function Get-ParamCount {
|
||||
param([string]$paramsText)
|
||||
if ([string]::IsNullOrWhiteSpace($paramsText)) { return 0 }
|
||||
return @(Split-TopLevel $paramsText | Where-Object { $_.Trim() -ne '' }).Count
|
||||
}
|
||||
|
||||
# Reconstruct v1 body and edit ops from a marked body
|
||||
function Parse-MarkedBody {
|
||||
param($bodyLines)
|
||||
@@ -651,7 +667,14 @@ function Invoke-Resync {
|
||||
$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
|
||||
$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })
|
||||
|
||||
if (($v1norm -join "`n") -eq ($v2norm -join "`n")) {
|
||||
# Signature: the platform rejects the interceptor when the parameter COUNT differs, so a
|
||||
# body-only comparison would miss a vendor-added parameter. Names/defaults it ignores.
|
||||
$extParamCount = Get-ParamCount $sig.ParamsText
|
||||
$srcParamCount = Get-ParamCount $method.ParamsText
|
||||
$paramsDrift = ($extParamCount -ne $srcParamCount)
|
||||
$paramsReason = if ($paramsDrift) { "список параметров: в оригинале $srcParamCount, в перехватчике $extParamCount" } else { '' }
|
||||
|
||||
if (-not $paramsDrift -and [string]::Equals((Get-ControlKey $v1), (Get-ControlKey $v2), 'Ordinal')) {
|
||||
return @{ Id = $methodId; Status = 'АКТУАЛЕН'; ExtBsl = $extBsl }
|
||||
}
|
||||
|
||||
@@ -701,7 +724,9 @@ function Invoke-Resync {
|
||||
|
||||
if ($ReportOnly) {
|
||||
$st = if ($disputed.Count -gt 0) { 'КОНФЛИКТ' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'ДРЕЙФ' }
|
||||
if ($paramsDrift -and $st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { $st = 'ДРЕЙФ' }
|
||||
$rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации' } else { '' }
|
||||
if ($paramsDrift) { $rsn = if ($rsn) { "$paramsReason; $rsn" } else { $paramsReason } }
|
||||
return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Absorbed = $absorbed.Count; Disputed = $disputed.Count; Reason = $rsn; AbsorbedNotes = $absorbedNotes }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-patch-method v2.9 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -491,6 +491,21 @@ def normalize(line):
|
||||
return re.sub(r'\s+', ' ', line).strip()
|
||||
|
||||
|
||||
# Control comparison key, as the platform compares a &ИзменениеИКонтроль copy with the original:
|
||||
# each line trimmed, blank lines dropped, everything else byte-for-byte and case-sensitive
|
||||
# (inner spaces, comments and letter case are significant). Measured on 8.3.24 and 8.3.27.
|
||||
def control_key(lines):
|
||||
return "\n".join([k for k in (x.strip() for x in lines) if k != ""])
|
||||
|
||||
|
||||
# Parameter count of a signature params text. The platform compares only the number of
|
||||
# parameters of a &ИзменениеИКонтроль copy (names and default values it ignores).
|
||||
def param_count(params_text):
|
||||
if not params_text or not params_text.strip():
|
||||
return 0
|
||||
return len([p for p in split_top_level(params_text) if p.strip()])
|
||||
|
||||
|
||||
def parse_marked_body(body_lines):
|
||||
v1 = []
|
||||
ops = []
|
||||
@@ -1129,7 +1144,7 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
|
||||
sig = read_signature(ext_lines, sig_line_idx)
|
||||
if not sig:
|
||||
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"}
|
||||
_params, sig_end = sig
|
||||
ext_params_text, sig_end = sig
|
||||
is_func = bool(re.match(r'^\s*(?:Асинх\s+)?Функция\b', ext_lines[sig_line_idx], re.IGNORECASE))
|
||||
end_re = re.compile(r'^\s*КонецФункции\b' if is_func else r'^\s*КонецПроцедуры\b', re.IGNORECASE)
|
||||
block_end = -1
|
||||
@@ -1146,7 +1161,15 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
|
||||
v1norm = [normalize(x) for x in v1]
|
||||
v2norm = [normalize(x) for x in v2]
|
||||
|
||||
if "\n".join(v1norm) == "\n".join(v2norm):
|
||||
# Signature: the platform rejects the interceptor when the parameter COUNT differs, so a
|
||||
# body-only comparison would miss a vendor-added parameter. Names/defaults it ignores.
|
||||
ext_param_count = param_count(ext_params_text)
|
||||
src_param_count = param_count(method["params_text"])
|
||||
params_drift = ext_param_count != src_param_count
|
||||
params_reason = ("список параметров: в оригинале %d, в перехватчике %d"
|
||||
% (src_param_count, ext_param_count)) if params_drift else ""
|
||||
|
||||
if not params_drift and control_key(v1) == control_key(v2):
|
||||
return {"id": method_id, "status": "АКТУАЛЕН", "ext_bsl": ext_bsl}
|
||||
|
||||
insert_top = []; insert_after = {}; del_start = set(); del_end = set(); disputed = []; transferred = 0; absorbed = 0; absorbed_notes = []
|
||||
@@ -1208,7 +1231,11 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
|
||||
st = "ПЕРЕНЕСЕНО В ОСНОВНУЮ"
|
||||
else:
|
||||
st = "ДРЕЙФ"
|
||||
if params_drift and st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ":
|
||||
st = "ДРЕЙФ"
|
||||
rsn = conflict_reason(disputed) if disputed else ("все правки уже в основной конфигурации" if st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ" else "")
|
||||
if params_drift:
|
||||
rsn = ("%s; %s" % (params_reason, rsn)) if rsn else params_reason
|
||||
return {"id": method_id, "status": st, "ext_bsl": ext_bsl, "transferred": transferred,
|
||||
"absorbed": absorbed, "disputed": len(disputed), "reason": rsn, "absorbed_notes": absorbed_notes}
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ allowed-tools:
|
||||
|
||||
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты, права ролей. Аналог `/cf-validate`, но для расширений.
|
||||
|
||||
Проверяются исходники. Применимость — уже после загрузки в базу: `/db-cfe-admin check`.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# cfe-validate v1.14 — Validate 1C configuration extension structure (CFE)
|
||||
# cfe-validate v1.16 — Validate 1C configuration extension structure (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -143,25 +143,25 @@ $validClassIds = @(
|
||||
"fb282519-d103-4dd3-bc12-cb271d631dfc"
|
||||
)
|
||||
|
||||
# 44 types in canonical order
|
||||
# 46 types in canonical order
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","IntegrationService"
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
# Type -> directory mapping
|
||||
$childTypeDirMap = @{
|
||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
|
||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"; "PaletteColor"="PaletteColors"
|
||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||
@@ -180,7 +180,7 @@ $childTypeDirMap = @{
|
||||
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
|
||||
"CalculationRegister"="CalculationRegisters"
|
||||
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
|
||||
"IntegrationService"="IntegrationServices"
|
||||
"ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
|
||||
}
|
||||
|
||||
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
|
||||
@@ -208,6 +208,7 @@ $generatedTypeCategories = @{
|
||||
"Sequence" = @("Record","Manager","RecordSet")
|
||||
"FilterCriterion" = @("Manager","List")
|
||||
"SettingsStorage" = @("Manager")
|
||||
"ExternalDataSource" = @("Manager","TablesManager","CubesManager")
|
||||
"IntegrationService" = @("Manager")
|
||||
"WSReference" = @("Manager")
|
||||
"DefinedType" = @("DefinedType")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# cfe-validate v1.14 — Validate 1C configuration extension XML structure (CFE)
|
||||
# cfe-validate v1.16 — Validate 1C configuration extension XML structure (CFE)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
||||
import sys, os, argparse, re
|
||||
@@ -55,20 +55,20 @@ VALID_CLASS_IDS = [
|
||||
'fb282519-d103-4dd3-bc12-cb271d631dfc',
|
||||
]
|
||||
|
||||
# 44 types in canonical order
|
||||
# 46 types in canonical order
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'IntegrationService',
|
||||
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
|
||||
]
|
||||
|
||||
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
|
||||
@@ -97,7 +97,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||
'Bot': 'Bots',
|
||||
'Bot': 'Bots', 'PaletteColor': 'PaletteColors',
|
||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||
@@ -114,6 +114,7 @@ CHILD_TYPE_DIR_MAP = {
|
||||
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
|
||||
'CalculationRegister': 'CalculationRegisters',
|
||||
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
|
||||
'ExternalDataSource': 'ExternalDataSources',
|
||||
'IntegrationService': 'IntegrationServices',
|
||||
}
|
||||
|
||||
@@ -142,6 +143,7 @@ GENERATED_TYPE_CATEGORIES = {
|
||||
'Sequence': ['Record', 'Manager', 'RecordSet'],
|
||||
'FilterCriterion': ['Manager', 'List'],
|
||||
'SettingsStorage': ['Manager'],
|
||||
'ExternalDataSource': ['Manager', 'TablesManager', 'CubesManager'],
|
||||
'IntegrationService': ['Manager'],
|
||||
'WSReference': ['Manager'],
|
||||
'DefinedType': ['DefinedType'],
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
name: db-cfe-admin
|
||||
description: Управление расширениями конфигурации в информационной базе 1С. Используй когда нужно узнать какие расширения подключены к базе, выполнить проверку применимости или синтаксическую проверку, изменить безопасный режим или активность, удалить расширение из базы
|
||||
argument-hint: <list|check|set-properties|delete> [database] [-Name <Имя>]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /db-cfe-admin — Управление расширениями конфигурации
|
||||
|
||||
Расширения **на стороне базы**: состав и свойства подключения, проверки, удаление.
|
||||
Про исходники расширения — другие навыки, см. «Смежное».
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-cfe-admin list [database]
|
||||
/db-cfe-admin check [database] [-Name Расш1]
|
||||
/db-cfe-admin set-properties [database] -Name Расш1 -SafeMode off
|
||||
/db-cfe-admin delete [database] -Name Расш1
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
|
||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||
4. Если ветка не совпала — используй `default`
|
||||
|
||||
Если `v8path` не задан — скрипт сам попытается определить платформу.
|
||||
|
||||
## Команда
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-cfe-admin.ps1" -Command <команда> <параметры>
|
||||
```
|
||||
|
||||
### Общие параметры
|
||||
|
||||
| Параметр | Обязательный | Описание |
|
||||
|----------|:------------:|----------|
|
||||
| `-Command <команда>` | да | `list` / `check` / `set-properties` / `delete` |
|
||||
| `-V8Path <путь>` | нет | Каталог bin платформы или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||
| `-UserName <имя>` | нет | Пользователь базы |
|
||||
| `-Password <пароль>` | нет | Пароль пользователя |
|
||||
| `-Name <имя>` | усл. | Расширение. Обязателен для `set-properties`; в `delete` — вместо `-All`. Без него `list` и `check` работают по всем расширениям |
|
||||
| `-All` | усл. | Только для `delete`: удалить все расширения. Вместо `-Name`, а не вместе с ним |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы `1cv8.exe` через запятую |
|
||||
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||
|
||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||
|
||||
### Параметры `check`
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-Checks <список>` | `apply` — применимость расширения, `modules` — синтаксическая проверка, `config` — проверки конфигурации (целостность, ссылки, неиспользуемые процедуры и обработчики). Через запятую, по умолчанию `apply,modules` |
|
||||
| `-Context <список>` | Контексты синтаксической проверки: `ThinClient`, `WebClient`, `Server`, `ExternalConnection`, `ThickClientManagedApplication`, `ThickClientOrdinaryApplication`, `MobileClient` и др. Через запятую, по умолчанию `ThinClient,Server` |
|
||||
|
||||
Применимость и синтаксис проверяют разное и друг друга не заменяют. Проверяется только
|
||||
расширение: ошибки самой конфигурации сюда не попадают.
|
||||
|
||||
### Свойства `set-properties`
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `-SafeMode <on/off>` | Безопасный режим |
|
||||
| `-Active <on/off>` | Активность расширения |
|
||||
| `-UnsafeActionProtection <on/off>` | Защита от опасных действий |
|
||||
| `-UsedInDistributedInfobase <on/off>` | Использование в распределённой ИБ |
|
||||
| `-Scope <область>` | `infobase` / `data-separation` |
|
||||
| `-SecurityProfile <имя>` | Профиль безопасности |
|
||||
|
||||
Передавай только то, что меняешь: не указанное свойство остаётся как было. Расширение, впервые
|
||||
попавшее в базу загрузкой, создаётся с включённым безопасным режимом, а в нём расширение модуля не
|
||||
применяется.
|
||||
|
||||
Свойствами управляет `ibcmd` — он есть не в каждой установке платформы и работает с файловой базой;
|
||||
остальные команды работают всегда.
|
||||
|
||||
## Смежное
|
||||
|
||||
| Задача | Навык |
|
||||
|--------|-------|
|
||||
| Создать расширение, заимствовать объекты, перехватить метод | `/cfe-init`, `/cfe-borrow`, `/cfe-patch-method`, `/cfe-validate` |
|
||||
| Загрузить исходники расширения в базу | `/db-load-xml -Extension` (из коммита Git — `/db-load-git`) |
|
||||
| Загрузить готовый `.cfe` | `/db-load-cf -Extension` |
|
||||
| Выгрузить расширение из базы | `/db-dump-xml -Extension`, `/db-dump-cf -Extension` |
|
||||
| Обновить конфигурацию базы после загрузки | `/db-update -Extension` |
|
||||
| Проверить дрейф контролируемых методов по исходникам | `/cfe-patch-method -Check` |
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Что подключено к базе
|
||||
... -Command list -InfoBasePath "C:\Bases\MyDB"
|
||||
|
||||
# Проверить расширение
|
||||
... -Command check -InfoBasePath "C:\Bases\MyDB" -Name "Расш1"
|
||||
|
||||
# Синтаксическая проверка в контексте веб-клиента
|
||||
... -Command check -InfoBasePath "C:\Bases\MyDB" -Checks modules -Context WebClient,Server
|
||||
|
||||
# Снять безопасный режим
|
||||
... -Command set-properties -InfoBasePath "C:\Bases\MyDB" -Name "Расш1" -SafeMode off
|
||||
|
||||
# Отключить, не удаляя
|
||||
... -Command set-properties -InfoBasePath "C:\Bases\MyDB" -Name "Расш1" -Active off
|
||||
|
||||
# Убрать расширение из базы
|
||||
... -Command delete -InfoBasePath "C:\Bases\MyDB" -Name "Расш1"
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -88,6 +88,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -138,6 +149,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -122,6 +133,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
|
||||
@@ -104,6 +104,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -154,6 +165,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -122,6 +133,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
|
||||
@@ -88,6 +88,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -138,6 +149,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -122,6 +133,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
|
||||
@@ -225,6 +225,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -275,6 +286,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -207,6 +218,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
|
||||
@@ -76,6 +76,9 @@ allowed-tools:
|
||||
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
|
||||
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
|
||||
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
|
||||
| `newObjectPosition` | `"end"` / `"byName"` | Куда попадает новый объект в составе конфигурации: в конец своего вида (по умолчанию) или на место по имени. Переопределяется в `databases[]` |
|
||||
| `extensionApplyCheck` | bool | Проверять ли применимость расширения после загрузки в базу (по умолчанию `true`). Разово отключается ключом `-NoApplyCheck` |
|
||||
| `externalCheck` | bool | Проверять ли исходники внешней обработки/отчёта платформой перед сборкой (по умолчанию `true`). Разово отключается ключом `-Checks off` |
|
||||
| `databases` | array | Массив баз данных |
|
||||
| `default` | string | id базы по умолчанию |
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-InputFile <путь>` | да | Путь к CF-файлу |
|
||||
| `-Extension <имя>` | нет | Загрузить как расширение |
|
||||
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения из архива |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||
|
||||
@@ -78,6 +78,16 @@ param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||
# но расширение при этом неприменимо.
|
||||
[switch]$StrictLog,
|
||||
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$NoApplyCheck,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
@@ -121,6 +131,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -171,6 +192,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -384,6 +413,85 @@ function Write-PlatformOutput {
|
||||
}
|
||||
|
||||
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
|
||||
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
|
||||
#
|
||||
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
|
||||
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
|
||||
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
|
||||
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
|
||||
#
|
||||
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
|
||||
function Invoke-ApplyCheck {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
|
||||
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
|
||||
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
|
||||
$a += $ExtraArgs
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
|
||||
}
|
||||
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
|
||||
# поднимать ли код возврата (строгий режим).
|
||||
function Invoke-ApplyCheckReport {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
|
||||
if ($ac.Skipped) {
|
||||
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
|
||||
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
|
||||
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
|
||||
function Get-ApplyCheckEnabled {
|
||||
param([switch]$Disabled)
|
||||
if ($Disabled) { return $false }
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
|
||||
} catch {}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
|
||||
# --- Resolve additional arguments for the selected engine ---
|
||||
@@ -434,22 +542,32 @@ try {
|
||||
} else {
|
||||
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
$acConn = @("/F", "`"$InfoBasePath`"")
|
||||
if ($UserName) { $acConn += "/N`"$UserName`"" }
|
||||
if ($Password) { $acConn += "/P`"$Password`"" }
|
||||
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
Write-PlatformOutput $output
|
||||
exit $exitCode
|
||||
}
|
||||
|
||||
# --- 1cv8 branch ---
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
|
||||
$connArgs = @()
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
$connArgs += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
if ($UserName) { $connArgs += "/N`"$UserName`"" }
|
||||
if ($Password) { $connArgs += "/P`"$Password`"" }
|
||||
|
||||
$arguments = @("DESIGNER") + $connArgs
|
||||
|
||||
$arguments += "/LoadCfg", "`"$InputFile`""
|
||||
|
||||
@@ -488,6 +606,11 @@ try {
|
||||
}
|
||||
Write-PlatformOutput $__v8.Output
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -122,6 +133,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
@@ -360,6 +379,91 @@ def print_platform_output(result):
|
||||
print("--- End ---")
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def run_apply_check(exe, conn_args, extension, extra_args):
|
||||
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
|
||||
которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал
|
||||
регистрации.
|
||||
|
||||
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
|
||||
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
|
||||
отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с
|
||||
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
|
||||
состоялась.
|
||||
|
||||
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
|
||||
"""
|
||||
exe_dir = os.path.dirname(exe)
|
||||
leaf = os.path.basename(exe)
|
||||
if leaf.lower().startswith("ibcmd"):
|
||||
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.isfile(v8):
|
||||
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
|
||||
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
|
||||
try:
|
||||
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
|
||||
if extension:
|
||||
a += ["-Extension", f'"{extension}"']
|
||||
out_file = os.path.join(temp_dir, "check_log.txt")
|
||||
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
|
||||
a += list(extra_args)
|
||||
r = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
|
||||
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
|
||||
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def apply_check_report(exe, conn_args, extension, extra_args):
|
||||
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
|
||||
поднимать ли код возврата (строгий режим)."""
|
||||
ac = run_apply_check(exe, conn_args, extension, extra_args)
|
||||
if ac["skipped"]:
|
||||
print(f"[note] applicability check skipped: {ac['reason']}")
|
||||
return False
|
||||
if ac["exit"] != 0 or ac["lines"]:
|
||||
print("[warning] the extension is loaded, but the platform will not apply it:")
|
||||
for line in ac["lines"]:
|
||||
print(f" {line}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_check_enabled(disabled):
|
||||
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
|
||||
if disabled:
|
||||
return False
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
if proj.get("extensionApplyCheck") is not None:
|
||||
return bool(proj.get("extensionApplyCheck"))
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
"""Run an ibcmd command non-interactively.
|
||||
|
||||
@@ -425,6 +529,12 @@ def main():
|
||||
parser.add_argument("-InputFile", required=True)
|
||||
parser.add_argument("-Extension", default="")
|
||||
parser.add_argument("-AllExtensions", action="store_true")
|
||||
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||
# но расширение при этом неприменимо.
|
||||
parser.add_argument("-StrictLog", action="store_true")
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
parser.add_argument("-NoApplyCheck", action="store_true")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -490,6 +600,18 @@ def main():
|
||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
||||
else:
|
||||
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
exit_code = result.returncode
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
ac_conn = ["/F", f'"{args.InfoBasePath}"']
|
||||
if args.UserName:
|
||||
ac_conn.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
ac_conn.append(f'/P"{args.Password}"')
|
||||
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(result.returncode)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -498,17 +620,20 @@ def main():
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
|
||||
conn_args = []
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||
conn_args.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||
else:
|
||||
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||
conn_args.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f'/N"{args.UserName}"')
|
||||
conn_args.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
conn_args.append(f'/P"{args.Password}"')
|
||||
|
||||
arguments = ["DESIGNER"] + conn_args
|
||||
|
||||
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
|
||||
|
||||
@@ -546,6 +671,12 @@ def main():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
print_platform_output(result)
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
@@ -118,6 +118,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -168,6 +179,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -122,6 +133,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
|
||||
@@ -55,6 +55,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
|
||||
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
|
||||
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
|
||||
| `-Extension <имя>` | нет | Загрузить в расширение |
|
||||
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
|
||||
|
||||
@@ -116,6 +116,10 @@ param(
|
||||
# но в логе есть отбраковка.
|
||||
[switch]$StrictLog,
|
||||
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$NoApplyCheck,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPath,
|
||||
|
||||
@@ -260,6 +264,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -310,6 +325,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -554,7 +577,74 @@ function Find-SilentRejections {
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
|
||||
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
|
||||
#
|
||||
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
|
||||
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
|
||||
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
|
||||
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
|
||||
#
|
||||
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
|
||||
function Invoke-ApplyCheck {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
|
||||
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
|
||||
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
|
||||
$a += $ExtraArgs
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
|
||||
}
|
||||
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
|
||||
# поднимать ли код возврата (строгий режим).
|
||||
function Invoke-ApplyCheckReport {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
|
||||
if ($ac.Skipped) {
|
||||
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
|
||||
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
|
||||
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
|
||||
function Get-ApplyCheckEnabled {
|
||||
param([switch]$Disabled)
|
||||
if ($Disabled) { return $false }
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
|
||||
} catch {}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
if ($engine -eq "ibcmd") {
|
||||
if (-not $InfoBasePath) {
|
||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
||||
@@ -763,6 +853,13 @@ try {
|
||||
} else {
|
||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
$acConn = @("/F", "`"$InfoBasePath`"")
|
||||
if ($UserName) { $acConn += "/N`"$UserName`"" }
|
||||
if ($Password) { $acConn += "/P`"$Password`"" }
|
||||
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
Write-PlatformOutput $applyOut
|
||||
}
|
||||
exit $exitCode
|
||||
@@ -775,21 +872,24 @@ try {
|
||||
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
|
||||
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
|
||||
$connArgs = @()
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
$connArgs += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
if ($UserName) { $connArgs += "/N`"$UserName`"" }
|
||||
if ($Password) { $connArgs += "/P`"$Password`"" }
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
$__repo = Resolve-RepositorySettings
|
||||
$arguments += Get-RepositoryArgs $__repo
|
||||
$connArgs += Get-RepositoryArgs $__repo
|
||||
|
||||
$arguments = @("DESIGNER") + $connArgs
|
||||
|
||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
$arguments += "-listFile", "`"$listFile`""
|
||||
@@ -853,6 +953,11 @@ try {
|
||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -228,6 +239,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
@@ -498,6 +517,76 @@ def find_silent_rejections(log_text):
|
||||
return found
|
||||
|
||||
|
||||
def run_apply_check(exe, conn_args, extension, extra_args):
|
||||
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
|
||||
которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал
|
||||
регистрации.
|
||||
|
||||
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
|
||||
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
|
||||
отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с
|
||||
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
|
||||
состоялась.
|
||||
|
||||
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
|
||||
"""
|
||||
exe_dir = os.path.dirname(exe)
|
||||
leaf = os.path.basename(exe)
|
||||
if leaf.lower().startswith("ibcmd"):
|
||||
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.isfile(v8):
|
||||
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
|
||||
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
|
||||
try:
|
||||
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
|
||||
if extension:
|
||||
a += ["-Extension", f'"{extension}"']
|
||||
out_file = os.path.join(temp_dir, "check_log.txt")
|
||||
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
|
||||
a += list(extra_args)
|
||||
r = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
|
||||
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
|
||||
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def apply_check_report(exe, conn_args, extension, extra_args):
|
||||
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
|
||||
поднимать ли код возврата (строгий режим)."""
|
||||
ac = run_apply_check(exe, conn_args, extension, extra_args)
|
||||
if ac["skipped"]:
|
||||
print(f"[note] applicability check skipped: {ac['reason']}")
|
||||
return False
|
||||
if ac["exit"] != 0 or ac["lines"]:
|
||||
print("[warning] the extension is loaded, but the platform will not apply it:")
|
||||
for line in ac["lines"]:
|
||||
print(f" {line}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_check_enabled(disabled):
|
||||
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
|
||||
if disabled:
|
||||
return False
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
if proj.get("extensionApplyCheck") is not None:
|
||||
return bool(proj.get("extensionApplyCheck"))
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
"""Run an ibcmd command non-interactively.
|
||||
|
||||
@@ -607,6 +696,8 @@ def main():
|
||||
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||
# но в логе есть отбраковка.
|
||||
parser.add_argument("-StrictLog", action="store_true")
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
parser.add_argument("-NoApplyCheck", action="store_true")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -809,6 +900,17 @@ def main():
|
||||
else:
|
||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||
print_platform_output(ar)
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
ac_conn = ["/F", f'"{args.InfoBasePath}"']
|
||||
if args.UserName:
|
||||
ac_conn.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
ac_conn.append(f'/P"{args.Password}"')
|
||||
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Write list file (UTF-8 with BOM) ---
|
||||
@@ -817,22 +919,25 @@ def main():
|
||||
f.write("\n".join(config_files))
|
||||
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
|
||||
conn_args = []
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||
conn_args += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||
else:
|
||||
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||
conn_args += ["/F", f'"{args.InfoBasePath}"']
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f'/N"{args.UserName}"')
|
||||
conn_args.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
conn_args.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
arguments.extend(repository_args(repo))
|
||||
conn_args.extend(repository_args(repo))
|
||||
|
||||
arguments = ["DESIGNER"] + conn_args
|
||||
|
||||
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||
arguments += ["-listFile", f'"{list_file}"']
|
||||
@@ -900,6 +1005,12 @@ def main():
|
||||
if args.StrictLog and exit_code == 0:
|
||||
exit_code = 1
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
|
||||
@@ -58,6 +58,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <
|
||||
| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) |
|
||||
| `-Extension <имя>` | нет | Загрузить в расширение |
|
||||
| `-AllExtensions` | нет | Загрузить все расширения |
|
||||
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
|
||||
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
|
||||
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
|
||||
@@ -112,6 +112,10 @@ param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$StrictLog,
|
||||
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$NoApplyCheck,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPath,
|
||||
|
||||
@@ -273,6 +277,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -323,6 +338,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -572,6 +595,73 @@ function Find-SilentRejections {
|
||||
}
|
||||
|
||||
|
||||
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
|
||||
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
|
||||
#
|
||||
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
|
||||
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
|
||||
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
|
||||
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
|
||||
#
|
||||
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
|
||||
function Invoke-ApplyCheck {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
|
||||
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
|
||||
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
|
||||
$a += $ExtraArgs
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
|
||||
}
|
||||
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
|
||||
# поднимать ли код возврата (строгий режим).
|
||||
function Invoke-ApplyCheckReport {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
|
||||
if ($ac.Skipped) {
|
||||
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
|
||||
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
|
||||
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
|
||||
function Get-ApplyCheckEnabled {
|
||||
param([switch]$Disabled)
|
||||
if ($Disabled) { return $false }
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
|
||||
} catch {}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
|
||||
# --- Resolve additional arguments for the selected engine ---
|
||||
@@ -681,26 +771,36 @@ try {
|
||||
}
|
||||
Write-PlatformOutput $applyOut
|
||||
}
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
$acConn = @("/F", "`"$InfoBasePath`"")
|
||||
if ($UserName) { $acConn += "/N`"$UserName`"" }
|
||||
if ($Password) { $acConn += "/P`"$Password`"" }
|
||||
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
exit $exitCode
|
||||
}
|
||||
|
||||
# --- 1cv8 branch ---
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
|
||||
$connArgs = @()
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
$connArgs += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
if ($UserName) { $connArgs += "/N`"$UserName`"" }
|
||||
if ($Password) { $connArgs += "/P`"$Password`"" }
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
$__repo = Resolve-RepositorySettings
|
||||
$arguments += Get-RepositoryArgs $__repo
|
||||
$connArgs += Get-RepositoryArgs $__repo
|
||||
|
||||
$arguments = @("DESIGNER") + $connArgs
|
||||
|
||||
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
|
||||
|
||||
@@ -806,6 +906,11 @@ try {
|
||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
# Расширение могло загрузиться «успешно» и при этом остаться неприменимым — спрашиваем платформу.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -228,6 +239,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
@@ -498,6 +517,76 @@ def find_silent_rejections(log_text):
|
||||
return found
|
||||
|
||||
|
||||
def run_apply_check(exe, conn_args, extension, extra_args):
|
||||
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
|
||||
которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал
|
||||
регистрации.
|
||||
|
||||
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
|
||||
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
|
||||
отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с
|
||||
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
|
||||
состоялась.
|
||||
|
||||
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
|
||||
"""
|
||||
exe_dir = os.path.dirname(exe)
|
||||
leaf = os.path.basename(exe)
|
||||
if leaf.lower().startswith("ibcmd"):
|
||||
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.isfile(v8):
|
||||
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
|
||||
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
|
||||
try:
|
||||
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
|
||||
if extension:
|
||||
a += ["-Extension", f'"{extension}"']
|
||||
out_file = os.path.join(temp_dir, "check_log.txt")
|
||||
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
|
||||
a += list(extra_args)
|
||||
r = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
|
||||
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
|
||||
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def apply_check_report(exe, conn_args, extension, extra_args):
|
||||
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
|
||||
поднимать ли код возврата (строгий режим)."""
|
||||
ac = run_apply_check(exe, conn_args, extension, extra_args)
|
||||
if ac["skipped"]:
|
||||
print(f"[note] applicability check skipped: {ac['reason']}")
|
||||
return False
|
||||
if ac["exit"] != 0 or ac["lines"]:
|
||||
print("[warning] the extension is loaded, but the platform will not apply it:")
|
||||
for line in ac["lines"]:
|
||||
print(f" {line}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_check_enabled(disabled):
|
||||
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
|
||||
if disabled:
|
||||
return False
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
if proj.get("extensionApplyCheck") is not None:
|
||||
return bool(proj.get("extensionApplyCheck"))
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
"""Run an ibcmd command non-interactively.
|
||||
|
||||
@@ -586,6 +675,8 @@ def main():
|
||||
action="store_true",
|
||||
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
|
||||
)
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
parser.add_argument("-NoApplyCheck", action="store_true")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -706,6 +797,17 @@ def main():
|
||||
else:
|
||||
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
|
||||
print_platform_output(ar)
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
ac_conn = ["/F", f'"{args.InfoBasePath}"']
|
||||
if args.UserName:
|
||||
ac_conn.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
ac_conn.append(f'/P"{args.Password}"')
|
||||
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -714,22 +816,25 @@ def main():
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
|
||||
conn_args = []
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||
conn_args += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
|
||||
else:
|
||||
arguments += ["/F", f'"{args.InfoBasePath}"']
|
||||
conn_args += ["/F", f'"{args.InfoBasePath}"']
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f'/N"{args.UserName}"')
|
||||
conn_args.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
conn_args.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
arguments.extend(repository_args(repo))
|
||||
conn_args.extend(repository_args(repo))
|
||||
|
||||
arguments = ["DESIGNER"] + conn_args
|
||||
|
||||
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
|
||||
|
||||
@@ -840,6 +945,12 @@ def main():
|
||||
if args.StrictLog and exit_code == 0:
|
||||
exit_code = 1
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
|
||||
@@ -244,6 +244,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -404,6 +412,17 @@ $script:V8OwnedKeys = @(
|
||||
'/ConfigurationRepositoryOptimizeData', '/ConfigurationRepositoryClearCache',
|
||||
'/ConfigurationRepositoryClearLocalCache', '/ConfigurationRepositoryClearGlobalCache'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @()
|
||||
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP', '-Pwd')
|
||||
$script:IbcmdSecretKeys = @()
|
||||
|
||||
@@ -37,6 +37,17 @@ V8_OWNED_KEYS = [
|
||||
"/ConfigurationRepositoryOptimizeData", "/ConfigurationRepositoryClearCache",
|
||||
"/ConfigurationRepositoryClearLocalCache", "/ConfigurationRepositoryClearGlobalCache",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = []
|
||||
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP", "-Pwd"]
|
||||
IBCMD_SECRET_KEYS = []
|
||||
@@ -134,6 +145,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
|
||||
@@ -107,6 +107,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -157,6 +168,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -64,6 +64,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -119,6 +130,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
|
||||
@@ -11,14 +11,16 @@ allowed-tools:
|
||||
|
||||
# /db-update — Обновление конфигурации БД
|
||||
|
||||
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`.
|
||||
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`) —
|
||||
отдельным шагом после загрузки. У `/db-load-xml` и `/db-load-git` то же самое делает
|
||||
ключ `-UpdateDB`.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/db-update [database]
|
||||
/db-update dev
|
||||
/db-update dev -Dynamic+
|
||||
/db-update dev -Dynamic on
|
||||
```
|
||||
|
||||
## Параметры подключения
|
||||
@@ -50,7 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-Extension <имя>` | нет | Обновить расширение |
|
||||
| `-AllExtensions` | нет | Обновить все расширения |
|
||||
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
|
||||
| `-NoApplyCheck` | нет | Не проверять применимость расширения после обновления |
|
||||
| `-Dynamic <on/off>` | нет | `on` — динамическое обновление, без монопольного доступа к базе; `off` — отключить |
|
||||
| `-Server` | нет | Обновление на стороне сервера |
|
||||
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
@@ -68,20 +71,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
|
||||
| `-BackgroundSuspend` | Приостановить |
|
||||
| `-BackgroundResume` | Возобновить |
|
||||
|
||||
## Предупреждения
|
||||
|
||||
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|
||||
- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки
|
||||
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
# Обычное обновление (файловая база)
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||
|
||||
# Динамическое обновление (серверная база)
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
||||
# Динамическое обновление
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic on
|
||||
|
||||
# Обновление расширения
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# db-update v1.19 — Update 1C database configuration
|
||||
# db-update v1.20 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -34,7 +34,7 @@
|
||||
Обновить все расширения
|
||||
|
||||
.PARAMETER Dynamic
|
||||
Динамическое обновление: "+" включить, "-" отключить
|
||||
Динамическое обновление: on включить, off отключить
|
||||
|
||||
.PARAMETER Server
|
||||
Обновление на стороне сервера
|
||||
@@ -81,8 +81,10 @@ param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$AllExtensions,
|
||||
|
||||
# on/off, а не +/-: значение "-" через powershell.exe -File парсер не связывает и молча
|
||||
# выходит с кодом 2, без единого сообщения. "+"/"-" принимаются, но в инструкции не значатся.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("+", "-")]
|
||||
[ValidateSet("on", "off", "yes", "no", "+", "-")]
|
||||
[string]$Dynamic,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
@@ -97,6 +99,10 @@ param(
|
||||
# но в логе есть отбраковка.
|
||||
[switch]$StrictLog,
|
||||
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[switch]$NoApplyCheck,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$RepositoryPath,
|
||||
|
||||
@@ -113,6 +119,8 @@ param(
|
||||
[string[]]$AdditionalIbcmdArguments = @()
|
||||
)
|
||||
|
||||
if ($Dynamic) { $Dynamic = if (@('on', 'yes', '+') -contains $Dynamic.ToLower()) { '+' } else { '-' } }
|
||||
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
@@ -233,6 +241,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -283,6 +302,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -530,6 +557,73 @@ function Find-SilentRejections {
|
||||
}
|
||||
|
||||
|
||||
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
|
||||
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
|
||||
#
|
||||
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
|
||||
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
|
||||
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
|
||||
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
|
||||
#
|
||||
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
|
||||
function Invoke-ApplyCheck {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
|
||||
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
|
||||
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
|
||||
$a += $ExtraArgs
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
|
||||
}
|
||||
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
|
||||
# поднимать ли код возврата (строгий режим).
|
||||
function Invoke-ApplyCheckReport {
|
||||
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
|
||||
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
|
||||
if ($ac.Skipped) {
|
||||
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
|
||||
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
|
||||
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
|
||||
function Get-ApplyCheckEnabled {
|
||||
param([switch]$Disabled)
|
||||
if ($Disabled) { return $false }
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
|
||||
} catch {}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
|
||||
# --- Resolve additional arguments for the selected engine ---
|
||||
@@ -575,27 +669,37 @@ try {
|
||||
} else {
|
||||
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
|
||||
}
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
$acConn = @("/F", "`"$InfoBasePath`"")
|
||||
if ($UserName) { $acConn += "/N`"$UserName`"" }
|
||||
if ($Password) { $acConn += "/P`"$Password`"" }
|
||||
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
Write-PlatformOutput $output
|
||||
exit $exitCode
|
||||
}
|
||||
|
||||
# --- 1cv8 branch ---
|
||||
# --- Build arguments ---
|
||||
$arguments = @("DESIGNER")
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
|
||||
$connArgs = @()
|
||||
|
||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
||||
} else {
|
||||
$arguments += "/F", "`"$InfoBasePath`""
|
||||
$connArgs += "/F", "`"$InfoBasePath`""
|
||||
}
|
||||
|
||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
||||
if ($UserName) { $connArgs += "/N`"$UserName`"" }
|
||||
if ($Password) { $connArgs += "/P`"$Password`"" }
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
$__repo = Resolve-RepositorySettings
|
||||
$arguments += Get-RepositoryArgs $__repo
|
||||
$connArgs += Get-RepositoryArgs $__repo
|
||||
|
||||
$arguments = @("DESIGNER") + $connArgs
|
||||
|
||||
$arguments += "/UpdateDBCfg"
|
||||
|
||||
@@ -656,6 +760,11 @@ try {
|
||||
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
|
||||
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# db-update v1.19 — Update 1C database configuration
|
||||
# db-update v1.20 — Update 1C database configuration
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -207,6 +218,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
@@ -477,6 +496,76 @@ def find_silent_rejections(log_text):
|
||||
return found
|
||||
|
||||
|
||||
def run_apply_check(exe, conn_args, extension, extra_args):
|
||||
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
|
||||
которое не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал
|
||||
регистрации.
|
||||
|
||||
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
|
||||
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
|
||||
отбрасывает — проверено на 8.3.24, /LoadConfigFromFiles вместе с
|
||||
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
|
||||
состоялась.
|
||||
|
||||
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
|
||||
"""
|
||||
exe_dir = os.path.dirname(exe)
|
||||
leaf = os.path.basename(exe)
|
||||
if leaf.lower().startswith("ibcmd"):
|
||||
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.isfile(v8):
|
||||
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
|
||||
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
|
||||
try:
|
||||
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
|
||||
if extension:
|
||||
a += ["-Extension", f'"{extension}"']
|
||||
out_file = os.path.join(temp_dir, "check_log.txt")
|
||||
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
|
||||
a += list(extra_args)
|
||||
r = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
|
||||
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
|
||||
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
|
||||
finally:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def apply_check_report(exe, conn_args, extension, extra_args):
|
||||
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
|
||||
поднимать ли код возврата (строгий режим)."""
|
||||
ac = run_apply_check(exe, conn_args, extension, extra_args)
|
||||
if ac["skipped"]:
|
||||
print(f"[note] applicability check skipped: {ac['reason']}")
|
||||
return False
|
||||
if ac["exit"] != 0 or ac["lines"]:
|
||||
print("[warning] the extension is loaded, but the platform will not apply it:")
|
||||
for line in ac["lines"]:
|
||||
print(f" {line}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_check_enabled(disabled):
|
||||
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
|
||||
if disabled:
|
||||
return False
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding="utf-8-sig") as f:
|
||||
proj = json.load(f)
|
||||
if proj.get("extensionApplyCheck") is not None:
|
||||
return bool(proj.get("extensionApplyCheck"))
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
||||
"""Run an ibcmd command non-interactively.
|
||||
|
||||
@@ -544,13 +633,17 @@ def main():
|
||||
parser.add_argument("-RepositoryPassword", default="")
|
||||
parser.add_argument("-Extension", default="")
|
||||
parser.add_argument("-AllExtensions", action="store_true")
|
||||
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
|
||||
# on/off, а не +/-: значение "-" через powershell.exe -File парсер PS не связывает и молча
|
||||
# выходит с кодом 2. "+"/"-" принимаются, но в инструкции не значатся.
|
||||
parser.add_argument("-Dynamic", default="", choices=["", "on", "off", "yes", "no", "+", "-"])
|
||||
parser.add_argument("-Server", action="store_true")
|
||||
parser.add_argument("-WarningsAsErrors", action="store_true")
|
||||
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
|
||||
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
|
||||
# но в логе есть отбраковка.
|
||||
parser.add_argument("-StrictLog", action="store_true")
|
||||
# Пропустить проверку применимости расширения после загрузки.
|
||||
parser.add_argument("-NoApplyCheck", action="store_true")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -559,6 +652,9 @@ def main():
|
||||
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
|
||||
args = ci_parse_args(parser, argv)
|
||||
|
||||
if args.Dynamic:
|
||||
args.Dynamic = "+" if args.Dynamic.lower() in ("on", "yes", "+") else "-"
|
||||
|
||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
|
||||
assert_infobase_exists(args.InfoBasePath)
|
||||
@@ -614,6 +710,18 @@ def main():
|
||||
print("Database configuration updated successfully")
|
||||
else:
|
||||
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}")
|
||||
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
|
||||
exit_code = result.returncode
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
ac_conn = ["/F", f'"{args.InfoBasePath}"']
|
||||
if args.UserName:
|
||||
ac_conn.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
ac_conn.append(f'/P"{args.Password}"')
|
||||
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(result.returncode)
|
||||
|
||||
# --- Temp dir ---
|
||||
@@ -622,22 +730,25 @@ def main():
|
||||
|
||||
try:
|
||||
# --- Build arguments ---
|
||||
arguments = ["DESIGNER"]
|
||||
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
|
||||
conn_args = []
|
||||
|
||||
if args.InfoBaseServer and args.InfoBaseRef:
|
||||
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||
conn_args.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
|
||||
else:
|
||||
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||
conn_args.extend(["/F", f'"{args.InfoBasePath}"'])
|
||||
|
||||
if args.UserName:
|
||||
arguments.append(f'/N"{args.UserName}"')
|
||||
conn_args.append(f'/N"{args.UserName}"')
|
||||
if args.Password:
|
||||
arguments.append(f'/P"{args.Password}"')
|
||||
conn_args.append(f'/P"{args.Password}"')
|
||||
|
||||
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
|
||||
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
|
||||
repo = resolve_repository_settings(args)
|
||||
arguments.extend(repository_args(repo))
|
||||
conn_args.extend(repository_args(repo))
|
||||
|
||||
arguments = ["DESIGNER"] + conn_args
|
||||
|
||||
arguments.append("/UpdateDBCfg")
|
||||
|
||||
@@ -700,6 +811,12 @@ def main():
|
||||
if args.StrictLog and exit_code == 0:
|
||||
exit_code = 1
|
||||
|
||||
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
|
||||
if (exit_code == 0 and (args.Extension or args.AllExtensions)
|
||||
and apply_check_enabled(args.NoApplyCheck)):
|
||||
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
|
||||
exit_code = 1
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
finally:
|
||||
|
||||
@@ -25,7 +25,8 @@ allowed-tools:
|
||||
|
||||
## Параметры подключения (опционально)
|
||||
|
||||
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
|
||||
Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается
|
||||
под проверку исходников, если она не отключена.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
@@ -55,11 +56,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
|
||||
| `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` |
|
||||
| `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||
|
||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||
|
||||
## Проверка перед сборкой
|
||||
|
||||
Перед сборкой исходники проверяет платформа — синтаксис модулей и наличие обработчиков форм.
|
||||
Если она нашла проблемы, сборка отменяется и файл не создаётся; в выводе — сообщение
|
||||
платформы со строкой и колонкой и путь к файлу исходника. Отключается `-Checks off`
|
||||
или ключом `"externalCheck": false` в `.v8-project.json`.
|
||||
|
||||
Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# epf-build v1.16 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
||||
<#
|
||||
@@ -72,6 +72,15 @@ param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$OutputFile,
|
||||
|
||||
# Что проверить в исходниках перед сборкой: modules (синтаксис в контекстах), handlers,
|
||||
# unreferenced, empty-handlers, config; off — не проверять. По умолчанию modules,handlers.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Checks,
|
||||
|
||||
# Контексты синтаксической проверки. По умолчанию ThinClient,Server.
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Context,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
@@ -98,6 +107,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -148,6 +168,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -369,6 +397,122 @@ function Test-OutputNonEmpty {
|
||||
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
|
||||
}
|
||||
|
||||
function Find-V8Project([string]$startDir) {
|
||||
$d = $startDir
|
||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
||||
$pj = Join-Path $d ".v8-project.json"
|
||||
if (Test-Path $pj) { return $pj }
|
||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
||||
if ($parent -eq $d) { break }
|
||||
$d = $parent
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- Проверка исходников платформой ---
|
||||
# Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML
|
||||
# и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии
|
||||
# обработки. Прямой команды «проверь внешнюю обработку» у платформы нет, но объект КОНФИГУРАЦИИ она
|
||||
# проверяет — поэтому обработка кладётся в конфигурацию временной базы (stub-db-create
|
||||
# -EmbedSourceFile) и спрашивается штатной /CheckConfig.
|
||||
#
|
||||
# Запуск отдельный и только через 1cv8: у ibcmd такой команды нет, а в одной командной строке
|
||||
# DESIGNER выполняет лишь последнюю пакетную команду.
|
||||
function Get-CheckFlags {
|
||||
param([string[]]$Checks, [string[]]$Contexts)
|
||||
$flags = @()
|
||||
if ($Checks -contains 'modules') { foreach ($c in $Contexts) { $flags += "-$c" } }
|
||||
if ($Checks -contains 'handlers') { $flags += '-HandlersExistence' }
|
||||
if ($Checks -contains 'unreferenced') { $flags += '-UnreferenceProcedures' }
|
||||
if ($Checks -contains 'empty-handlers') { $flags += '-EmptyHandlers' }
|
||||
if ($Checks -contains 'config') { $flags += '-ConfigLogIntegrity', '-IncorrectReferences' }
|
||||
return $flags
|
||||
}
|
||||
|
||||
# Платформа называет объект своим именем внутри конфигурации; модели нужен путь к исходнику.
|
||||
# Путь СКЛЕИВАЕТСЯ по конвенции выгрузки, поэтому возвращается только существующий файл:
|
||||
# выдуманный путь хуже отсутствующего — модель пойдёт открывать файл, которого нет.
|
||||
function Resolve-SourcePath {
|
||||
param([string]$Line, [string]$SourceDir)
|
||||
$candidate = $null
|
||||
$m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(?:Форма|Form)\.([^.]+)\.')
|
||||
if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value (Join-Path "Forms" (Join-Path $m.Groups[2].Value "Ext\Form\Module.bsl")))) }
|
||||
if (-not $candidate) {
|
||||
$m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(МодульОбъекта|ObjectModule)')
|
||||
if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value "Ext\ObjectModule.bsl")) }
|
||||
}
|
||||
if (-not $candidate) {
|
||||
$m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(МодульМенеджера|ManagerModule)')
|
||||
if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value "Ext\ManagerModule.bsl")) }
|
||||
}
|
||||
if ($candidate -and (Test-Path $candidate -PathType Leaf)) { return $candidate }
|
||||
return $null
|
||||
}
|
||||
|
||||
# $true, если платформа нашла проблемы — вызывающий не собирает артефакт.
|
||||
function Invoke-SourceCheck {
|
||||
param([string]$Exe, [string]$BasePath, [string[]]$Flags, [string]$SourceDir, [string[]]$ExtraArgs)
|
||||
$exeDir = Split-Path $Exe -Parent
|
||||
$exeLeaf = Split-Path $Exe -Leaf
|
||||
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
|
||||
if (-not (Test-Path $v8)) {
|
||||
Write-Host "[note] source check skipped: 1cv8 not found at $v8" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
$dir = Join-Path $env:TEMP "epf_check_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
try {
|
||||
$outFile = Join-Path $dir "check_log.txt"
|
||||
$a = @("DESIGNER", "/F", "`"$BasePath`"", "/CheckConfig") + $Flags + @("/Out", "`"$outFile`"", "/DisableStartupDialogs") + $ExtraArgs
|
||||
Write-Host "Running: 1cv8.exe $($a -join ' ')"
|
||||
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
|
||||
$lines = @()
|
||||
if (Test-Path $outFile) {
|
||||
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
||||
if ($raw) { $lines = @($raw -split "`r?`n" | Where-Object { $_.Trim() -ne '' }) }
|
||||
}
|
||||
# Платформа отвечает 101 на найденные проблемы; «Ошибок не обнаружено» приходит с кодом 0.
|
||||
if ($res.ExitCode -eq 0) { return $false }
|
||||
Write-Host "Error: платформа нашла проблемы в исходниках — сборка отменена" -ForegroundColor Red
|
||||
# Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя.
|
||||
if ($lines.Count -eq 0) { Write-Host " платформа вернула код $($res.ExitCode) без сообщений" -ForegroundColor Red }
|
||||
foreach ($l in $lines) {
|
||||
Write-Host " $($l.TrimEnd())" -ForegroundColor Red
|
||||
$srcPath = Resolve-SourcePath $l $SourceDir
|
||||
if ($srcPath) { Write-Host " -> $srcPath" -ForegroundColor Red }
|
||||
}
|
||||
return $true
|
||||
} finally {
|
||||
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
|
||||
# Проверять ли исходники: -Checks off сильнее настройки проекта externalCheck.
|
||||
function Get-SourceCheckList {
|
||||
param([string]$Checks)
|
||||
$known = @('modules', 'handlers', 'unreferenced', 'empty-handlers', 'config')
|
||||
if ($Checks) {
|
||||
$list = @($Checks -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ })
|
||||
if ($list -contains 'off') { return @() }
|
||||
foreach ($c in $list) {
|
||||
if ($known -notcontains $c) {
|
||||
Write-Host "Error: unknown check '$c' (expected: $($known -join ', ') or off)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
return $list
|
||||
}
|
||||
$pf = Find-V8Project (Get-Location).Path
|
||||
if ($pf) {
|
||||
try {
|
||||
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($null -ne $proj.externalCheck -and -not [bool]$proj.externalCheck) { return @() }
|
||||
} catch {}
|
||||
}
|
||||
return @('modules', 'handlers')
|
||||
}
|
||||
|
||||
|
||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
||||
|
||||
# --- Resolve additional arguments for the selected engine ---
|
||||
@@ -379,33 +523,69 @@ if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
$autoCreatedBase = $null
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
$sourceDir = Split-Path $SourceFile -Parent
|
||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
||||
# --- Что проверяем в исходниках перед сборкой ---
|
||||
$checkList = @(Get-SourceCheckList $Checks)
|
||||
$contextList = @()
|
||||
if ($Context) { $contextList = @($Context -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
|
||||
if ($contextList.Count -eq 0) { $contextList = @('ThinClient', 'Server') }
|
||||
elseif ($checkList.Count -gt 0 -and $checkList -notcontains 'modules') {
|
||||
Write-Host "Error: -Context задан, но в -Checks нет modules — контексты относятся только к ней" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$sourceDir = Split-Path $SourceFile -Parent
|
||||
|
||||
function New-StubBase {
|
||||
# Стаб запускает свои процессы платформы (CREATEINFOBASE, LoadConfigFromFiles, UpdateDBCfg) —
|
||||
# им нужны те же дополнительные аргументы, что и сборке. Передаются только явные: файл проекта
|
||||
# стаб читает сам. Вызов через -Command, не -File: -File берёт хвост буквально, и массивный
|
||||
# параметр пришёл бы одним склеенным токеном.
|
||||
param([string]$BasePath, [switch]$Embed)
|
||||
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
|
||||
Write-Host "No database specified. Creating temporary stub database..."
|
||||
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
|
||||
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
|
||||
# explicit ones are forwarded: the stub reads .v8-project.json itself.
|
||||
# Invoked via -Command, not -File: -File takes the tail literally, so an array
|
||||
# parameter would arrive as a single comma-glued token.
|
||||
$q = { param($s) "'" + ($s -replace "'", "''") + "'" }
|
||||
$stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $autoBasePath)"
|
||||
$stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $BasePath)"
|
||||
if ($Embed) { $stubCmd += " -EmbedSourceFile $(& $q $SourceFile)" }
|
||||
if ($AdditionalV8Arguments.Count -gt 0) {
|
||||
$stubCmd += " -AdditionalV8Arguments " + (($AdditionalV8Arguments | ForEach-Object { & $q $_ }) -join ',')
|
||||
}
|
||||
if ($AdditionalIbcmdArguments.Count -gt 0) {
|
||||
$stubCmd += " -AdditionalIbcmdArguments " + (($AdditionalIbcmdArguments | ForEach-Object { & $q $_ }) -join ',')
|
||||
}
|
||||
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru
|
||||
if ($stubProc.ExitCode -ne 0) {
|
||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
||||
$p = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru
|
||||
return $p.ExitCode
|
||||
}
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
$autoCreatedBase = $null
|
||||
$checkBase = $null
|
||||
$checkBasePath = $null
|
||||
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
|
||||
Write-Host "No database specified. Creating temporary stub database..."
|
||||
if ((New-StubBase $autoBasePath -Embed:($checkList.Count -gt 0)) -ne 0) {
|
||||
# С внедрённой обработкой база падает прежде всего из-за самих исходников
|
||||
# (пример: DefaultForm на несуществующую форму) — говорить про базу значит увести не туда.
|
||||
if ($checkList.Count -gt 0) {
|
||||
Write-Host "Error: платформа не приняла исходники при подготовке проверки — сборка отменена" -ForegroundColor Red
|
||||
Write-Host " сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт" -ForegroundColor Red
|
||||
} else {
|
||||
Write-Host "Error: failed to create stub database" -ForegroundColor Red
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
$InfoBasePath = $autoBasePath
|
||||
$autoCreatedBase = $autoBasePath
|
||||
if ($checkList.Count -gt 0) { $checkBasePath = $autoBasePath }
|
||||
} elseif ($checkList.Count -gt 0) {
|
||||
# Базу указали снаружи: класть проверяемую обработку в чужую конфигурацию нельзя, поэтому под
|
||||
# проверку поднимается своя временная база, а сборка идёт на указанной.
|
||||
$checkBase = Join-Path $env:TEMP "epf_check_db_$(Get-Random)"
|
||||
Write-Host "Creating temporary database for the source check..."
|
||||
if ((New-StubBase $checkBase -Embed) -ne 0) {
|
||||
Write-Host "Error: платформа не приняла исходники при подготовке проверки — сборка отменена" -ForegroundColor Red
|
||||
Write-Host " сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$checkBasePath = $checkBase
|
||||
}
|
||||
|
||||
# --- Validate source file ---
|
||||
@@ -414,6 +594,18 @@ if (-not (Test-Path $SourceFile)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Проверка исходников платформой: сломанный .epf до пользователя доезжать не должен ---
|
||||
if ($checkList.Count -gt 0 -and $checkBasePath) {
|
||||
# Проверку ведёт 1cv8, поэтому ibcmd-шные дополнительные аргументы ей не отдаём.
|
||||
$checkExtra = if ($engine -eq "ibcmd") { @() } else { $extraArgs }
|
||||
$found = Invoke-SourceCheck $V8Path $checkBasePath (Get-CheckFlags $checkList $contextList) $sourceDir $checkExtra
|
||||
if ($found) {
|
||||
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
if ($checkBase -and (Test-Path $checkBase)) { Remove-Item -Path $checkBase -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
$outDir = Split-Path $OutputFile -Parent
|
||||
if ($outDir -and -not (Test-Path $outDir)) {
|
||||
@@ -507,4 +699,7 @@ try {
|
||||
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
|
||||
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($checkBase -and (Test-Path $checkBase)) {
|
||||
Remove-Item -Path $checkBase -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# epf-build v1.16 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import glob
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
@@ -67,6 +68,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -122,6 +134,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
@@ -391,6 +411,136 @@ def _redact(text, *secrets):
|
||||
return text
|
||||
|
||||
|
||||
def _sg_find_v8project(start_dir):
|
||||
d = start_dir
|
||||
for _ in range(20):
|
||||
if not d:
|
||||
break
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.isfile(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
# --- Проверка исходников платформой ---
|
||||
# Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML
|
||||
# и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии
|
||||
# обработки. Прямой команды «проверь внешнюю обработку» у платформы нет, но объект КОНФИГУРАЦИИ она
|
||||
# проверяет — поэтому обработка кладётся в конфигурацию временной базы (stub-db-create
|
||||
# -EmbedSourceFile) и спрашивается штатной /CheckConfig.
|
||||
#
|
||||
# Запуск отдельный и только через 1cv8: у ibcmd такой команды нет, а в одной командной строке
|
||||
# DESIGNER выполняет лишь последнюю пакетную команду.
|
||||
def get_check_flags(checks, contexts):
|
||||
flags = []
|
||||
if 'modules' in checks:
|
||||
for c in contexts:
|
||||
flags.append('-' + c)
|
||||
if 'handlers' in checks:
|
||||
flags.append('-HandlersExistence')
|
||||
if 'unreferenced' in checks:
|
||||
flags.append('-UnreferenceProcedures')
|
||||
if 'empty-handlers' in checks:
|
||||
flags.append('-EmptyHandlers')
|
||||
if 'config' in checks:
|
||||
flags += ['-ConfigLogIntegrity', '-IncorrectReferences']
|
||||
return flags
|
||||
|
||||
|
||||
# Платформа называет объект своим именем внутри конфигурации; модели нужен путь к исходнику.
|
||||
# Путь СКЛЕИВАЕТСЯ по конвенции выгрузки, поэтому возвращается только существующий файл:
|
||||
# выдуманный путь хуже отсутствующего — модель пойдёт открывать файл, которого нет.
|
||||
def resolve_source_path(line, source_dir):
|
||||
candidate = None
|
||||
m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u0424\u043e\u0440\u043c\u0430|Form)\.([^.]+)\.', line)
|
||||
if m:
|
||||
candidate = os.path.join(source_dir, m.group(1), 'Forms', m.group(2), 'Ext', 'Form', 'Module.bsl')
|
||||
if candidate is None:
|
||||
m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u041c\u043e\u0434\u0443\u043b\u044c\u041e\u0431\u044a\u0435\u043a\u0442\u0430|ObjectModule)', line)
|
||||
if m:
|
||||
candidate = os.path.join(source_dir, m.group(1), 'Ext', 'ObjectModule.bsl')
|
||||
if candidate is None:
|
||||
m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u041c\u043e\u0434\u0443\u043b\u044c\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430|ManagerModule)', line)
|
||||
if m:
|
||||
candidate = os.path.join(source_dir, m.group(1), 'Ext', 'ManagerModule.bsl')
|
||||
if candidate and os.path.isfile(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
# True, если платформа нашла проблемы — вызывающий не собирает артефакт.
|
||||
def invoke_source_check(exe, base_path, flags, source_dir, extra_args):
|
||||
exe_dir = os.path.dirname(exe)
|
||||
exe_leaf = os.path.basename(exe)
|
||||
if exe_leaf.lower().startswith('ibcmd'):
|
||||
v8 = os.path.join(exe_dir, '1cv8' + os.path.splitext(exe)[1])
|
||||
else:
|
||||
v8 = exe
|
||||
if not os.path.exists(v8):
|
||||
print(f'[note] source check skipped: 1cv8 not found at {v8}')
|
||||
return False
|
||||
d = os.path.join(tempfile.gettempdir(), f'epf_check_{random.randint(0, 999999)}')
|
||||
os.makedirs(d, exist_ok=True)
|
||||
try:
|
||||
out_file = os.path.join(d, 'check_log.txt')
|
||||
a = (['DESIGNER', '/F', f'"{base_path}"', '/CheckConfig'] + flags
|
||||
+ ['/Out', f'"{out_file}"', '/DisableStartupDialogs']
|
||||
+ [quote_if_needed(x) for x in extra_args])
|
||||
print(f'Running: 1cv8.exe {" ".join(a)}')
|
||||
result = run_v8(v8, a)
|
||||
lines = []
|
||||
if os.path.isfile(out_file):
|
||||
try:
|
||||
with io.open(out_file, encoding='utf-8-sig', errors='replace') as fh:
|
||||
raw = fh.read()
|
||||
lines = [l for l in raw.splitlines() if l.strip()]
|
||||
except Exception:
|
||||
lines = []
|
||||
# Платформа отвечает 101 на найденные проблемы; «Ошибок не обнаружено» приходит с кодом 0.
|
||||
if result.returncode == 0:
|
||||
return False
|
||||
print('Error: \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u043d\u0430\u0448\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0432 \u0438\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0430\u0445 \u2014 \u0441\u0431\u043e\u0440\u043a\u0430 \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430')
|
||||
# Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя.
|
||||
if not lines:
|
||||
print(f' платформа вернула код {result.returncode} без сообщений')
|
||||
for l in lines:
|
||||
print(f' {l.rstrip()}')
|
||||
src_path = resolve_source_path(l, source_dir)
|
||||
if src_path:
|
||||
print(f' -> {src_path}')
|
||||
return True
|
||||
finally:
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
|
||||
|
||||
# Проверять ли исходники: -Checks off сильнее настройки проекта externalCheck.
|
||||
def get_source_check_list(checks):
|
||||
known = ['modules', 'handlers', 'unreferenced', 'empty-handlers', 'config']
|
||||
if checks:
|
||||
lst = [c.strip().lower() for c in checks.split(',') if c.strip()]
|
||||
if 'off' in lst:
|
||||
return []
|
||||
for c in lst:
|
||||
if c not in known:
|
||||
print(f'Error: unknown check \'{c}\' (expected: {", ".join(known)} or off)')
|
||||
sys.exit(1)
|
||||
return lst
|
||||
pf = _sg_find_v8project(os.getcwd())
|
||||
if pf:
|
||||
try:
|
||||
with open(pf, encoding='utf-8-sig') as fh:
|
||||
proj = json.load(fh)
|
||||
if 'externalCheck' in proj and not proj['externalCheck']:
|
||||
return []
|
||||
except Exception:
|
||||
pass
|
||||
return ['modules', 'handlers']
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
@@ -406,6 +556,11 @@ def main():
|
||||
parser.add_argument("-Password", default="", help="1C user password")
|
||||
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
|
||||
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
|
||||
# Что проверить в исходниках перед сборкой: modules (синтаксис в контекстах), handlers,
|
||||
# unreferenced, empty-handlers, config; off — не проверять. По умолчанию modules,handlers.
|
||||
parser.add_argument("-Checks", default="")
|
||||
# Контексты синтаксической проверки. По умолчанию ThinClient,Server.
|
||||
parser.add_argument("-Context", default="")
|
||||
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
|
||||
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
|
||||
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
|
||||
@@ -439,34 +594,80 @@ def main():
|
||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
auto_created_base = None
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
source_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
||||
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
||||
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
||||
print("No database specified. Creating temporary stub database...")
|
||||
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
|
||||
"-TempBasePath", auto_base_path]
|
||||
# --- Что проверяем в исходниках перед сборкой ---
|
||||
check_list = get_source_check_list(args.Checks)
|
||||
context_list = [c.strip() for c in args.Context.split(',') if c.strip()] if args.Context else []
|
||||
if not context_list:
|
||||
context_list = ['ThinClient', 'Server']
|
||||
elif check_list and 'modules' not in check_list:
|
||||
print('Error: -Context задан, но в -Checks нет modules — контексты относятся только к ней')
|
||||
sys.exit(1)
|
||||
source_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
||||
|
||||
def new_stub_base(base_path, embed):
|
||||
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
|
||||
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
|
||||
# explicit ones are forwarded: the stub reads .v8-project.json itself.
|
||||
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
|
||||
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
|
||||
"-TempBasePath", base_path]
|
||||
if embed:
|
||||
stub_cmd += ["-EmbedSourceFile", args.SourceFile]
|
||||
if v8_extra:
|
||||
stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra)
|
||||
if ibcmd_extra:
|
||||
stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra)
|
||||
result = subprocess.run(stub_cmd, capture_output=False)
|
||||
if result.returncode != 0:
|
||||
print("Error: failed to create stub database")
|
||||
return subprocess.run(stub_cmd, capture_output=False).returncode
|
||||
|
||||
# --- Auto-create stub database if no connection specified ---
|
||||
auto_created_base = None
|
||||
check_base = None
|
||||
check_base_path = None
|
||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
|
||||
print("No database specified. Creating temporary stub database...")
|
||||
if new_stub_base(auto_base_path, bool(check_list)) != 0:
|
||||
# С внедрённой обработкой база падает прежде всего из-за самих исходников
|
||||
# (пример: DefaultForm на несуществующую форму) — говорить про базу значит увести не туда.
|
||||
if check_list:
|
||||
print('Error: платформа не приняла исходники при подготовке проверки — сборка отменена')
|
||||
print(' сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт')
|
||||
else:
|
||||
print("Error: failed to create stub database")
|
||||
sys.exit(1)
|
||||
args.InfoBasePath = auto_base_path
|
||||
auto_created_base = auto_base_path
|
||||
if check_list:
|
||||
check_base_path = auto_base_path
|
||||
elif check_list:
|
||||
# Базу указали снаружи: класть проверяемую обработку в чужую конфигурацию нельзя, поэтому под
|
||||
# проверку поднимается своя временная база, а сборка идёт на указанной.
|
||||
check_base = os.path.join(tempfile.gettempdir(), f"epf_check_db_{random.randint(0, 999999)}")
|
||||
print("Creating temporary database for the source check...")
|
||||
if new_stub_base(check_base, True) != 0:
|
||||
print('Error: платформа не приняла исходники при подготовке проверки — сборка отменена')
|
||||
print(' сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт')
|
||||
sys.exit(1)
|
||||
check_base_path = check_base
|
||||
|
||||
# --- Validate source file ---
|
||||
if not os.path.isfile(args.SourceFile):
|
||||
print(f"Error: source file not found: {args.SourceFile}")
|
||||
sys.exit(1)
|
||||
|
||||
# --- Проверка исходников платформой: сломанный .epf до пользователя доезжать не должен ---
|
||||
if check_list and check_base_path:
|
||||
# Проверку ведёт 1cv8, поэтому ibcmd-шные дополнительные аргументы ей не отдаём.
|
||||
check_extra = [] if engine == "ibcmd" else extra_args
|
||||
found = invoke_source_check(v8path, check_base_path,
|
||||
get_check_flags(check_list, context_list), source_dir, check_extra)
|
||||
if found:
|
||||
if auto_created_base and os.path.exists(auto_created_base):
|
||||
shutil.rmtree(auto_created_base, ignore_errors=True)
|
||||
if check_base and os.path.exists(check_base):
|
||||
shutil.rmtree(check_base, ignore_errors=True)
|
||||
sys.exit(1)
|
||||
|
||||
# --- Ensure output directory exists ---
|
||||
out_dir = os.path.dirname(args.OutputFile)
|
||||
if out_dir and not os.path.exists(out_dir):
|
||||
@@ -560,6 +761,8 @@ def main():
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
if auto_created_base and os.path.exists(auto_created_base):
|
||||
shutil.rmtree(auto_created_base, ignore_errors=True)
|
||||
if check_base and os.path.exists(check_base):
|
||||
shutil.rmtree(check_base, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||
# stub-db-create v1.10 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
@@ -9,6 +9,10 @@ param(
|
||||
|
||||
[string]$TempBasePath,
|
||||
|
||||
# XML проверяемой обработки/отчёта: объект кладётся в конфигурацию-заглушку, чтобы платформа
|
||||
# смогла проверить его штатными проверками. Без параметра стаб работает как раньше.
|
||||
[string]$EmbedSourceFile,
|
||||
|
||||
[string[]]$AdditionalV8Arguments = @(),
|
||||
|
||||
[string[]]$AdditionalIbcmdArguments = @()
|
||||
@@ -49,6 +53,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -99,6 +114,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
@@ -331,6 +354,9 @@ foreach ($f in $xmlFiles) {
|
||||
}
|
||||
|
||||
$hasRefTypes = $typeMap.Count -gt 0
|
||||
# Конфигурация нужна и тогда, когда ссылочных типов нет: в неё кладётся сам объект.
|
||||
$embedRequested = -not [string]::IsNullOrWhiteSpace($EmbedSourceFile)
|
||||
$needCfg = $hasRefTypes -or $embedRequested
|
||||
|
||||
# --- 2. Determine TempBasePath ---
|
||||
if (-not $TempBasePath) {
|
||||
@@ -351,13 +377,110 @@ if ($needsRegistrator) {
|
||||
$typeMap["Document"]["ЗаглушкаРегистратора"] = $true
|
||||
}
|
||||
|
||||
# --- Внедрение проверяемого объекта в конфигурацию-заглушку ---
|
||||
# Платформа не умеет проверять внешнюю обработку: /LoadExternalDataProcessorOrReportFromFiles
|
||||
# только упаковывает XML и модули не компилирует. Зато она проверяет объект КОНФИГУРАЦИИ, а
|
||||
# внешняя обработка отличается от него немногим (замер 8.3.24): корневым тегом, именем
|
||||
# порождаемого объектного типа и отсутствием типа менеджера. Правим ровно эти точки и переносим
|
||||
# остальное как есть — под проверку попадает всё, что написал автор, включая реквизиты, формы и
|
||||
# макеты, а формат может расти без правок здесь.
|
||||
#
|
||||
# Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl
|
||||
# такой же текст был бы кодом, и трогать его нельзя.
|
||||
function Add-SourceObjectToConfig {
|
||||
param([string]$SourceXml, [string]$CfgDir)
|
||||
|
||||
# Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ
|
||||
# обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение
|
||||
# XDTO при чтении файла» на исправном исходнике — поэтому у копии все GUID свои, но согласованные
|
||||
# между её файлами (ссылки внутри объекта идут по идентификатору).
|
||||
$guidMap = @{}
|
||||
$reGuid = [regex]'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
|
||||
$reissue = {
|
||||
param($m)
|
||||
$k = $m.Value.ToLower()
|
||||
if (-not $guidMap.ContainsKey($k)) { $guidMap[$k] = [guid]::NewGuid().ToString() }
|
||||
$guidMap[$k]
|
||||
}
|
||||
|
||||
$text = [IO.File]::ReadAllText($SourceXml, [Text.Encoding]::UTF8)
|
||||
if ($text -match '<ExternalDataProcessor[\s>]') {
|
||||
$extTag = 'ExternalDataProcessor'; $cfgTag = 'DataProcessor'; $folder = 'DataProcessors'
|
||||
} elseif ($text -match '<ExternalReport[\s>]') {
|
||||
$extTag = 'ExternalReport'; $cfgTag = 'Report'; $folder = 'Reports'
|
||||
} else {
|
||||
return $null
|
||||
}
|
||||
|
||||
$name = if ($text -match '<Name>([^<]+)</Name>') { $Matches[1] } else { [IO.Path]::GetFileNameWithoutExtension($SourceXml) }
|
||||
|
||||
$conv = $reGuid.Replace($text, $reissue)
|
||||
$conv = $conv.Replace("<$extTag ", "<$cfgTag ").Replace("<$extTag>", "<$cfgTag>").Replace("</$extTag>", "</$cfgTag>")
|
||||
$conv = $conv.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.")
|
||||
|
||||
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
|
||||
# без него платформа отвечает «отсутствует один или более типов объекта».
|
||||
$mgr = "`t`t`t<xr:GeneratedType name=`"${cfgTag}Manager.$name`" category=`"Manager`">`r`n" +
|
||||
"`t`t`t`t<xr:TypeId>$([guid]::NewGuid().ToString())</xr:TypeId>`r`n" +
|
||||
"`t`t`t`t<xr:ValueId>$([guid]::NewGuid().ToString())</xr:ValueId>`r`n" +
|
||||
"`t`t`t</xr:GeneratedType>`r`n"
|
||||
if ($conv -match '</InternalInfo>') {
|
||||
$conv = [regex]::Replace($conv, '(\s*)</InternalInfo>', ("`r`n" + $mgr + "`t`t</InternalInfo>"), 1)
|
||||
} else {
|
||||
$objType = "`t`t`t<xr:GeneratedType name=`"${cfgTag}Object.$name`" category=`"Object`">`r`n" +
|
||||
"`t`t`t`t<xr:TypeId>$([guid]::NewGuid().ToString())</xr:TypeId>`r`n" +
|
||||
"`t`t`t`t<xr:ValueId>$([guid]::NewGuid().ToString())</xr:ValueId>`r`n" +
|
||||
"`t`t`t</xr:GeneratedType>`r`n"
|
||||
$conv = [regex]::Replace($conv, "(<$cfgTag[^>]*>)", ("`$1`r`n`t`t<InternalInfo>`r`n" + $objType + $mgr + "`t`t</InternalInfo>"), 1)
|
||||
}
|
||||
|
||||
$objDir = Join-Path $CfgDir $folder
|
||||
New-Item -ItemType Directory -Path $objDir -Force | Out-Null
|
||||
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||
[IO.File]::WriteAllText((Join-Path $objDir "$name.xml"), $conv, $encBom)
|
||||
|
||||
# Содержимое объекта — как есть; в XML та же подстановка типа, .bsl копируются байт в байт.
|
||||
$srcContent = Join-Path (Split-Path $SourceXml -Parent) $name
|
||||
if (Test-Path $srcContent) {
|
||||
# Длину префикса берём у РАЗРЕШЁННОГО пути, а не у переданной строки: путь может
|
||||
# прийти с коротким именем (C:\Users\NSHIRO~1\…), а Get-ChildItem отдаёт полное — тогда
|
||||
# отрезание по длине исходной строки оставляет в относительном пути чужие символы.
|
||||
$srcRoot = (Get-Item -LiteralPath $srcContent).FullName.TrimEnd('\', '/')
|
||||
$dstContent = Join-Path $objDir $name
|
||||
foreach ($f in (Get-ChildItem -LiteralPath $srcRoot -Recurse -File)) {
|
||||
$rel = $f.FullName.Substring($srcRoot.Length).TrimStart('\', '/')
|
||||
$dst = Join-Path $dstContent $rel
|
||||
New-Item -ItemType Directory -Path (Split-Path $dst -Parent) -Force | Out-Null
|
||||
if ($f.Extension -ieq '.xml') {
|
||||
$t = [IO.File]::ReadAllText($f.FullName, [Text.Encoding]::UTF8)
|
||||
$t = $reGuid.Replace($t, $reissue)
|
||||
$t = $t.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.")
|
||||
[IO.File]::WriteAllText($dst, $t, $encBom)
|
||||
} else {
|
||||
Copy-Item -Path $f.FullName -Destination $dst -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return @{ Tag = $cfgTag; Name = $name }
|
||||
}
|
||||
|
||||
# --- 4. Generate configuration XML ---
|
||||
|
||||
if ($hasRefTypes) {
|
||||
if ($needCfg) {
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
$cfgDir = Join-Path $TempBasePath "cfg"
|
||||
New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null
|
||||
|
||||
$embedded = $null
|
||||
if ($embedRequested) {
|
||||
$embedded = Add-SourceObjectToConfig $EmbedSourceFile $cfgDir
|
||||
if (-not $embedded) {
|
||||
Write-Host "Error: $EmbedSourceFile is neither ExternalDataProcessor nor ExternalReport" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы
|
||||
# одностороннее — она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+
|
||||
# заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе
|
||||
@@ -547,6 +670,7 @@ if ($hasRefTypes) {
|
||||
$childXml += "`r`n`t`t`t<$tag>$name</$tag>"
|
||||
}
|
||||
}
|
||||
if ($embedded) { $childXml += "`r`n`t`t`t<$($embedded.Tag)>$($embedded.Name)</$($embedded.Tag)>" }
|
||||
|
||||
$cfgXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -1529,7 +1653,7 @@ if ($stubEngine -eq "ibcmd") {
|
||||
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
||||
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
|
||||
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
||||
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
||||
if ($needCfg) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
||||
$ibArgs += "--data=$ibData"
|
||||
$ibArgs += $extraArgs
|
||||
$__ib = Invoke-PlatformProcess $V8Path $ibArgs
|
||||
@@ -1541,7 +1665,7 @@ if ($stubEngine -eq "ibcmd") {
|
||||
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
||||
exit 1
|
||||
}
|
||||
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
if ($needCfg) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
Write-Host "[OK] Stub database created: $TempBasePath"
|
||||
Write-Host $TempBasePath
|
||||
exit 0
|
||||
@@ -1557,8 +1681,8 @@ if ($proc.ExitCode -ne 0) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 6. Load config and update DB if ref types exist ---
|
||||
if ($hasRefTypes) {
|
||||
# --- 6. Load config and update DB if there is one ---
|
||||
if ($needCfg) {
|
||||
$cfgDir = Join-Path $TempBasePath "cfg"
|
||||
# LoadConfigFromFiles
|
||||
Write-Host "Loading configuration from files..."
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||
# stub-db-create v1.10 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import random
|
||||
import re
|
||||
import subprocess
|
||||
@@ -64,11 +66,31 @@ def run_v8(v8path, arguments):
|
||||
The arguments carry their own quotes inside the value (File="C:\\a b") — that is where
|
||||
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
|
||||
escape those quotes, so there the command line is handed over ready-made.
|
||||
|
||||
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
|
||||
частью значения: путь с пробелом платформа не находит («Неопределена информационная
|
||||
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
|
||||
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
|
||||
File="…") не задеты: у них кавычки внутри токена, а не по краям.
|
||||
"""
|
||||
if os.name == "nt":
|
||||
cmd = '"' + v8path + '" ' + " ".join(arguments)
|
||||
else:
|
||||
cmd = [v8path] + arguments
|
||||
def strip_framing_quotes(a):
|
||||
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
|
||||
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
|
||||
# токеном даёт «Неопределена информационная база», а склеенный
|
||||
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
|
||||
# без кавычек обе формы работают.
|
||||
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
|
||||
return a[1:-1] # "значение" отдельным токеном
|
||||
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
|
||||
i = a.index('"')
|
||||
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
|
||||
return a # File="…" не трогаем: там кавычки —
|
||||
# часть синтаксиса строки соединения,
|
||||
# и с ними на POSIX всё работает
|
||||
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
|
||||
r = subprocess.run(cmd, input=b"", capture_output=True)
|
||||
r.stdout = decode_platform_bytes(r.stdout)
|
||||
r.stderr = decode_platform_bytes(r.stderr)
|
||||
@@ -114,6 +136,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -167,15 +200,21 @@ def assert_extra_args(extra, engine, hints):
|
||||
print(
|
||||
f"Error: '{tok}' is a positional token — pass values as --key=value "
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
print(
|
||||
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -243,14 +282,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
|
||||
print(
|
||||
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
|
||||
"(use -AdditionalIbcmdArguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd" and ibcmd_extra:
|
||||
print(
|
||||
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
|
||||
"(use -AdditionalV8Arguments)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine == "ibcmd":
|
||||
@@ -1083,6 +1120,94 @@ def write_bom(path, content):
|
||||
f.write(content)
|
||||
|
||||
|
||||
# --- Внедрение проверяемого объекта в конфигурацию-заглушку ---
|
||||
# Платформа не умеет проверять внешнюю обработку: /LoadExternalDataProcessorOrReportFromFiles
|
||||
# только упаковывает XML и модули не компилирует. Зато она проверяет объект КОНФИГУРАЦИИ, а
|
||||
# внешняя обработка отличается от него немногим (замер 8.3.24): корневым тегом, именем
|
||||
# порождаемого объектного типа и отсутствием типа менеджера. Правим ровно эти точки и переносим
|
||||
# остальное как есть — под проверку попадает всё, что написал автор, включая реквизиты, формы и
|
||||
# макеты, а формат может расти без правок здесь.
|
||||
#
|
||||
# Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl
|
||||
# такой же текст был бы кодом, и трогать его нельзя.
|
||||
GUID_RE = re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}')
|
||||
|
||||
|
||||
def add_source_object_to_config(source_xml, cfg_dir):
|
||||
# Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ
|
||||
# обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение
|
||||
# XDTO при чтении файла» на исправном исходнике — поэтому у копии все GUID свои, но согласованные
|
||||
# между её файлами (ссылки внутри объекта идут по идентификатору).
|
||||
guid_map = {}
|
||||
|
||||
def reissue(text):
|
||||
def sub(m):
|
||||
k = m.group(0).lower()
|
||||
if k not in guid_map:
|
||||
guid_map[k] = new_uuid()
|
||||
return guid_map[k]
|
||||
return GUID_RE.sub(sub, text)
|
||||
|
||||
with io.open(source_xml, encoding='utf-8-sig') as fh:
|
||||
text = fh.read()
|
||||
if re.search(r'<ExternalDataProcessor[\s>]', text):
|
||||
ext_tag, cfg_tag, folder = 'ExternalDataProcessor', 'DataProcessor', 'DataProcessors'
|
||||
elif re.search(r'<ExternalReport[\s>]', text):
|
||||
ext_tag, cfg_tag, folder = 'ExternalReport', 'Report', 'Reports'
|
||||
else:
|
||||
return None
|
||||
|
||||
m = re.search(r'<Name>([^<]+)</Name>', text)
|
||||
name = m.group(1) if m else os.path.splitext(os.path.basename(source_xml))[0]
|
||||
|
||||
conv = reissue(text)
|
||||
conv = conv.replace('<%s ' % ext_tag, '<%s ' % cfg_tag).replace('<%s>' % ext_tag, '<%s>' % cfg_tag)
|
||||
conv = conv.replace('</%s>' % ext_tag, '</%s>' % cfg_tag)
|
||||
conv = conv.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag)
|
||||
|
||||
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
|
||||
# без него платформа отвечает «отсутствует один или более типов объекта».
|
||||
mgr = ('\t\t\t<xr:GeneratedType name="%sManager.%s" category="Manager">\r\n' % (cfg_tag, name) +
|
||||
'\t\t\t\t<xr:TypeId>%s</xr:TypeId>\r\n' % new_uuid() +
|
||||
'\t\t\t\t<xr:ValueId>%s</xr:ValueId>\r\n' % new_uuid() +
|
||||
'\t\t\t</xr:GeneratedType>\r\n')
|
||||
if '</InternalInfo>' in conv:
|
||||
conv = re.sub(r'\s*</InternalInfo>', lambda _m: '\r\n' + mgr + '\t\t</InternalInfo>', conv, count=1)
|
||||
else:
|
||||
obj_type = ('\t\t\t<xr:GeneratedType name="%sObject.%s" category="Object">\r\n' % (cfg_tag, name) +
|
||||
'\t\t\t\t<xr:TypeId>%s</xr:TypeId>\r\n' % new_uuid() +
|
||||
'\t\t\t\t<xr:ValueId>%s</xr:ValueId>\r\n' % new_uuid() +
|
||||
'\t\t\t</xr:GeneratedType>\r\n')
|
||||
conv = re.sub('(<%s[^>]*>)' % cfg_tag,
|
||||
lambda m2: m2.group(1) + '\r\n\t\t<InternalInfo>\r\n' + obj_type + mgr + '\t\t</InternalInfo>',
|
||||
conv, count=1)
|
||||
|
||||
obj_dir = os.path.join(cfg_dir, folder)
|
||||
os.makedirs(obj_dir, exist_ok=True)
|
||||
write_bom(os.path.join(obj_dir, '%s.xml' % name), conv)
|
||||
|
||||
# Содержимое объекта — как есть; в XML та же подстановка типа, .bsl копируются байт в байт.
|
||||
src_content = os.path.join(os.path.dirname(source_xml), name)
|
||||
if os.path.isdir(src_content):
|
||||
dst_content = os.path.join(obj_dir, name)
|
||||
for root, _dirs, files in os.walk(src_content):
|
||||
for fname in files:
|
||||
full = os.path.join(root, fname)
|
||||
rel = os.path.relpath(full, src_content)
|
||||
dst = os.path.join(dst_content, rel)
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
if os.path.splitext(fname)[1].lower() == '.xml':
|
||||
with io.open(full, encoding='utf-8-sig') as fh:
|
||||
t = fh.read()
|
||||
t = reissue(t)
|
||||
t = t.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag)
|
||||
write_bom(dst, t)
|
||||
else:
|
||||
shutil.copyfile(full, dst)
|
||||
|
||||
return {'tag': cfg_tag, 'name': name}
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
@@ -1091,6 +1216,9 @@ def main():
|
||||
parser.add_argument('-SourceDir', required=True)
|
||||
parser.add_argument('-V8Path', required=True)
|
||||
parser.add_argument('-TempBasePath', default='')
|
||||
# XML проверяемой обработки/отчёта: объект кладётся в конфигурацию-заглушку, чтобы платформа
|
||||
# смогла проверить его штатными проверками. Без параметра стаб работает как раньше.
|
||||
parser.add_argument('-EmbedSourceFile', default='')
|
||||
parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[],
|
||||
help='Extra 1cv8 arguments, e.g. /UseHwLicenses+')
|
||||
parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[],
|
||||
@@ -1102,10 +1230,13 @@ def main():
|
||||
args.SourceDir = clean_path(args.SourceDir, "-SourceDir")
|
||||
args.V8Path = clean_path(args.V8Path, "-V8Path")
|
||||
args.TempBasePath = clean_path(args.TempBasePath, "-TempBasePath")
|
||||
args.EmbedSourceFile = clean_path(args.EmbedSourceFile, "-EmbedSourceFile")
|
||||
|
||||
type_map = scan_ref_types(args.SourceDir)
|
||||
register_columns = scan_register_columns(args.SourceDir)
|
||||
has_ref_types = len(type_map) > 0
|
||||
embed_requested = bool(args.EmbedSourceFile and args.EmbedSourceFile.strip())
|
||||
need_cfg = has_ref_types or embed_requested
|
||||
stub_format_version = detect_stub_format_version(args.SourceDir)
|
||||
stub_compat = stub_compatibility_mode(stub_format_version)
|
||||
ns_decl = f'{NS} version="{stub_format_version}"'
|
||||
@@ -1118,10 +1249,18 @@ def main():
|
||||
if needs_registrator:
|
||||
type_map.setdefault('Document', {})['\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430'] = True # ЗаглушкаРегистратора
|
||||
|
||||
if has_ref_types:
|
||||
if need_cfg:
|
||||
cfg_dir = os.path.join(temp_base, 'cfg')
|
||||
os.makedirs(cfg_dir, exist_ok=True)
|
||||
|
||||
embedded = None
|
||||
if embed_requested:
|
||||
embedded = add_source_object_to_config(args.EmbedSourceFile, cfg_dir)
|
||||
if not embedded:
|
||||
print('Error: %s is neither ExternalDataProcessor nor ExternalReport' % args.EmbedSourceFile,
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Configuration.xml
|
||||
uuid_cfg = new_uuid()
|
||||
uuid_lang = new_uuid()
|
||||
@@ -1138,6 +1277,8 @@ def main():
|
||||
tag = META_INFO[meta_type][0]
|
||||
for name in names:
|
||||
child_xml += f'\n\t\t\t<{tag}>{name}</{tag}>'
|
||||
if embedded:
|
||||
child_xml += '\n\t\t\t<%s>%s</%s>' % (embedded['tag'], embedded['name'], embedded['tag'])
|
||||
|
||||
cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MetaDataObject {ns_decl}>
|
||||
@@ -1368,7 +1509,7 @@ def main():
|
||||
print(f'Creating infobase (ibcmd): {temp_base}')
|
||||
ib_data = tempfile.mkdtemp(prefix="stub_data_")
|
||||
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
|
||||
if has_ref_types:
|
||||
if need_cfg:
|
||||
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
||||
ib_args.append(f'--data={ib_data}')
|
||||
ib_args.extend(extra_args)
|
||||
@@ -1381,7 +1522,7 @@ def main():
|
||||
print(result.stderr, file=sys.stderr)
|
||||
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if has_ref_types:
|
||||
if need_cfg:
|
||||
import shutil
|
||||
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
|
||||
print(f'[OK] Stub database created: {temp_base}')
|
||||
@@ -1397,13 +1538,24 @@ def main():
|
||||
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if has_ref_types:
|
||||
if need_cfg:
|
||||
cfg_dir = os.path.join(temp_base, 'cfg')
|
||||
# LoadConfigFromFiles
|
||||
print('Loading configuration from files...')
|
||||
load_log = os.path.join(tempfile.gettempdir(), 'stub_load_log.txt')
|
||||
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/LoadConfigFromFiles', f'"{cfg_dir}"',
|
||||
'/Out', f'"{load_log}"',
|
||||
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
|
||||
if result.returncode != 0:
|
||||
# Причина отказа живёт только в /Out: в консоль пакетный 1cv8 не пишет ничего.
|
||||
if os.path.isfile(load_log):
|
||||
try:
|
||||
with io.open(load_log, encoding='utf-8-sig', errors='replace') as fh:
|
||||
text = fh.read().strip()
|
||||
if text:
|
||||
print(text)
|
||||
except Exception:
|
||||
pass
|
||||
print_platform_output(result)
|
||||
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -98,6 +98,17 @@ $script:V8OwnedKeys = @(
|
||||
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
|
||||
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
|
||||
)
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
$script:V8BatchKeys = @(
|
||||
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
|
||||
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
|
||||
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
|
||||
)
|
||||
|
||||
$script:IbcmdOwnedKeys = @(
|
||||
'--db-path', '--data', '--out', '--file', '--load', '--restore',
|
||||
'--import', '--export', '--apply', '--force', '--create-database',
|
||||
@@ -148,6 +159,14 @@ function Assert-ExtraArgs {
|
||||
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
if ($Engine -ne 'ibcmd') {
|
||||
foreach ($b in $script:V8BatchKeys) {
|
||||
if (Test-ArgKeyMatch $tok $b) {
|
||||
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($k in $owned) {
|
||||
if (Test-ArgKeyMatch $tok $k) {
|
||||
$hint = ''
|
||||
|
||||
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
|
||||
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
|
||||
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
|
||||
]
|
||||
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
|
||||
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
|
||||
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
|
||||
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
|
||||
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
|
||||
V8_BATCH_KEYS = [
|
||||
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
|
||||
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
|
||||
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
|
||||
]
|
||||
|
||||
IBCMD_OWNED_KEYS = [
|
||||
"--db-path", "--data", "--out", "--file", "--load", "--restore",
|
||||
"--import", "--export", "--apply", "--force", "--create-database",
|
||||
@@ -122,6 +133,14 @@ def assert_extra_args(extra, engine, hints):
|
||||
f"({param} cannot extend the ibcmd command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
if engine != "ibcmd":
|
||||
for b in V8_BATCH_KEYS:
|
||||
if arg_key_match(tok, b):
|
||||
print(
|
||||
f"Error: {b} is a batch command; passed via {param} it would replace "
|
||||
f"the skill's own operation (a command line runs only its last batch command)",
|
||||
)
|
||||
sys.exit(1)
|
||||
for k in owned:
|
||||
if arg_key_match(tok, k):
|
||||
hint = f" (use {hints[k]})" if hints and k in hints else ""
|
||||
|
||||
@@ -12,6 +12,8 @@ allowed-tools:
|
||||
|
||||
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
|
||||
|
||||
Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|
||||
@@ -25,7 +25,8 @@ allowed-tools:
|
||||
|
||||
## Параметры подключения (опционально)
|
||||
|
||||
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
|
||||
Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается
|
||||
под проверку исходников, если она не отключена.
|
||||
|
||||
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
|
||||
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
||||
@@ -57,11 +58,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
|
||||
| `-Password <пароль>` | нет | Пароль |
|
||||
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
|
||||
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
|
||||
| `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` |
|
||||
| `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` |
|
||||
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
|
||||
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
|
||||
|
||||
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
|
||||
|
||||
## Проверка перед сборкой
|
||||
|
||||
Перед сборкой исходники проверяет платформа — синтаксис модулей и наличие обработчиков форм.
|
||||
Если она нашла проблемы, сборка отменяется и файл не создаётся; в выводе — сообщение
|
||||
платформы со строкой и колонкой и путь к файлу исходника. Отключается `-Checks off`
|
||||
или ключом `"externalCheck": false` в `.v8-project.json`.
|
||||
|
||||
Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает.
|
||||
|
||||
## Примеры
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -14,6 +14,8 @@ allowed-tools:
|
||||
|
||||
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
|
||||
|
||||
Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Обяз. | Умолч. | Описание |
|
||||
|
||||
@@ -62,10 +62,15 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -Obje
|
||||
| DocumentJournal, FilterCriterion | List, Custom |
|
||||
| Enum | List, Choice, Custom |
|
||||
| SettingsStorage | Save, Load, Custom |
|
||||
| Table (таблица внешнего источника) | `tableDataType=ObjectData` — Object, List, Choice, Custom; `NonobjectData` — Record, List, Choice, Custom |
|
||||
|
||||
Недопустимое сочетание отклоняется со списком доступных для этого типа. У константы собственных
|
||||
форм нет — для неё используется общая форма (`CommonForm`).
|
||||
|
||||
Таблица внешнего источника адресуется файлом таблицы:
|
||||
`ExternalDataSources/<Источник>/Tables/<Таблица>.xml`. Ссылки в такой форме трёхчастные;
|
||||
без `-Purpose` берётся форма объекта, а у таблицы с составным ключом — форма записи.
|
||||
|
||||
## Примеры
|
||||
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-add v1.28 — Add managed form to 1C config object
|
||||
# form-add v1.29 — Add managed form to 1C config object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -370,6 +370,15 @@ $formKinds = @{
|
||||
"Load" = @{ MainAttr = $null; AttrName = $null; Slot = "DefaultLoadForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
# Таблица внешнего источника — единственный вид, чьё имя в ссылках трёхчастное
|
||||
# (Источник.Таблица): подставляется {2}, а не {1}.
|
||||
"Table" = @{
|
||||
"Object" = @{ MainAttr = "ExternalDataSourceTableObject.{2}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
|
||||
"Record" = @{ MainAttr = "ExternalDataSourceTableRecordManager.{2}"; AttrName = "Запись"; Slot = "DefaultRecordForm"; SavedData = $true }
|
||||
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
|
||||
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
|
||||
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
|
||||
}
|
||||
}
|
||||
|
||||
# Виды, у которых свойство DefaultForm есть, но собственных форм не бывает — отказ с причиной,
|
||||
@@ -422,6 +431,25 @@ if (-not $objectName) {
|
||||
Write-Host ""
|
||||
Write-Host "=== form-add ==="
|
||||
Write-Host ""
|
||||
# Ссылка на объект и имя для типов формы. У всех видов это "Вид.Имя", и только
|
||||
# у таблицы внешнего источника — "ExternalDataSource.<Источник>.Table.<Таблица>", а в именах
|
||||
# типов — "<Источник>.<Таблица>". Имя источника в самом файле таблицы не хранится —
|
||||
# единственное место, где навык смотрит на путь: ExternalDataSources/<Источник>/Tables/<Таблица>.xml
|
||||
$objectQualifiedName = $objectName
|
||||
$objectRef = "$objectType.$objectName"
|
||||
if ($objectType -eq "Table") {
|
||||
$tablesDir = Split-Path -Parent $objectXmlFull.Path
|
||||
$edsSource = Split-Path -Leaf (Split-Path -Parent $tablesDir)
|
||||
if (-not $edsSource -or (Split-Path -Leaf $tablesDir) -ne "Tables") {
|
||||
Write-Error "Таблица внешнего источника ожидается по пути ExternalDataSources/<Источник>/Tables/<Таблица>.xml, а не '$($objectXmlFull.Path)'"
|
||||
exit 1
|
||||
}
|
||||
$objectQualifiedName = "$edsSource.$objectName"
|
||||
$objectRef = "ExternalDataSource.$edsSource.Table.$objectName"
|
||||
$tdtNode = $xmlDoc.SelectSingleNode("//md:Table/md:Properties/md:TableDataType", $nsMgr)
|
||||
$tableDataType = if ($tdtNode) { $tdtNode.InnerText.Trim() } else { "ObjectData" }
|
||||
}
|
||||
|
||||
Write-Host "Object: $objectType.$objectName"
|
||||
|
||||
# --- Фаза 2: Валидация Purpose ---
|
||||
@@ -457,8 +485,14 @@ if ($Purpose) {
|
||||
}
|
||||
}
|
||||
if (-not $Purpose) {
|
||||
foreach ($p in $kindPurposes.Keys) {
|
||||
if ($kindPurposes[$p].Primary) { $Purpose = $p; break }
|
||||
if ($objectType -eq "Table" -and $tableDataType -eq "NonobjectData") {
|
||||
# Пометка Primary в таблице видов одна на вид, а у таблицы с составным ключом
|
||||
# формы объекта не бывает — основной становится форма записи.
|
||||
$Purpose = "Record"
|
||||
} else {
|
||||
foreach ($p in $kindPurposes.Keys) {
|
||||
if ($kindPurposes[$p].Primary) { $Purpose = $p; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
$purposeKey = $null
|
||||
@@ -472,6 +506,20 @@ if (-not $purposeKey) {
|
||||
$Purpose = $purposeKey
|
||||
$purposeRule = $kindPurposes[$Purpose]
|
||||
|
||||
# У таблицы внешнего источника набор назначений зависит от вида данных (замерено на
|
||||
# 8.3.24.1691): ObjectData — Object/List/Choice, NonobjectData — Record/List/Choice. Неверная пара
|
||||
# не отвергается схемой формы, а валит загрузку всей конфигурации «Исключением XDTO» без причины.
|
||||
if ($objectType -eq "Table") {
|
||||
if ($tableDataType -eq "NonobjectData" -and $Purpose -eq "Object") {
|
||||
Write-Error "Таблица '$objectName' с составным ключом (TableDataType=NonobjectData): формы объекта у неё нет — используйте -Purpose Record."
|
||||
exit 1
|
||||
}
|
||||
if ($tableDataType -ne "NonobjectData" -and $Purpose -eq "Record") {
|
||||
Write-Error "Таблица '$objectName' с ключом из одного поля (TableDataType=ObjectData): формы записи у неё нет — используйте -Purpose Object."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Гард от повторения дефекта: запись таблицы обязана быть заполненной. Пустой MainAttr — это
|
||||
# произвольная форма (законное состояние), а вот наполовину заполненная запись означала бы, что
|
||||
# таблицу правили невнимательно, и в XML уйдёт мусор вроде `cfg:.Журнал`.
|
||||
@@ -561,13 +609,13 @@ $formXmlPath = Join-Path $formExtDir "Form.xml"
|
||||
# реквизита брался из отдельной карты, и отсутствие вида в ней давало `cfg:.Имя` — молча.
|
||||
$attributesBlock = ""
|
||||
if ($purposeRule.MainAttr) {
|
||||
$mainAttrType = $purposeRule.MainAttr -f $objectType, $objectName
|
||||
$mainAttrType = $purposeRule.MainAttr -f $objectType, $objectName, $objectQualifiedName
|
||||
$mainAttrName = $purposeRule.AttrName
|
||||
|
||||
# Динамический список несёт MainTable, остальные типы — SavedData по записи таблицы.
|
||||
$tailLines = ""
|
||||
if ($mainAttrType -eq "DynamicList") {
|
||||
$mainTable = "$objectType.$objectName"
|
||||
$mainTable = $objectRef
|
||||
$tailLines = "`n`t`t`t<Settings xsi:type=""DynamicList"">`n`t`t`t`t<MainTable>$mainTable</MainTable>`n`t`t`t</Settings>"
|
||||
} elseif ($purposeRule.SavedData) {
|
||||
$tailLines = "`n`t`t`t<SavedData>true</SavedData>"
|
||||
@@ -713,7 +761,7 @@ if ($insertBefore) {
|
||||
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
|
||||
$isFirstFormForPurpose = $false
|
||||
$defaultPropName = $null
|
||||
$defaultValue = "$objectType.$objectName.Form.$FormName"
|
||||
$defaultValue = "$objectRef.Form.$FormName"
|
||||
|
||||
# Свойство «основная форма» — из записи таблицы. Раньше выбиралось по одному Purpose без учёта
|
||||
# вида, и для журнала писалось DefaultListForm, которого у журнала нет: слот не находился, навык
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-add v1.28 — Add managed form to 1C config object (Python port)
|
||||
# form-add v1.29 — Add managed form to 1C config object (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -559,6 +559,17 @@ def main():
|
||||
"Load": {"main_attr": None, "attr_name": None, "slot": "DefaultLoadForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
# Таблица внешнего источника — единственный вид, чьё имя в ссылках трёхчастное
|
||||
# (Источник.Таблица): подставляется {2}, а не {1}.
|
||||
"Table": {
|
||||
"Object": {"main_attr": "ExternalDataSourceTableObject.{2}", "attr_name": "Объект",
|
||||
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
|
||||
"Record": {"main_attr": "ExternalDataSourceTableRecordManager.{2}", "attr_name": "Запись",
|
||||
"slot": "DefaultRecordForm", "saved_data": True},
|
||||
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
|
||||
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
|
||||
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
|
||||
},
|
||||
}
|
||||
|
||||
# Виды, у которых свойство DefaultForm есть, но собственных форм не бывает.
|
||||
@@ -604,6 +615,26 @@ def main():
|
||||
sys.exit(1)
|
||||
object_name = name_node.text
|
||||
|
||||
# Ссылка на объект и имя для типов формы. У всех видов это "Вид.Имя", и только
|
||||
# у таблицы внешнего источника — "ExternalDataSource.<Источник>.Table.<Таблица>", а в именах
|
||||
# типов — "<Источник>.<Таблица>". Имя источника в самом файле таблицы не хранится —
|
||||
# единственное место, где навык смотрит на путь: ExternalDataSources/<Источник>/Tables/<Таблица>.xml
|
||||
object_qualified_name = object_name
|
||||
object_ref = f"{object_type}.{object_name}"
|
||||
table_data_type = "ObjectData"
|
||||
if object_type == "Table":
|
||||
tables_dir = os.path.dirname(object_xml_full)
|
||||
eds_source = os.path.basename(os.path.dirname(tables_dir))
|
||||
if not eds_source or os.path.basename(tables_dir) != "Tables":
|
||||
print("Таблица внешнего источника ожидается по пути "
|
||||
f"ExternalDataSources/<Источник>/Tables/<Таблица>.xml, а не '{object_xml_full}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
object_qualified_name = f"{eds_source}.{object_name}"
|
||||
object_ref = f"ExternalDataSource.{eds_source}.Table.{object_name}"
|
||||
tdt_node = root.find(".//md:Table/md:Properties/md:TableDataType", NSMAP)
|
||||
if tdt_node is not None and tdt_node.text:
|
||||
table_data_type = tdt_node.text.strip()
|
||||
|
||||
print()
|
||||
print("=== form-add ===")
|
||||
print()
|
||||
@@ -641,10 +672,15 @@ def main():
|
||||
purpose = purpose_synonyms[purpose_probe]
|
||||
|
||||
if not purpose:
|
||||
for k, rule in kind_purposes.items():
|
||||
if rule.get("primary"):
|
||||
purpose = k
|
||||
break
|
||||
if object_type == "Table" and table_data_type == "NonobjectData":
|
||||
# Пометка primary в таблице видов одна на вид, а у таблицы с составным ключом
|
||||
# формы объекта не бывает — основной становится форма записи.
|
||||
purpose = "Record"
|
||||
else:
|
||||
for k, rule in kind_purposes.items():
|
||||
if rule.get("primary"):
|
||||
purpose = k
|
||||
break
|
||||
purpose_key = None
|
||||
for k in kind_purposes:
|
||||
if k.lower() == purpose.lower():
|
||||
@@ -657,6 +693,19 @@ def main():
|
||||
purpose = purpose_key
|
||||
purpose_rule = kind_purposes[purpose]
|
||||
|
||||
# У таблицы внешнего источника набор назначений зависит от вида данных (замерено на
|
||||
# 8.3.24.1691): ObjectData — Object/List/Choice, NonobjectData — Record/List/Choice. Неверная пара
|
||||
# не отвергается схемой формы, а валит загрузку всей конфигурации «Исключением XDTO» без причины.
|
||||
if object_type == "Table":
|
||||
if table_data_type == "NonobjectData" and purpose == "Object":
|
||||
print(f"Таблица '{object_name}' с составным ключом (TableDataType=NonobjectData): "
|
||||
"формы объекта у неё нет — используйте -Purpose Record.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if table_data_type != "NonobjectData" and purpose == "Record":
|
||||
print(f"Таблица '{object_name}' с ключом из одного поля (TableDataType=ObjectData): "
|
||||
"формы записи у неё нет — используйте -Purpose Object.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Гард от повторения дефекта: запись таблицы обязана быть заполненной. Пустой main_attr —
|
||||
# это произвольная форма (законное состояние), а наполовину заполненная запись означала бы,
|
||||
# что таблицу правили невнимательно, и в XML уйдёт мусор вроде `cfg:.Журнал`.
|
||||
@@ -724,13 +773,13 @@ def main():
|
||||
# реквизита брался из отдельной карты, и отсутствие вида в ней давало `cfg:.Имя` — молча.
|
||||
attributes_block = ''
|
||||
if purpose_rule.get("main_attr"):
|
||||
main_attr_type = purpose_rule["main_attr"].format(object_type, object_name)
|
||||
main_attr_type = purpose_rule["main_attr"].format(object_type, object_name, object_qualified_name)
|
||||
main_attr_name = purpose_rule["attr_name"]
|
||||
|
||||
# Динамический список несёт MainTable, остальные типы — SavedData по записи таблицы.
|
||||
tail_lines = ''
|
||||
if main_attr_type == "DynamicList":
|
||||
main_table = f"{object_type}.{object_name}"
|
||||
main_table = object_ref
|
||||
tail_lines = ('\t\t\t<Settings xsi:type="DynamicList">\n'
|
||||
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
|
||||
'\t\t\t</Settings>\n')
|
||||
@@ -857,7 +906,7 @@ def main():
|
||||
# --- SetDefault ---
|
||||
|
||||
is_first_form_for_purpose = False
|
||||
default_value = f"{object_type}.{object_name}.Form.{form_name}"
|
||||
default_value = f"{object_ref}.Form.{form_name}"
|
||||
|
||||
# Свойство «основная форма» — из записи таблицы. Раньше выбиралось по одному purpose без
|
||||
# учёта вида, и для журнала писалось DefaultListForm, которого у журнала нет: слот не
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# form-validate v1.18 — Validate 1C managed form
|
||||
# form-validate v1.19 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -928,6 +928,8 @@ $validCfgPrefixes = @(
|
||||
"ConstantsSet","DataProcessorObject","DocumentObject","DocumentRef"
|
||||
"DynamicList","EnumRef","ExchangePlanObject","ExchangePlanRef"
|
||||
"ExternalDataProcessorObject","ExternalReportObject"
|
||||
"ExternalDataSourceTableObject","ExternalDataSourceTableRecordManager"
|
||||
"ExternalDataSourceTableRef"
|
||||
"InformationRegisterRecordManager","InformationRegisterRecordSet"
|
||||
"ReportObject","TaskObject","TaskRef"
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# form-validate v1.18 — Validate 1C managed form
|
||||
# form-validate v1.19 — Validate 1C managed form
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -66,6 +66,8 @@ VALID_CFG_PREFIXES = {
|
||||
'ConstantsSet', 'DataProcessorObject', 'DocumentObject', 'DocumentRef',
|
||||
'DynamicList', 'EnumRef', 'ExchangePlanObject', 'ExchangePlanRef',
|
||||
'ExternalDataProcessorObject', 'ExternalReportObject',
|
||||
'ExternalDataSourceTableObject', 'ExternalDataSourceTableRecordManager',
|
||||
'ExternalDataSourceTableRef',
|
||||
'InformationRegisterRecordManager', 'InformationRegisterRecordSet',
|
||||
'ReportObject', 'TaskObject', 'TaskRef',
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -
|
||||
| CommonModule, ScheduledJob, EventSubscription | `reference/code.md` |
|
||||
| HTTPService, WebService | `reference/web.md` |
|
||||
| Enum, Constant, DefinedType | `reference/simple.md` |
|
||||
| ExternalDataSource (внешний источник данных) | `reference/external-data-source.md` |
|
||||
| FunctionalOption, FilterCriterion, SettingsStorage, CommonForm, CommonPicture, CommonTemplate, служебные | `reference/other-types.md` |
|
||||
|
||||
Кросс-типовые детали:
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# ExternalDataSource (внешний источник данных)
|
||||
|
||||
Источник описывается **одним JSON целиком**: сам источник, его таблицы с полями и функции.
|
||||
Результат — `ExternalDataSources/<Имя>.xml` плюс по файлу на таблицу в `<Имя>/Tables/`.
|
||||
|
||||
Строку соединения, пользователя, пароль и тип СУБД задавать не нужно: в конфигурации их нет,
|
||||
они настраиваются в режиме «Предприятие» и хранятся в базе.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ExternalDataSource",
|
||||
"name": "PG",
|
||||
"tables": {
|
||||
"prices": ["product_id: Number(10,0)", "period: Date", "price: Number(15,2)"],
|
||||
"products": {
|
||||
"nameInDataSource": "eds.public.products",
|
||||
"tableDataType": "ObjectData",
|
||||
"keyFields": ["id"],
|
||||
"presentationField": "name",
|
||||
"fields": [
|
||||
"id: Number(10,0)",
|
||||
"name: String(150)",
|
||||
"article: String(50) | nullable",
|
||||
"parent_id: ExternalDataSourceTableRef.PG.products | nullable"
|
||||
]
|
||||
}
|
||||
},
|
||||
"functions": {
|
||||
"total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Свойства источника
|
||||
|
||||
| Ключ | Умолчание | Значения |
|
||||
|------|-----------|----------|
|
||||
| `dataLockControlMode` | `Automatic` | `Automatic` / `Managed` / `AutomaticAndManaged` |
|
||||
| `tables` | `{}` | таблицы (см. ниже) |
|
||||
| `functions` | `{}` | функции (см. ниже) |
|
||||
|
||||
При `AutomaticAndManaged` режим блокировок решает каждая таблица сама; при конкретном значении
|
||||
одноимённое свойство таблицы игнорируется платформой.
|
||||
|
||||
## Таблицы
|
||||
|
||||
Ключ — имя таблицы в конфигурации. Значение — **массив полей** либо **объект** со свойствами
|
||||
и ключом `fields` (та же двойственность, что у `tabularSections` справочника).
|
||||
|
||||
| Ключ | Умолчание | Значения |
|
||||
|------|-----------|----------|
|
||||
| `nameInDataSource` | = имя таблицы | имя физической таблицы; для реляционной СУБД обычно `<база>.<схема>.<таблица>` |
|
||||
| `tableType` | `Table` | `Table` — таблица или представление; `Expression` — табличная функция |
|
||||
| `readOnly` | `false` | запрет записи; ставь `true` для представлений и таблиц вида `Expression` — писать в них нельзя |
|
||||
| `expressionInDataSource` | пусто | выражение для `Expression`, напр. `public.f_by_parent(&1)`; имя базы не указывается |
|
||||
| `tableDataType` | `NonobjectData` | `ObjectData` (запись определяется одним полем) / `NonobjectData` |
|
||||
| `keyFields` | `[]` | имена ключевых полей; без них таблица собирается, но недоступны форма записи и набор записей |
|
||||
| `presentationField` | пусто | имя поля представления (только `ObjectData`) |
|
||||
| `parentField` | пусто | имя поля родителя; его тип должен быть ссылкой на эту же таблицу |
|
||||
| `inputByString` | `[]` | имена полей ввода по строке; ключа нет → берётся `presentationField` |
|
||||
| `dataVersionField` | пусто | имя поля версии данных |
|
||||
| `dataLockFields` | `[]` | имена полей блокировки |
|
||||
| `transactionsIsolationLevel` | `Auto` | `Auto` / `ReadUncommitted` / `ReadCommitted` / `RepeatableRead` / `Serializable` |
|
||||
| `dataLockControlMode` | `Automatic` | `Automatic` / `Managed` / `AutomaticAndManaged` |
|
||||
| `basedOn` | `[]` | ввод на основании, ссылки вида `Catalog.Контрагенты` |
|
||||
| `useStandardCommands` | `true` | bool |
|
||||
| `quickChoice` | `false` | bool |
|
||||
| `editType` | `InDialog` | `InDialog` / `InList` |
|
||||
| `fields` | `[]` | поля (см. ниже); синоним — `columns` |
|
||||
|
||||
Ссылки на поля (`keyFields`, `presentationField`, `parentField`, `dataVersionField`,
|
||||
`inputByString`, `dataLockFields`) задаются **короткими именами полей** этой же таблицы.
|
||||
|
||||
Прочие свойства — представления (`objectPresentation`, `listPresentation`, …), формы по умолчанию
|
||||
(`defaultObjectForm`, `defaultListForm`, …), `characteristics`, `explanation`,
|
||||
`includeHelpInContents` — как у справочника.
|
||||
|
||||
**Значение незаполненного родителя загрузкой XML не задаётся** — платформа сбрасывает его в пустое
|
||||
при любой загрузке, включая загрузку собственной выгрузки. Ставится только в Конфигураторе вручную.
|
||||
|
||||
## Поля
|
||||
|
||||
Строковая и объектная форма — те же, что у реквизитов (см. `attributes.md`). Своих ключа три:
|
||||
|
||||
| Ключ | Умолчание | Значения |
|
||||
|------|-----------|----------|
|
||||
| `nameInDataSource` | = имя поля | имя колонки; в одинарных кавычках уходит в SQL как есть |
|
||||
| `readOnly` | `false` | поле не записывается (вычисляемые, автоинкрементные) |
|
||||
| `allowNull` | `false` | допускает `NULL` |
|
||||
|
||||
Флаги строковой формы: `readonly`, `nullable`.
|
||||
|
||||
```json
|
||||
"fields": [
|
||||
"id: Number(10,0) | readonly",
|
||||
{ "name": "article", "type": "String(50)", "nameInDataSource": "art_code", "allowNull": true }
|
||||
]
|
||||
```
|
||||
|
||||
Допустимые типы: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` и ссылка на таблицу
|
||||
внешнего источника — `ExternalDataSourceTableRef.<Источник>.<Таблица>`.
|
||||
|
||||
Двоичные данные: `BinaryData` — безлимит (так их пишет платформа при импорте из СУБД),
|
||||
`BinaryData(N)` — переменной длины, `BinaryData(N,fixed)` — фиксированной.
|
||||
|
||||
**Составной тип у поля недопустим** — платформа такую конфигурацию не загружает.
|
||||
|
||||
## Функции
|
||||
|
||||
Ключ — имя функции. Значение — строка (интерпретируется как `expression`) либо объект.
|
||||
|
||||
| Ключ | Умолчание | Значения |
|
||||
|------|-----------|----------|
|
||||
| `expression` | — | выражение в источнике, обязательный |
|
||||
| `returns` | `String` | тип возвращаемого значения |
|
||||
| `returnValue` | `true` | `false` — процедура, тип не пишется |
|
||||
|
||||
Параметры описываются прямо в выражении как `&1`, `&2` — отдельного ключа для них нет.
|
||||
Необязательные — в фигурных скобках `f(&1{, &2})`, переменное число — `&n[]` (только последним).
|
||||
|
||||
```json
|
||||
"functions": {
|
||||
"nextKey": "NEXT VALUE FOR dbo.SimpleSequence",
|
||||
"total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" }
|
||||
}
|
||||
```
|
||||
|
||||
## Добавить в существующий источник
|
||||
|
||||
`meta-compile` описывает источник **целиком**: повторный запуск заменяет его файл и выдаёт новый
|
||||
uuid, а таблицы, которых нет в описании, останутся на диске сиротами. Чтобы дописать таблицу или
|
||||
функцию в уже существующий источник, есть `meta-edit`:
|
||||
|
||||
```json
|
||||
{ "add": {
|
||||
"tables": { "sales": { "keyFields": ["id"], "fields": ["id: Number(10,0)", "summa: Number(15,2)"] } },
|
||||
"functions": { "nextKey": "NEXT VALUE FOR public.seq_key" }
|
||||
} }
|
||||
```
|
||||
|
||||
Удалить таблицу — `meta-remove ExternalDataSource.<Источник>.Table.<Таблица>`.
|
||||
|
||||
## Не поддерживается
|
||||
|
||||
- **Кубы OLAP** (`Cube`, `DimensionTable`, `Dimension`, `Resource`).
|
||||
- **Формы и модули** таблиц — форму добавляет навык `form-add`, содержимое собирает `form-compile`.
|
||||
Набор назначений зависит от `tableDataType`: `ObjectData` — Object/List/Choice, `NonobjectData` — Record/List/Choice.
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-compile v1.99 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.109 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -279,6 +279,7 @@ $script:objectTypeSynonyms = @{
|
||||
"ВебСервис" = "WebService"
|
||||
"ОпределяемыйТип" = "DefinedType"
|
||||
"ФункциональнаяОпция" = "FunctionalOption"
|
||||
"ВнешнийИсточникДанных" = "ExternalDataSource"
|
||||
}
|
||||
|
||||
# Enum property value synonyms — model often gets these slightly wrong
|
||||
@@ -320,7 +321,9 @@ $script:validEnumValues = @{
|
||||
"WriteMode" = @("Independent","RecorderSubordinate")
|
||||
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
|
||||
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
|
||||
"DataLockControlMode" = @("Automatic","Managed")
|
||||
# AutomaticAndManaged — только у внешнего источника данных и его таблиц: там режим может
|
||||
# решаться на уровне таблицы, у прочих объектов такого значения нет.
|
||||
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
||||
"FullTextSearch" = @("Use","DontUse")
|
||||
"DataHistory" = @("Use","DontUse")
|
||||
"DefaultPresentation" = @("AsDescription","AsCode")
|
||||
@@ -472,7 +475,7 @@ $validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","Ac
|
||||
"HTTPService","WebService","DefinedType","FunctionalOption",
|
||||
"Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm",
|
||||
"SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference",
|
||||
"CommonPicture","CommonTemplate")
|
||||
"CommonPicture","CommonTemplate","ExternalDataSource")
|
||||
# -notin регистронезависим, поэтому "catalog" проходил проверку и дальше шёл в ИМЯ ТЕГА и в
|
||||
# Configuration.xml как есть — выгрузка получалась с <catalog>, которую платформа не принимает.
|
||||
# Прощаем регистр, но приводим к канону списка.
|
||||
@@ -601,6 +604,10 @@ $script:typeSynonyms["bool"] = "Boolean"
|
||||
# ValueStorage / UUID — прощающий ввод (модель может написать base64Binary / рус. форму → канон).
|
||||
$script:typeSynonyms["valuestorage"] = "ValueStorage"
|
||||
$script:typeSynonyms["base64binary"] = "ValueStorage"
|
||||
# ДвоичныеДанные — ОТДЕЛЬНЫЙ тип, не ХранилищеЗначения: платформа пишет его как
|
||||
# xs:base64Binary с квалификаторами. Встречается у полей внешних источников данных.
|
||||
$script:typeSynonyms["binarydata"] = "BinaryData"
|
||||
$script:typeSynonyms["двоичныеданные"] = "BinaryData"
|
||||
$script:typeSynonyms["хранилищезначений"] = "ValueStorage"
|
||||
$script:typeSynonyms["хранилищезначения"] = "ValueStorage"
|
||||
$script:typeSynonyms["uuid"] = "UUID"
|
||||
@@ -638,6 +645,7 @@ $script:typeSynonyms["планвидовхарактеристикссылка"]
|
||||
$script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef"
|
||||
$script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef"
|
||||
$script:typeSynonyms["планобменассылка"] = "ExchangePlanRef"
|
||||
$script:typeSynonyms["внешнийисточникданныхтаблицассылка"] = "ExternalDataSourceTableRef"
|
||||
$script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef"
|
||||
$script:typeSynonyms["задачассылка"] = "TaskRef"
|
||||
$script:typeSynonyms["определяемыйтип"] = "DefinedType"
|
||||
@@ -803,6 +811,25 @@ function Emit-TypeContent {
|
||||
}
|
||||
|
||||
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary, хоть 1С и принимает оба).
|
||||
# ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников).
|
||||
if ($typeStr -match '^BinaryData(\(|$)') {
|
||||
# BinaryData — безлимит (так платформа пишет поле внешнего источника: 4294967292/Fixed).
|
||||
# BinaryData(N) — переменной длины, BinaryData(N,fixed) — фиксированной.
|
||||
$bm = [regex]::Match($typeStr, '^BinaryData(\((\d+)(,\s*(fixed|variable))?\))?$', 'IgnoreCase')
|
||||
if (-not $bm.Success) {
|
||||
Write-Error "Неверный тип '$typeStr': ждётся BinaryData, BinaryData(Длина) или BinaryData(Длина,fixed|variable)."
|
||||
exit 1
|
||||
}
|
||||
$blen = if ($bm.Groups[2].Success) { $bm.Groups[2].Value } else { "4294967292" }
|
||||
$ballowed = if ($bm.Groups[4].Success) { if ($bm.Groups[4].Value.ToLowerInvariant() -eq "fixed") { "Fixed" } else { "Variable" } }
|
||||
elseif ($bm.Groups[2].Success) { "Variable" } else { "Fixed" }
|
||||
X "$indent<v8:Type>xs:base64Binary</v8:Type>"
|
||||
X "$indent<v8:BinaryDataQualifiers>"
|
||||
X "$indent`t<v8:Length>$blen</v8:Length>"
|
||||
X "$indent`t<v8:AllowedLength>$ballowed</v8:AllowedLength>"
|
||||
X "$indent</v8:BinaryDataQualifiers>"
|
||||
return
|
||||
}
|
||||
if ($typeStr -eq "ValueStorage") {
|
||||
X "$indent<v8:Type>v8:ValueStorage</v8:Type>"
|
||||
return
|
||||
@@ -856,7 +883,9 @@ function Emit-TypeContent {
|
||||
# $script:cfgPrefix = $null означает «пишем файл, корень которого cfg НЕ объявляет»
|
||||
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
|
||||
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
|
||||
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') {
|
||||
# ExternalDataSourceTableRef — единственный ссылочный тип с ДВУМЯ частями после префикса
|
||||
# (Источник.Таблица), поэтому `(.+)$` здесь существенно.
|
||||
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef|ExternalDataSourceTableRef)\.(.+)$') {
|
||||
if ($script:cfgPrefix) {
|
||||
X "$indent<v8:Type>$($script:cfgPrefix):$typeStr</v8:Type>"
|
||||
} else {
|
||||
@@ -1080,8 +1109,10 @@ function Emit-FillValue {
|
||||
# --- 5. Attribute shorthand parser ---
|
||||
|
||||
function Build-TypeStr {
|
||||
param($obj)
|
||||
$t = if ($obj.valueType) { "$($obj.valueType)" } elseif ($obj.type) { "$($obj.type)" } else { "" }
|
||||
# ValueTypeOnly — для корневого определения объекта: там ключ type означает ВИД объекта
|
||||
# (Constant, Catalog, …), а не тип значения, и подхватывать его нельзя.
|
||||
param($obj, [switch]$ValueTypeOnly)
|
||||
$t = if ($obj.valueType) { "$($obj.valueType)" } elseif (-not $ValueTypeOnly -and $obj.type) { "$($obj.type)" } else { "" }
|
||||
if ($t -and -not $t.Contains('(')) {
|
||||
if ($t -eq "String" -and $obj.length) {
|
||||
$t = "String($($obj.length))"
|
||||
@@ -1184,6 +1215,10 @@ function Parse-AttributeShorthand {
|
||||
# Режим приведения типов измерения РС (формат 2.18). Ключ обязан доехать до эмиттера:
|
||||
# без него не-дефолтное значение (Deny / DeleteData) молча заменялось на TransformValues.
|
||||
typeReductionMode = $val.typeReductionMode
|
||||
# Поле внешнего источника данных (контекст eds-field).
|
||||
nameInDataSource = if ($val.nameInDataSource) { "$($val.nameInDataSource)" } else { "" }
|
||||
readOnly = if ($val.readOnly -eq $true) { $true } else { $false }
|
||||
allowNull = if ($val.allowNull -eq $true) { $true } else { $false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1324,6 +1359,11 @@ $script:generatedTypes = @{
|
||||
"DefinedType" = @(
|
||||
@{ prefix = "DefinedType"; category = "DefinedType" }
|
||||
)
|
||||
"ExternalDataSource" = @(
|
||||
@{ prefix = "ExternalDataSourceManager"; category = "Manager" }
|
||||
@{ prefix = "ExternalDataSourceTablesManager"; category = "TablesManager" }
|
||||
@{ prefix = "ExternalDataSourceCubesManager"; category = "CubesManager" }
|
||||
)
|
||||
"DocumentJournal" = @(
|
||||
@{ prefix = "DocumentJournalSelection"; category = "Selection" }
|
||||
@{ prefix = "DocumentJournalList"; category = "List" }
|
||||
@@ -2049,7 +2089,7 @@ function Emit-Attribute {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
} elseif ($context -notin @("tabular", "processor-tabular") -and
|
||||
} elseif ($context -notin @("tabular", "processor-tabular", "eds-field") -and
|
||||
($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName))) {
|
||||
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
|
||||
}
|
||||
@@ -2113,18 +2153,34 @@ function Emit-Attribute {
|
||||
if ($parsed.fillChecking) { $fillChecking = $parsed.fillChecking }
|
||||
X "$indent`t`t<FillChecking>$fillChecking</FillChecking>"
|
||||
|
||||
X "$indent`t`t<ChoiceFoldersAndItems>$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })</ChoiceFoldersAndItems>"
|
||||
# Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm
|
||||
# у него стоит ПОСЛЕ ChoiceHistoryOnInput, а не перед — порядок снят с выгрузки платформы.
|
||||
if ($context -ne "eds-field") {
|
||||
X "$indent`t`t<ChoiceFoldersAndItems>$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })</ChoiceFoldersAndItems>"
|
||||
}
|
||||
Emit-ChoiceParameterLinks "$indent`t`t" $parsed.choiceParameterLinks
|
||||
Emit-ChoiceParameters "$indent`t`t" $parsed.choiceParameters
|
||||
$qc = if ($parsed.quickChoice) { $parsed.quickChoice } else { "Auto" }
|
||||
X "$indent`t`t<QuickChoice>$qc</QuickChoice>"
|
||||
$coi = if ($parsed.createOnInput) { $parsed.createOnInput } else { "Auto" }
|
||||
X "$indent`t`t<CreateOnInput>$coi</CreateOnInput>"
|
||||
if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
|
||||
Emit-LinkByType "$indent`t`t" $parsed.linkByType
|
||||
if ($context -ne "eds-field") {
|
||||
if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
|
||||
Emit-LinkByType "$indent`t`t" $parsed.linkByType
|
||||
}
|
||||
$chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" }
|
||||
X "$indent`t`t<ChoiceHistoryOnInput>$chi</ChoiceHistoryOnInput>"
|
||||
|
||||
if ($context -eq "eds-field") {
|
||||
if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
|
||||
$nids = if ($parsed.nameInDataSource) { "$($parsed.nameInDataSource)" } else { $parsed.name }
|
||||
X "$indent`t`t<NameInDataSource>$(Esc-XmlText $nids)</NameInDataSource>"
|
||||
$ro = if ($parsed.readOnly -eq $true -or $parsed.flags -contains "readonly") { "true" } else { "false" }
|
||||
X "$indent`t`t<ReadOnly>$ro</ReadOnly>"
|
||||
$an = if ($parsed.allowNull -eq $true -or $parsed.flags -contains "nullable") { "true" } else { "false" }
|
||||
X "$indent`t`t<AllowNull>$an</AllowNull>"
|
||||
}
|
||||
|
||||
# Измерение регистра сведений: Master/MainFilter/DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing).
|
||||
if ($elemTag -eq "Dimension" -and $context -eq "register-info") {
|
||||
$master = if ($parsed.master -eq $true -or $parsed.flags -contains "master") { "true" } else { "false" }
|
||||
@@ -2178,7 +2234,8 @@ function Emit-Attribute {
|
||||
}
|
||||
|
||||
# Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular)
|
||||
if ($context -notin @("processor", "processor-tabular")) {
|
||||
# и не для полей внешнего источника: индексами и полнотекстовым поиском чужой таблицы 1С не владеет.
|
||||
if ($context -notin @("processor", "processor-tabular", "eds-field")) {
|
||||
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
|
||||
if ($context -ne "account-flag") {
|
||||
# Ресурс регистра накопления/расчёта/бухгалтерии НЕ имеет <Indexing> (только <FullTextSearch>); измерение/реквизит — имеют.
|
||||
@@ -2771,8 +2828,8 @@ function Emit-ConstantProperties {
|
||||
if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
|
||||
|
||||
# Type — valueType (пустой явный '' → <Type/>, реквизит без типа; отсутствие → String дефолт).
|
||||
$valueType = Build-TypeStr $def
|
||||
$typeEmpty = ($null -ne $def.valueType -and "$($def.valueType)".Trim() -eq '') -or ($null -ne $def.type -and "$($def.type)".Trim() -eq '')
|
||||
$valueType = Build-TypeStr $def -ValueTypeOnly
|
||||
$typeEmpty = ($null -ne $def.valueType -and "$($def.valueType)".Trim() -eq '')
|
||||
if ($typeEmpty) { X "$i<Type/>" }
|
||||
else { if (-not $valueType) { $valueType = "String" }; Emit-ValueType $i $valueType }
|
||||
|
||||
@@ -4314,6 +4371,222 @@ function Emit-AddressingAttribute {
|
||||
Emit-Attribute $indent $parsed "task-addressing" "AddressingAttribute"
|
||||
}
|
||||
|
||||
# --- 13h. Внешние источники данных ---
|
||||
# Источник пишется в один файл, каждая его таблица — в свой. Функции живут ВНУТРИ файла источника
|
||||
# полными узлами, наравне со списком имён таблиц: так их выгружает платформа.
|
||||
|
||||
# Таблицы: dict имя → массив полей ЛИБО объект со свойствами и ключом fields/columns.
|
||||
# Нормализуем в [ordered]@{ имя = @{ props; fields } } — форма зеркальна tabularSections.
|
||||
function Get-EdsTables {
|
||||
param($val)
|
||||
$tables = [ordered]@{}
|
||||
if (-not $val) { return $tables }
|
||||
function New-EdsTableEntry { param($v)
|
||||
if ($v -is [array] -or $v.GetType().Name -eq 'Object[]') {
|
||||
return @{ props = $null; fields = @($v) }
|
||||
}
|
||||
$f = if ($null -ne $v.fields) { @($v.fields) } elseif ($null -ne $v.columns) { @($v.columns) } else { @() }
|
||||
return @{ props = $v; fields = $f }
|
||||
}
|
||||
if ($val -is [array] -or $val.GetType().Name -eq 'Object[]') {
|
||||
foreach ($t in $val) { $tables["$($t.name)"] = New-EdsTableEntry $t }
|
||||
} else {
|
||||
$val.PSObject.Properties | ForEach-Object { $tables[$_.Name] = New-EdsTableEntry $_.Value }
|
||||
}
|
||||
return $tables
|
||||
}
|
||||
|
||||
# Ссылка на поле таблицы: в DSL короткое имя, в XML — полный путь.
|
||||
function Get-EdsFieldRef {
|
||||
param([string]$srcName, [string]$tableName, [string]$fieldName)
|
||||
if (-not $fieldName) { return "" }
|
||||
if ($fieldName -like "ExternalDataSource.*") { return $fieldName }
|
||||
return "ExternalDataSource.$srcName.Table.$tableName.Field.$fieldName"
|
||||
}
|
||||
|
||||
function Emit-EdsFieldRefList {
|
||||
param([string]$indent, [string]$tag, $names, [string]$srcName, [string]$tableName)
|
||||
$list = @($names | Where-Object { $_ })
|
||||
if ($list.Count -eq 0) { X "$indent<$tag/>"; return }
|
||||
X "$indent<$tag>"
|
||||
foreach ($n in $list) {
|
||||
X "$indent`t<xr:Field>$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$n"))</xr:Field>"
|
||||
}
|
||||
X "$indent</$tag>"
|
||||
}
|
||||
|
||||
function Emit-EdsFieldRefScalar {
|
||||
param([string]$indent, [string]$tag, $name, [string]$srcName, [string]$tableName)
|
||||
if (-not $name) { X "$indent<$tag/>"; return }
|
||||
X "$indent<$tag>$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$name"))</$tag>"
|
||||
}
|
||||
|
||||
function Emit-ExternalDataSourceProperties {
|
||||
param([string]$indent)
|
||||
$i = $indent
|
||||
X "$i<Name>$(Esc-XmlText $objName)</Name>"
|
||||
Emit-MLText $i "Synonym" $synonym
|
||||
if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
|
||||
$dlcm = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
|
||||
X "$i<DataLockControlMode>$dlcm</DataLockControlMode>"
|
||||
}
|
||||
|
||||
# Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо
|
||||
# в выражении как &1, &2 (см. reference/external-data-source.md).
|
||||
function Emit-EdsFunction {
|
||||
# $typeXml — уже собранный узел <Type> возвращаемого значения: его рендерит вызывающий навык
|
||||
# своим эмиттером типов. Так тело функции не зависит от того, какой это навык.
|
||||
param([string]$indent, [string]$fnName, $val, [string]$typeXml)
|
||||
$expr = ""
|
||||
$returns = ""
|
||||
$returnValue = $true
|
||||
$fnSynonym = $null
|
||||
$fnComment = ""
|
||||
if ($val -is [string]) {
|
||||
$expr = "$val"
|
||||
} else {
|
||||
$expr = if ($val.expression) { "$($val.expression)" } elseif ($val.expressionInDataSource) { "$($val.expressionInDataSource)" } else { "" }
|
||||
$returns = if ($val.returns) { "$($val.returns)" } elseif ($val.returnType) { "$($val.returnType)" } else { "" }
|
||||
if ($null -ne $val.returnValue) { $returnValue = ($val.returnValue -eq $true) }
|
||||
$fnSynonym = $val.synonym
|
||||
$fnComment = if ($val.comment) { "$($val.comment)" } else { "" }
|
||||
}
|
||||
if (-not $expr) {
|
||||
Write-Error "Функция '$fnName' внешнего источника данных: не задано выражение (ключ expression)."
|
||||
exit 1
|
||||
}
|
||||
$uuid = New-Guid-String
|
||||
X "$indent<Function uuid=`"$uuid`">"
|
||||
X "$indent`t<Properties>"
|
||||
X "$indent`t`t<Name>$(Esc-XmlText $fnName)</Name>"
|
||||
Emit-MLText "$indent`t`t" "Synonym" $fnSynonym
|
||||
if ($fnComment) { X "$indent`t`t<Comment>$(Esc-XmlText $fnComment)</Comment>" } else { X "$indent`t`t<Comment/>" }
|
||||
X "$indent`t`t<ReturnValue>$(if ($returnValue) { 'true' } else { 'false' })</ReturnValue>"
|
||||
if ($returnValue -and $typeXml) {
|
||||
X $typeXml.TrimEnd("`r", "`n")
|
||||
} else {
|
||||
X "$indent`t`t<Type/>"
|
||||
}
|
||||
X "$indent`t`t<ExpressionInDataSource>$(Esc-XmlText $expr)</ExpressionInDataSource>"
|
||||
X "$indent`t</Properties>"
|
||||
X "$indent</Function>"
|
||||
}
|
||||
|
||||
# Свойства таблицы: 38 узлов в порядке выгрузки платформы.
|
||||
function Emit-EdsTableProperties {
|
||||
# $charXml и $defaultFormsXml — уже собранные блоки <Characteristics> и четыре слота
|
||||
# <Default*Form>: их рендерит вызывающий навык своим эмиттером. Так тело не зависит
|
||||
# от хелперов конкретного навыка и годится для копирования (check-inline-drift).
|
||||
param([string]$indent, [string]$srcName, [string]$tableName, $t, [string]$charXml, [string]$defaultFormsXml)
|
||||
$i = $indent
|
||||
$tblSynonym = if ($t -and $null -ne $t.synonym) { $t.synonym } else { Split-CamelCase $tableName }
|
||||
X "$i<Name>$(Esc-XmlText $tableName)</Name>"
|
||||
Emit-MLText $i "Synonym" $tblSynonym
|
||||
if ($t -and $t.comment) { X "$i<Comment>$(Esc-XmlText "$($t.comment)")</Comment>" } else { X "$i<Comment/>" }
|
||||
|
||||
$tableType = if ($t -and $t.tableType) { "$($t.tableType)" } else { "Table" }
|
||||
X "$i<TableType>$tableType</TableType>"
|
||||
# Имя в источнике по умолчанию равно имени объекта — так поступает и платформа.
|
||||
$nids = if ($t -and $t.nameInDataSource) { "$($t.nameInDataSource)" } elseif ($tableType -eq "Expression") { "" } else { $tableName }
|
||||
if ($nids) { X "$i<NameInDataSource>$(Esc-XmlText $nids)</NameInDataSource>" } else { X "$i<NameInDataSource/>" }
|
||||
$expr = if ($t -and $t.expressionInDataSource) { "$($t.expressionInDataSource)" } elseif ($t -and $t.expression) { "$($t.expression)" } else { "" }
|
||||
if ($expr) { X "$i<ExpressionInDataSource>$(Esc-XmlText $expr)</ExpressionInDataSource>" } else { X "$i<ExpressionInDataSource/>" }
|
||||
$dataType = if ($t -and $t.tableDataType) { "$($t.tableDataType)" } else { "NonobjectData" }
|
||||
X "$i<TableDataType>$dataType</TableDataType>"
|
||||
|
||||
Emit-EdsFieldRefList $i "KeyFields" $(if ($t) { $t.keyFields } else { $null }) $srcName $tableName
|
||||
Emit-EdsFieldRefScalar $i "PresentationField" $(if ($t) { $t.presentationField } else { $null }) $srcName $tableName
|
||||
Emit-EdsFieldRefScalar $i "ParentField" $(if ($t) { $t.parentField } else { $null }) $srcName $tableName
|
||||
# Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения»
|
||||
# различаются формой самого значения (xsi:nil против типизированного).
|
||||
# ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено
|
||||
# на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы
|
||||
# с полем родителя — пустая строка (как после загрузки), а без него — nil.
|
||||
if ($t -and $t.parentField) { X "$i<UnfilledParentValue xsi:type=`"xs:string`"/>" }
|
||||
else { X "$i<UnfilledParentValue xsi:nil=`"true`"/>" }
|
||||
if ($charXml) { X $charXml.TrimEnd("`r", "`n") } else { X "$i<Characteristics/>" }
|
||||
|
||||
X "$i<UseStandardCommands>$(if ($t -and $t.useStandardCommands -eq $false) { 'false' } else { 'true' })</UseStandardCommands>"
|
||||
X "$i<QuickChoice>$(if ($t -and $t.quickChoice -eq $true) { 'true' } else { 'false' })</QuickChoice>"
|
||||
# Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке).
|
||||
# Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка.
|
||||
$ibsGiven = ($t -and $t.PSObject -and $t.PSObject.Properties -and ($t.PSObject.Properties.Name -contains 'inputByString'))
|
||||
$ibs = if ($ibsGiven) { $t.inputByString } elseif ($t -and $t.presentationField) { @($t.presentationField) } else { $null }
|
||||
Emit-EdsFieldRefList $i "InputByString" $ibs $srcName $tableName
|
||||
X "$i<CreateOnInput>$(if ($t -and $t.createOnInput) { "$($t.createOnInput)" } else { 'Auto' })</CreateOnInput>"
|
||||
X "$i<SearchStringModeOnInputByString>$(if ($t -and $t.searchStringModeOnInputByString) { "$($t.searchStringModeOnInputByString)" } else { 'Begin' })</SearchStringModeOnInputByString>"
|
||||
X "$i<ChoiceDataGetModeOnInputByString>$(if ($t -and $t.choiceDataGetModeOnInputByString) { "$($t.choiceDataGetModeOnInputByString)" } else { 'Directly' })</ChoiceDataGetModeOnInputByString>"
|
||||
X "$i<ChoiceHistoryOnInput>$(if ($t -and $t.choiceHistoryOnInput) { "$($t.choiceHistoryOnInput)" } else { 'Auto' })</ChoiceHistoryOnInput>"
|
||||
|
||||
# Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38.
|
||||
if ($defaultFormsXml) { X $defaultFormsXml.TrimEnd("`r", "`n") }
|
||||
else { foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { X "$i<$formTag/>" } }
|
||||
foreach ($presTag in @("ObjectPresentation","ExtendedObjectPresentation","RecordPresentation",
|
||||
"ExtendedRecordPresentation","ListPresentation","ExtendedListPresentation","Explanation")) {
|
||||
$key = $presTag.Substring(0,1).ToLower() + $presTag.Substring(1)
|
||||
Emit-MLText $i $presTag $(if ($t) { $t.$key } else { $null })
|
||||
}
|
||||
X "$i<IncludeHelpInContents>$(if ($t -and $t.includeHelpInContents -eq $true) { 'true' } else { 'false' })</IncludeHelpInContents>"
|
||||
X "$i<ReadOnly>$(if ($t -and $t.readOnly -eq $true) { 'true' } else { 'false' })</ReadOnly>"
|
||||
X "$i<TransactionsIsolationLevel>$(if ($t -and $t.transactionsIsolationLevel) { "$($t.transactionsIsolationLevel)" } else { 'Auto' })</TransactionsIsolationLevel>"
|
||||
Emit-EdsFieldRefScalar $i "DataVersionField" $(if ($t) { $t.dataVersionField } else { $null }) $srcName $tableName
|
||||
X "$i<EditType>$(if ($t -and $t.editType) { "$($t.editType)" } else { 'InDialog' })</EditType>"
|
||||
Emit-MDRefList $i "BasedOn" $(if ($t) { $t.basedOn } else { $null })
|
||||
Emit-EdsFieldRefList $i "DataLockFields" $(if ($t) { $t.dataLockFields } else { $null }) $srcName $tableName
|
||||
X "$i<DataLockControlMode>$(if ($t -and $t.dataLockControlMode) { "$($t.dataLockControlMode)" } else { 'Automatic' })</DataLockControlMode>"
|
||||
}
|
||||
|
||||
# Отдельный XML-документ таблицы. Возвращает строку: X пишет в общий StringBuilder,
|
||||
# поэтому «перехват» — запомнить длину, отдать эмиттерам, вырезать добавленное
|
||||
# (тот же приём, что у составного типа).
|
||||
function Build-EdsTableXml {
|
||||
# $fieldsXml, $charXml, $defaultFormsXml — уже собранные узлы: их рендерит вызывающий навык
|
||||
# своими эмиттерами. Так тело функции не зависит от того, какой это навык.
|
||||
param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml, [string]$charXml, [string]$defaultFormsXml)
|
||||
$before = $script:xml.Length
|
||||
|
||||
$tableUuid = New-Guid-String
|
||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
X "<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">"
|
||||
X "`t<Table uuid=`"$tableUuid`">"
|
||||
# InternalInfo у таблицы эмитится здесь, а не через $script:generatedTypes: имя элемента
|
||||
# трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает.
|
||||
X "`t`t<InternalInfo>"
|
||||
foreach ($pair in @(
|
||||
@("ExternalDataSourceTableManager", "Manager"),
|
||||
@("ExternalDataSourceTableObject", "Object"),
|
||||
@("ExternalDataSourceTableRef", "Ref"),
|
||||
@("ExternalDataSourceTableList", "List"),
|
||||
@("ExternalDataSourceTableRecord", "Record"),
|
||||
@("ExternalDataSourceTableRecordSet", "RecordSet"),
|
||||
@("ExternalDataSourceTableRecordKey", "RecordKey"),
|
||||
@("ExternalDataSourceTableRecordManager", "RecordManager"))) {
|
||||
X "`t`t`t<xr:GeneratedType name=`"$($pair[0]).$srcName.$tableName`" category=`"$($pair[1])`">"
|
||||
X "`t`t`t`t<xr:TypeId>$(New-Guid-String)</xr:TypeId>"
|
||||
X "`t`t`t`t<xr:ValueId>$(New-Guid-String)</xr:ValueId>"
|
||||
X "`t`t`t</xr:GeneratedType>"
|
||||
}
|
||||
X "`t`t</InternalInfo>"
|
||||
|
||||
X "`t`t<Properties>"
|
||||
Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props $charXml $defaultFormsXml
|
||||
X "`t`t</Properties>"
|
||||
|
||||
if ($fieldsXml) {
|
||||
X "`t`t<ChildObjects>"
|
||||
X $fieldsXml.TrimEnd("`r", "`n")
|
||||
X "`t`t</ChildObjects>"
|
||||
} else {
|
||||
X "`t`t<ChildObjects/>"
|
||||
}
|
||||
X "`t</Table>"
|
||||
X "</MetaDataObject>"
|
||||
|
||||
$chunk = $script:xml.ToString($before, $script:xml.Length - $before)
|
||||
[void]$script:xml.Remove($before, $script:xml.Length - $before)
|
||||
return $chunk
|
||||
}
|
||||
|
||||
# --- 14. Namespaces ---
|
||||
|
||||
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
@@ -4457,6 +4730,7 @@ switch ($objType) {
|
||||
"Task" { Emit-TaskProperties "`t`t`t" }
|
||||
"HTTPService" { Emit-HTTPServiceProperties "`t`t`t" }
|
||||
"WebService" { Emit-WebServiceProperties "`t`t`t" }
|
||||
"ExternalDataSource" { Emit-ExternalDataSourceProperties "`t`t`t" }
|
||||
}
|
||||
|
||||
X "`t`t</Properties>"
|
||||
@@ -4754,6 +5028,41 @@ if ($objType -eq "WebService") {
|
||||
}
|
||||
}
|
||||
|
||||
# --- ExternalDataSource: Tables (именами) + Functions (полными узлами) ---
|
||||
$script:edsTables = [ordered]@{}
|
||||
if ($objType -eq "ExternalDataSource") {
|
||||
$script:edsTables = Get-EdsTables $def.tables
|
||||
$functions = [ordered]@{}
|
||||
if ($def.functions) {
|
||||
$def.functions.PSObject.Properties | ForEach-Object { $functions[$_.Name] = $_.Value }
|
||||
}
|
||||
if ($script:edsTables.Count -gt 0 -or $functions.Count -gt 0) {
|
||||
$hasChildren = $true
|
||||
X "`t`t<ChildObjects>"
|
||||
foreach ($tblName in $script:edsTables.Keys) {
|
||||
X "`t`t`t<Table>$(Esc-XmlText $tblName)</Table>"
|
||||
}
|
||||
foreach ($fnName in $functions.Keys) {
|
||||
$fnVal = $functions[$fnName]
|
||||
$fnReturns = if ($fnVal -is [string]) { "String" }
|
||||
elseif ($fnVal.returns) { "$($fnVal.returns)" }
|
||||
elseif ($fnVal.returnType) { "$($fnVal.returnType)" } else { "String" }
|
||||
$fnNoValue = (-not ($fnVal -is [string])) -and ($null -ne $fnVal.returnValue) -and ($fnVal.returnValue -ne $true)
|
||||
$fnTypeXml = ""
|
||||
if (-not $fnNoValue) {
|
||||
$typeBefore = $script:xml.Length
|
||||
Emit-ValueType "`t`t`t`t`t" $fnReturns
|
||||
$fnTypeXml = $script:xml.ToString($typeBefore, $script:xml.Length - $typeBefore)
|
||||
[void]$script:xml.Remove($typeBefore, $script:xml.Length - $typeBefore)
|
||||
}
|
||||
Emit-EdsFunction "`t`t`t" $fnName $fnVal $fnTypeXml
|
||||
}
|
||||
X "`t`t</ChildObjects>"
|
||||
} else {
|
||||
X "`t`t<ChildObjects/>"
|
||||
}
|
||||
}
|
||||
|
||||
# --- CommonModule: no ChildObjects ---
|
||||
|
||||
X "`t</$objType>"
|
||||
@@ -4802,6 +5111,7 @@ $script:typePluralMap = @{
|
||||
"WSReference" = "WSReferences"
|
||||
"CommonPicture" = "CommonPictures"
|
||||
"CommonTemplate" = "CommonTemplates"
|
||||
"ExternalDataSource" = "ExternalDataSources"
|
||||
}
|
||||
|
||||
$typePlural = $script:typePluralMap[$objType]
|
||||
@@ -5034,8 +5344,54 @@ function Write-XmlFileKeepEol([string]$path, [string]$text, $encoding) {
|
||||
[System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $encoding)
|
||||
}
|
||||
|
||||
# Объект с таким именем уже есть: компиляция заменит его файл ЦЕЛИКОМ и выдаст новый uuid —
|
||||
# ссылки на прежний объект (из кода, состава подсистем, типов реквизитов) станут висячими.
|
||||
# Для доработки существующего объекта есть meta-edit; молчать об этом нельзя.
|
||||
if (Test-Path $mainXmlPath) {
|
||||
Write-Warning "$objType '$objName' уже существует ($typePlural/$objName.xml) — файл будет перезаписан, объект получит НОВЫЙ uuid, ссылки на прежний сломаются. Для правки существующего объекта используйте meta-edit."
|
||||
}
|
||||
|
||||
Write-XmlFileKeepEol $mainXmlPath $metadataXml $enc
|
||||
|
||||
# Таблицы внешнего источника — отдельными файлами в <Источник>/Tables/.
|
||||
# Единственный вид, у которого объект складывается более чем из одного XML.
|
||||
$edsTablesCreated = @()
|
||||
if ($objType -eq "ExternalDataSource" -and $script:edsTables.Count -gt 0) {
|
||||
$tablesDir = Join-Path $objSubDir "Tables"
|
||||
if (-not (Test-Path $tablesDir)) { New-Item -ItemType Directory -Path $tablesDir -Force | Out-Null }
|
||||
foreach ($tblName in $script:edsTables.Keys) {
|
||||
$entry = $script:edsTables[$tblName]
|
||||
$fieldsBefore = $script:xml.Length
|
||||
foreach ($f in @($entry.fields)) {
|
||||
Emit-Attribute "`t`t`t" (Parse-AttributeShorthand $f) "eds-field" "Field"
|
||||
}
|
||||
$fieldsXml = $script:xml.ToString($fieldsBefore, $script:xml.Length - $fieldsBefore)
|
||||
[void]$script:xml.Remove($fieldsBefore, $script:xml.Length - $fieldsBefore)
|
||||
$tp = $entry.props
|
||||
$charBefore = $script:xml.Length
|
||||
Emit-Characteristics "`t`t`t" $(if ($tp) { $tp.characteristics } else { $null })
|
||||
$charXml = $script:xml.ToString($charBefore, $script:xml.Length - $charBefore)
|
||||
[void]$script:xml.Remove($charBefore, $script:xml.Length - $charBefore)
|
||||
|
||||
# Слот формы: короткое имя разворачивается в полный путь таблицы внешнего источника —
|
||||
# голое имя платформа отвергает («Неизвестный объект метаданных»).
|
||||
$formsBefore = $script:xml.Length
|
||||
foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) {
|
||||
$key = $formTag.Substring(0,1).ToLower() + $formTag.Substring(1)
|
||||
$formVal = if ($tp) { $tp.$key } else { $null }
|
||||
if ($formVal -and "$formVal" -notmatch '\.') { $formVal = "ExternalDataSource.$objName.Table.$tblName.Form.$formVal" }
|
||||
Emit-FormRef "`t`t`t" $formTag $formVal
|
||||
}
|
||||
$defaultFormsXml = $script:xml.ToString($formsBefore, $script:xml.Length - $formsBefore)
|
||||
[void]$script:xml.Remove($formsBefore, $script:xml.Length - $formsBefore)
|
||||
|
||||
$tableXml = Build-EdsTableXml $objName $tblName $entry $fieldsXml $charXml $defaultFormsXml
|
||||
$tablePath = Join-Path $tablesDir "$tblName.xml"
|
||||
Write-XmlFileKeepEol $tablePath $tableXml $enc
|
||||
$edsTablesCreated += $tablePath
|
||||
}
|
||||
}
|
||||
|
||||
# Module files
|
||||
$modulesCreated = @()
|
||||
|
||||
@@ -5234,9 +5590,102 @@ if ($commands -and $commands.Count -gt 0) {
|
||||
|
||||
# --- 17. Register in Configuration.xml ---
|
||||
|
||||
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
# остаётся рабочим каталогом проекта.
|
||||
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Get-NewObjectPosition([string]$cfgDir) {
|
||||
try {
|
||||
if (-not $cfgDir) { $cfgDir = "." }
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||
if (-not $pj) { return "end" }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc -and $db.newObjectPosition) {
|
||||
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
} catch { return "end" }
|
||||
}
|
||||
|
||||
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
# Явно названный вид сортируется в любом случае.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Test-OrderSensitiveType([string]$typeName) {
|
||||
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||
}
|
||||
|
||||
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||
$keys = @("", "")
|
||||
$names = @($a, $b)
|
||||
for ($i = 0; $i -lt 2; $i++) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||
else { [void]$sb.Append('0') }
|
||||
[void]$sb.Append($ch)
|
||||
}
|
||||
$keys[$i] = $sb.ToString()
|
||||
}
|
||||
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||
if ($r -lt 0) { return -1 }
|
||||
if ($r -gt 0) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
# Регистрация объекта в <ChildObjects> родительского XML. Общая реализация: эталон —
|
||||
# meta-compile, копия — role-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
# meta-compile, копии — role-compile, xdto-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
# Возвращает исход: added | already | no-childobj | no-config.
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
|
||||
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
|
||||
|
||||
@@ -5255,56 +5704,73 @@ function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [st
|
||||
if ($e.InnerText -eq $ChildName) { return "already" }
|
||||
}
|
||||
|
||||
$newElem = $doc.CreateElement($ChildTag, "http://v8.1c.ru/8.3/MDClasses")
|
||||
$newElem.InnerText = $ChildName
|
||||
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
|
||||
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт —
|
||||
# дельта ровно в одну строку. Правим чужой файл, значит наследуем его стиль (#44/#46/#47).
|
||||
# DOM выше — только на чтение: найти ChildObjects и отсечь дубликат.
|
||||
$configContent = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
|
||||
$eol = if ($configContent.Contains("`r`n")) { "`r`n" } else { "`n" }
|
||||
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
if ($existing.Count -gt 0) {
|
||||
# Insert after last existing element of same type
|
||||
$lastElem = $existing[$existing.Count - 1]
|
||||
$newWs = $doc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertAfter($newWs, $lastElem) | Out-Null
|
||||
$childObjects.InsertAfter($newElem, $newWs) | Out-Null
|
||||
} else {
|
||||
# No existing elements of this type — insert before closing whitespace.
|
||||
# Самозакрытый <ChildObjects/> попадает сюда же: LastChild пуст, идёт ветка AppendChild.
|
||||
$lastChild = $childObjects.LastChild
|
||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
||||
$newWs = $doc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
|
||||
$childObjects.InsertBefore($newElem, $lastChild) | Out-Null
|
||||
} else {
|
||||
$childObjects.AppendChild($doc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||
$childObjects.AppendChild($newElem) | Out-Null
|
||||
$childObjects.AppendChild($doc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
$block = [regex]::Match($configContent, '(?s)<ChildObjects\s*>.*?</ChildObjects>')
|
||||
if (-not $block.Success) {
|
||||
# Самозакрытый <ChildObjects/> раскрываем первой записью
|
||||
$empty = [regex]::Match($configContent, '<ChildObjects\s*/>')
|
||||
if (-not $empty.Success) { return "no-childobj" }
|
||||
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
|
||||
$newContent = $configContent.Substring(0, $empty.Index) + $replacement + $configContent.Substring($empty.Index + $empty.Length)
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
|
||||
# byName: перед первым объектом того же вида, чьё имя больше нового.
|
||||
# Виды с осмысленным порядком в дереве пропускаем — см. Test-OrderSensitiveType.
|
||||
if (-not (Test-OrderSensitiveType $ChildTag) -and (Get-NewObjectPosition ([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($ParentXmlPath)))) -eq "byName") {
|
||||
$lineRx = [regex]"(?m)^([ \t]*)<$ChildTag>([^<]*)</$ChildTag>"
|
||||
$m = $lineRx.Match($configContent, $block.Index, $block.Length)
|
||||
while ($m.Success) {
|
||||
if ((Compare-MetadataNames $m.Groups[2].Value $ChildName) -gt 0) {
|
||||
$newContent = $configContent.Substring(0, $m.Index) + $m.Groups[1].Value + $entry + $eol + $configContent.Substring($m.Index)
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
$m = $m.NextMatch()
|
||||
}
|
||||
}
|
||||
|
||||
# Save. Пишем через MemoryStream, а не прямо в файл: нужен шаг пост-обработки
|
||||
# строки — XmlWriter отдаёт `encoding="utf-8"` и `<a />`, Конфигуратор пишет
|
||||
# `encoding="UTF-8"` и `<a/>`.
|
||||
$cfgSettings = New-Object System.Xml.XmlWriterSettings
|
||||
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$cfgSettings.Indent = $false
|
||||
$cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
|
||||
$doc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
|
||||
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ([System.IO.File]::ReadAllText($ParentXmlPath) -notmatch "`r`n") { "`n" } else { "`r`n" }
|
||||
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
|
||||
|
||||
$closeSame = "</$ChildTag>"
|
||||
$blockEnd = $block.Index + $block.Length
|
||||
$lastSame = $configContent.LastIndexOf($closeSame, $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
|
||||
if ($lastSame -ge 0) {
|
||||
# После последнего объекта того же вида (группы по видам сохраняются)
|
||||
$insertAt = $lastSame + $closeSame.Length
|
||||
$newContent = $configContent.Substring(0, $insertAt) + "$eol`t`t`t$entry" + $configContent.Substring($insertAt)
|
||||
} else {
|
||||
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
|
||||
# группой вида старше по $childObjectTypes. Дописать в конец блока нельзя: платформа
|
||||
# переставит группу при первой же выгрузке и даст диф на ровном месте.
|
||||
$ownIdx = $childObjectTypes.IndexOf($ChildTag)
|
||||
$anchor = $null
|
||||
if ($ownIdx -ge 0) {
|
||||
$typeRx = [regex]"(?m)^([ \t]*)<(\w+)>[^<]*</\2>"
|
||||
$tm = $typeRx.Match($configContent, $block.Index, $block.Length)
|
||||
while ($tm.Success) {
|
||||
$otherIdx = $childObjectTypes.IndexOf($tm.Groups[2].Value)
|
||||
if ($otherIdx -gt $ownIdx) { $anchor = $tm; break }
|
||||
$tm = $tm.NextMatch()
|
||||
}
|
||||
}
|
||||
if ($anchor) {
|
||||
$newContent = $configContent.Substring(0, $anchor.Index) + $anchor.Groups[1].Value + $entry + $eol + $configContent.Substring($anchor.Index)
|
||||
} else {
|
||||
# Видов старше в файле нет — новая строка перед </ChildObjects>,
|
||||
# отступ закрывающего тега переиспользуется
|
||||
$closeAt = $configContent.LastIndexOf("</ChildObjects>", $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
|
||||
$newContent = $configContent.Substring(0, $closeAt) + "`t$entry$eol`t`t" + $configContent.Substring($closeAt)
|
||||
}
|
||||
}
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-compile v1.99 — Compile 1C metadata object from JSON
|
||||
# meta-compile v1.109 — Compile 1C metadata object from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -497,6 +497,7 @@ object_type_synonyms = {
|
||||
'ВебСервис': 'WebService',
|
||||
'ОпределяемыйТип': 'DefinedType',
|
||||
'ФункциональнаяОпция': 'FunctionalOption',
|
||||
'ВнешнийИсточникДанных': 'ExternalDataSource',
|
||||
}
|
||||
|
||||
# Enum property value synonyms — model often gets these slightly wrong
|
||||
@@ -542,7 +543,9 @@ valid_enum_values = {
|
||||
'WriteMode': ['Independent', 'RecorderSubordinate'],
|
||||
'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'],
|
||||
'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'],
|
||||
'DataLockControlMode': ['Automatic', 'Managed'],
|
||||
# AutomaticAndManaged — только у внешнего источника данных и его таблиц: там режим может
|
||||
# решаться на уровне таблицы, у прочих объектов такого значения нет.
|
||||
'DataLockControlMode': ['Automatic', 'Managed', 'AutomaticAndManaged'],
|
||||
'FullTextSearch': ['Use', 'DontUse'],
|
||||
'DataHistory': ['Use', 'DontUse'],
|
||||
'DefaultPresentation': ['AsDescription', 'AsCode'],
|
||||
@@ -692,7 +695,7 @@ valid_types = [
|
||||
'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage',
|
||||
'CommonForm',
|
||||
'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference',
|
||||
'CommonPicture', 'CommonTemplate',
|
||||
'CommonPicture', 'CommonTemplate', 'ExternalDataSource',
|
||||
]
|
||||
# Регистр имени вида — как в PS (-contains регистронезависим): приводим к канону списка
|
||||
obj_type = next((t for t in valid_types if t.lower() == obj_type.lower()), obj_type)
|
||||
@@ -732,6 +735,10 @@ type_synonyms = {
|
||||
# ValueStorage / UUID — прощающий ввод (base64Binary / рус. форма → канон).
|
||||
'valuestorage': 'ValueStorage',
|
||||
'base64binary': 'ValueStorage',
|
||||
# ДвоичныеДанные — ОТДЕЛЬНЫЙ тип, не ХранилищеЗначения: платформа пишет его как
|
||||
# xs:base64Binary с квалификаторами. Встречается у полей внешних источников данных.
|
||||
'binarydata': 'BinaryData',
|
||||
'двоичныеданные': 'BinaryData',
|
||||
'хранилищезначений': 'ValueStorage',
|
||||
'хранилищезначения': 'ValueStorage',
|
||||
'uuid': 'UUID',
|
||||
@@ -745,6 +752,7 @@ type_synonyms = {
|
||||
'планвидоврасчётассылка': 'ChartOfCalculationTypesRef',
|
||||
'планвидоврасчетассылка': 'ChartOfCalculationTypesRef',
|
||||
'планобменассылка': 'ExchangePlanRef',
|
||||
'внешнийисточникданныхтаблицассылка': 'ExternalDataSourceTableRef',
|
||||
'бизнеспроцессссылка': 'BusinessProcessRef',
|
||||
'задачассылка': 'TaskRef',
|
||||
'определяемыйтип': 'DefinedType',
|
||||
@@ -923,6 +931,25 @@ def emit_type_content(indent, type_str):
|
||||
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|AnyRef|AnyIBRef)$', type_str):
|
||||
X(f'{indent}<v8:TypeSet>cfg:{type_str}</v8:TypeSet>')
|
||||
return
|
||||
# ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников).
|
||||
if re.match(r'^BinaryData(\(|$)', type_str, re.I):
|
||||
# BinaryData — безлимит (так платформа пишет поле внешнего источника: 4294967292/Fixed).
|
||||
# BinaryData(N) — переменной длины, BinaryData(N,fixed) — фиксированной.
|
||||
m_bin = re.match(r'^BinaryData(?:\((\d+)(?:,\s*(fixed|variable))?\))?$', type_str, re.I)
|
||||
if not m_bin:
|
||||
print(f"Неверный тип '{type_str}': ждётся BinaryData, BinaryData(Длина) или BinaryData(Длина,fixed|variable).", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
blen = m_bin.group(1) or '4294967292'
|
||||
if m_bin.group(2):
|
||||
ballowed = 'Fixed' if m_bin.group(2).lower() == 'fixed' else 'Variable'
|
||||
else:
|
||||
ballowed = 'Variable' if m_bin.group(1) else 'Fixed'
|
||||
X(f'{indent}<v8:Type>xs:base64Binary</v8:Type>')
|
||||
X(f'{indent}<v8:BinaryDataQualifiers>')
|
||||
X(f'{indent}\t<v8:Length>{blen}</v8:Length>')
|
||||
X(f'{indent}\t<v8:AllowedLength>{ballowed}</v8:AllowedLength>')
|
||||
X(f'{indent}</v8:BinaryDataQualifiers>')
|
||||
return
|
||||
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary).
|
||||
if type_str == 'ValueStorage':
|
||||
X(f'{indent}<v8:Type>v8:ValueStorage</v8:Type>')
|
||||
@@ -969,7 +996,9 @@ def emit_type_content(indent, type_str):
|
||||
# cfg_prefix = None означает «пишем файл, корень которого cfg НЕ объявляет»
|
||||
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
|
||||
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
|
||||
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$', type_str)
|
||||
# ExternalDataSourceTableRef — единственный ссылочный тип с ДВУМЯ частями после префикса
|
||||
# (Источник.Таблица), поэтому `(.+)$` здесь существенно.
|
||||
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef|ExternalDataSourceTableRef)\.(.+)$', type_str)
|
||||
if m:
|
||||
if cfg_prefix:
|
||||
X(f'{indent}<v8:Type>{cfg_prefix}:{type_str}</v8:Type>')
|
||||
@@ -1194,8 +1223,10 @@ def emit_fill_value(indent, type_str, spec, has_spec, type_empty=False):
|
||||
# 5. Attribute shorthand parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_type_str(obj):
|
||||
t = str(obj.get('valueType') or obj.get('type') or '')
|
||||
def build_type_str(obj, value_type_only=False):
|
||||
# value_type_only — для корневого определения объекта: там ключ type означает ВИД объекта
|
||||
# (Constant, Catalog, …), а не тип значения, и подхватывать его нельзя.
|
||||
t = str(obj.get('valueType') or ('' if value_type_only else obj.get('type')) or '')
|
||||
if t and '(' not in t:
|
||||
if t == 'String' and obj.get('length'):
|
||||
t = f"String({obj['length']})"
|
||||
@@ -1291,6 +1322,10 @@ def parse_attribute_shorthand(val):
|
||||
# Режим приведения типов измерения РС (формат 2.18). Ключ обязан доехать до эмиттера:
|
||||
# без него не-дефолтное значение (Deny / DeleteData) молча заменялось на TransformValues.
|
||||
'typeReductionMode': val.get('typeReductionMode'),
|
||||
# Поле внешнего источника данных (контекст eds-field).
|
||||
'nameInDataSource': str(val['nameInDataSource']) if val.get('nameInDataSource') else '',
|
||||
'readOnly': val.get('readOnly') is True,
|
||||
'allowNull': val.get('allowNull') is True,
|
||||
}
|
||||
|
||||
def parse_enum_value_shorthand(val):
|
||||
@@ -1427,6 +1462,11 @@ generated_types = {
|
||||
'DefinedType': [
|
||||
{'prefix': 'DefinedType', 'category': 'DefinedType'},
|
||||
],
|
||||
'ExternalDataSource': [
|
||||
{'prefix': 'ExternalDataSourceManager', 'category': 'Manager'},
|
||||
{'prefix': 'ExternalDataSourceTablesManager', 'category': 'TablesManager'},
|
||||
{'prefix': 'ExternalDataSourceCubesManager', 'category': 'CubesManager'},
|
||||
],
|
||||
'DocumentJournal': [
|
||||
{'prefix': 'DocumentJournalSelection', 'category': 'Selection'},
|
||||
{'prefix': 'DocumentJournalList', 'category': 'List'},
|
||||
@@ -2278,15 +2318,28 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
|
||||
if parsed.get('fillChecking'):
|
||||
fill_checking = parsed['fillChecking']
|
||||
X(f'{indent}\t\t<FillChecking>{fill_checking}</FillChecking>')
|
||||
X(f'{indent}\t\t<ChoiceFoldersAndItems>{parsed.get("choiceFoldersAndItems") or "Items"}</ChoiceFoldersAndItems>')
|
||||
# Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm
|
||||
# у него стоит ПОСЛЕ ChoiceHistoryOnInput, а не перед — порядок снят с выгрузки платформы.
|
||||
if context != 'eds-field':
|
||||
X(f'{indent}\t\t<ChoiceFoldersAndItems>{parsed.get("choiceFoldersAndItems") or "Items"}</ChoiceFoldersAndItems>')
|
||||
emit_choice_parameter_links(f'{indent}\t\t', parsed.get('choiceParameterLinks'))
|
||||
emit_choice_parameters(f'{indent}\t\t', parsed.get('choiceParameters'))
|
||||
X(f'{indent}\t\t<QuickChoice>{parsed.get("quickChoice") or "Auto"}</QuickChoice>')
|
||||
X(f'{indent}\t\t<CreateOnInput>{parsed.get("createOnInput") or "Auto"}</CreateOnInput>')
|
||||
X(f'{indent}\t\t<ChoiceForm>{esc_xml_text(str(parsed["choiceForm"]))}</ChoiceForm>' if parsed.get('choiceForm') else f'{indent}\t\t<ChoiceForm/>')
|
||||
emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType'))
|
||||
if context != 'eds-field':
|
||||
X(f'{indent}\t\t<ChoiceForm>{esc_xml_text(str(parsed["choiceForm"]))}</ChoiceForm>' if parsed.get('choiceForm') else f'{indent}\t\t<ChoiceForm/>')
|
||||
emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType'))
|
||||
chi = parsed.get('choiceHistoryOnInput') or 'Auto'
|
||||
X(f'{indent}\t\t<ChoiceHistoryOnInput>{chi}</ChoiceHistoryOnInput>')
|
||||
|
||||
if context == 'eds-field':
|
||||
X(f'{indent}\t\t<ChoiceForm>{esc_xml_text(str(parsed["choiceForm"]))}</ChoiceForm>' if parsed.get('choiceForm') else f'{indent}\t\t<ChoiceForm/>')
|
||||
nids = parsed.get('nameInDataSource') or parsed['name']
|
||||
X(f'{indent}\t\t<NameInDataSource>{esc_xml_text(str(nids))}</NameInDataSource>')
|
||||
ro = 'true' if (parsed.get('readOnly') is True or 'readonly' in parsed.get('flags', [])) else 'false'
|
||||
X(f'{indent}\t\t<ReadOnly>{ro}</ReadOnly>')
|
||||
an = 'true' if (parsed.get('allowNull') is True or 'nullable' in parsed.get('flags', [])) else 'false'
|
||||
X(f'{indent}\t\t<AllowNull>{an}</AllowNull>')
|
||||
# Измерение регистра сведений: Master/MainFilter/DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing).
|
||||
if elem_tag == 'Dimension' and context == 'register-info':
|
||||
master = 'true' if (parsed.get('master') is True or 'master' in parsed.get('flags', [])) else 'false'
|
||||
@@ -2334,7 +2387,8 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
|
||||
use_value = parsed.get("use") or "ForItem"
|
||||
if context == 'catalog':
|
||||
X(f'{indent}\t\t<Use>{use_value}</Use>')
|
||||
if context not in ('processor', 'processor-tabular'):
|
||||
# и не для полей внешнего источника: индексами и полнотекстовым поиском чужой таблицы 1С не владеет.
|
||||
if context not in ('processor', 'processor-tabular', 'eds-field'):
|
||||
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
|
||||
if context != 'account-flag':
|
||||
# Ресурс регистра накопления НЕ имеет <Indexing> (только <FullTextSearch>); измерение/реквизит — имеют.
|
||||
@@ -2869,9 +2923,8 @@ def emit_constant_properties(indent):
|
||||
else:
|
||||
X(f'{i}<Comment/>')
|
||||
# Type — valueType (явный '' → <Type/>, реквизит без типа; отсутствие → String дефолт).
|
||||
value_type = build_type_str(defn)
|
||||
type_empty = (defn.get('valueType') is not None and str(defn.get('valueType')).strip() == '') or \
|
||||
(defn.get('type') is not None and str(defn.get('type')).strip() == '')
|
||||
value_type = build_type_str(defn, value_type_only=True)
|
||||
type_empty = defn.get('valueType') is not None and str(defn.get('valueType')).strip() == ''
|
||||
if type_empty:
|
||||
X(f'{i}<Type/>')
|
||||
else:
|
||||
@@ -4307,6 +4360,248 @@ def emit_addressing_attribute(indent, addr_def):
|
||||
parsed = parse_attribute_shorthand(addr_def)
|
||||
emit_attribute(indent, parsed, 'task-addressing', 'AddressingAttribute')
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 13h. Внешние источники данных
|
||||
# ---------------------------------------------------------------------------
|
||||
# Источник пишется в один файл, каждая его таблица — в свой. Функции живут ВНУТРИ файла источника
|
||||
# полными узлами, наравне со списком имён таблиц: так их выгружает платформа.
|
||||
|
||||
def get_eds_tables(val):
|
||||
"""Таблицы: dict имя → массив полей ЛИБО объект со свойствами и ключом fields/columns."""
|
||||
tables = {}
|
||||
if not val:
|
||||
return tables
|
||||
|
||||
def entry(v):
|
||||
if isinstance(v, list):
|
||||
return {'props': None, 'fields': list(v)}
|
||||
f = v.get('fields') if v.get('fields') is not None else v.get('columns')
|
||||
return {'props': v, 'fields': list(f) if f else []}
|
||||
|
||||
if isinstance(val, list):
|
||||
for t in val:
|
||||
tables[str(t.get('name'))] = entry(t)
|
||||
else:
|
||||
for k, v in val.items():
|
||||
tables[k] = entry(v)
|
||||
return tables
|
||||
|
||||
|
||||
def get_eds_field_ref(src_name, table_name, field_name):
|
||||
"""Ссылка на поле таблицы: в DSL короткое имя, в XML — полный путь."""
|
||||
if not field_name:
|
||||
return ''
|
||||
if str(field_name).startswith('ExternalDataSource.'):
|
||||
return str(field_name)
|
||||
return f'ExternalDataSource.{src_name}.Table.{table_name}.Field.{field_name}'
|
||||
|
||||
|
||||
def emit_eds_field_ref_list(indent, tag, names, src_name, table_name):
|
||||
items = [n for n in (names or []) if n]
|
||||
if not items:
|
||||
X(f'{indent}<{tag}/>')
|
||||
return
|
||||
X(f'{indent}<{tag}>')
|
||||
for n in items:
|
||||
X(f'{indent}\t<xr:Field>{esc_xml_text(get_eds_field_ref(src_name, table_name, n))}</xr:Field>')
|
||||
X(f'{indent}</{tag}>')
|
||||
|
||||
|
||||
def emit_eds_field_ref_scalar(indent, tag, name, src_name, table_name):
|
||||
if not name:
|
||||
X(f'{indent}<{tag}/>')
|
||||
return
|
||||
X(f'{indent}<{tag}>{esc_xml_text(get_eds_field_ref(src_name, table_name, name))}</{tag}>')
|
||||
|
||||
|
||||
def emit_external_data_source_properties(indent):
|
||||
i = indent
|
||||
X(f'{i}<Name>{esc_xml_text(obj_name)}</Name>')
|
||||
emit_mltext(i, 'Synonym', synonym)
|
||||
if defn.get('comment'):
|
||||
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>')
|
||||
else:
|
||||
X(f'{i}<Comment/>')
|
||||
dlcm = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
|
||||
X(f'{i}<DataLockControlMode>{dlcm}</DataLockControlMode>')
|
||||
|
||||
|
||||
def emit_eds_function(indent, fn_name, val, type_xml):
|
||||
"""Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо
|
||||
в выражении как &1, &2 (см. reference/external-data-source.md).
|
||||
|
||||
type_xml — уже собранный узел <Type> возвращаемого значения: его рендерит вызывающий навык
|
||||
своим эмиттером типов. Так тело функции не зависит от того, какой это навык."""
|
||||
fn_synonym = None
|
||||
fn_comment = ''
|
||||
returns = ''
|
||||
return_value = True
|
||||
if isinstance(val, str):
|
||||
expr = val
|
||||
else:
|
||||
expr = str(val.get('expression') or val.get('expressionInDataSource') or '')
|
||||
returns = str(val.get('returns') or val.get('returnType') or '')
|
||||
if val.get('returnValue') is not None:
|
||||
return_value = val.get('returnValue') is True
|
||||
fn_synonym = val.get('synonym')
|
||||
fn_comment = str(val['comment']) if val.get('comment') else ''
|
||||
if not expr:
|
||||
print(f"ERROR: Функция '{fn_name}' внешнего источника данных: не задано выражение (ключ expression).",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
X(f'{indent}<Function uuid="{new_uuid()}">')
|
||||
X(f'{indent}\t<Properties>')
|
||||
X(f'{indent}\t\t<Name>{esc_xml_text(fn_name)}</Name>')
|
||||
emit_mltext(f'{indent}\t\t', 'Synonym', fn_synonym)
|
||||
if fn_comment:
|
||||
X(f'{indent}\t\t<Comment>{esc_xml_text(fn_comment)}</Comment>')
|
||||
else:
|
||||
X(f'{indent}\t\t<Comment/>')
|
||||
X(f'{indent}\t\t<ReturnValue>{"true" if return_value else "false"}</ReturnValue>')
|
||||
if return_value and type_xml:
|
||||
X(type_xml.rstrip('\r\n'))
|
||||
else:
|
||||
X(f'{indent}\t\t<Type/>')
|
||||
X(f'{indent}\t\t<ExpressionInDataSource>{esc_xml_text(expr)}</ExpressionInDataSource>')
|
||||
X(f'{indent}\t</Properties>')
|
||||
X(f'{indent}</Function>')
|
||||
|
||||
|
||||
def emit_eds_table_properties(indent, src_name, table_name, t, char_xml, default_forms_xml):
|
||||
"""Свойства таблицы: 38 узлов в порядке выгрузки платформы.
|
||||
|
||||
char_xml и default_forms_xml — уже собранные блоки <Characteristics> и четыре слота
|
||||
<Default*Form>: их рендерит вызывающий навык своим эмиттером. Так тело не зависит
|
||||
от хелперов конкретного навыка и годится для копирования (check-inline-drift)."""
|
||||
i = indent
|
||||
t = t or {}
|
||||
tbl_synonym = t['synonym'] if t.get('synonym') is not None else split_camel_case(table_name)
|
||||
X(f'{i}<Name>{esc_xml_text(table_name)}</Name>')
|
||||
emit_mltext(i, 'Synonym', tbl_synonym)
|
||||
if t.get('comment'):
|
||||
X(f'{i}<Comment>{esc_xml_text(str(t["comment"]))}</Comment>')
|
||||
else:
|
||||
X(f'{i}<Comment/>')
|
||||
|
||||
table_type = str(t.get('tableType') or 'Table')
|
||||
X(f'{i}<TableType>{table_type}</TableType>')
|
||||
# Имя в источнике по умолчанию равно имени объекта — так поступает и платформа.
|
||||
if t.get('nameInDataSource'):
|
||||
nids = str(t['nameInDataSource'])
|
||||
elif table_type == 'Expression':
|
||||
nids = ''
|
||||
else:
|
||||
nids = table_name
|
||||
X(f'{i}<NameInDataSource>{esc_xml_text(nids)}</NameInDataSource>' if nids else f'{i}<NameInDataSource/>')
|
||||
expr = str(t.get('expressionInDataSource') or t.get('expression') or '')
|
||||
X(f'{i}<ExpressionInDataSource>{esc_xml_text(expr)}</ExpressionInDataSource>' if expr else f'{i}<ExpressionInDataSource/>')
|
||||
X(f'{i}<TableDataType>{t.get("tableDataType") or "NonobjectData"}</TableDataType>')
|
||||
|
||||
emit_eds_field_ref_list(i, 'KeyFields', t.get('keyFields'), src_name, table_name)
|
||||
emit_eds_field_ref_scalar(i, 'PresentationField', t.get('presentationField'), src_name, table_name)
|
||||
emit_eds_field_ref_scalar(i, 'ParentField', t.get('parentField'), src_name, table_name)
|
||||
# Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения»
|
||||
# различаются формой самого значения (xsi:nil против типизированного).
|
||||
# ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено
|
||||
# на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы
|
||||
# с полем родителя — пустая строка (как после загрузки), а без него — nil.
|
||||
if t.get('parentField'):
|
||||
X(f'{i}<UnfilledParentValue xsi:type="xs:string"/>')
|
||||
else:
|
||||
X(f'{i}<UnfilledParentValue xsi:nil="true"/>')
|
||||
if char_xml:
|
||||
X(char_xml.rstrip('\r\n'))
|
||||
else:
|
||||
X(f'{i}<Characteristics/>')
|
||||
|
||||
X(f'{i}<UseStandardCommands>{"false" if t.get("useStandardCommands") is False else "true"}</UseStandardCommands>')
|
||||
X(f'{i}<QuickChoice>{"true" if t.get("quickChoice") is True else "false"}</QuickChoice>')
|
||||
# Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке).
|
||||
# Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка.
|
||||
if 'inputByString' in t:
|
||||
ibs = t.get('inputByString')
|
||||
elif t.get('presentationField'):
|
||||
ibs = [t['presentationField']]
|
||||
else:
|
||||
ibs = None
|
||||
emit_eds_field_ref_list(i, 'InputByString', ibs, src_name, table_name)
|
||||
X(f'{i}<CreateOnInput>{t.get("createOnInput") or "Auto"}</CreateOnInput>')
|
||||
X(f'{i}<SearchStringModeOnInputByString>{t.get("searchStringModeOnInputByString") or "Begin"}</SearchStringModeOnInputByString>')
|
||||
X(f'{i}<ChoiceDataGetModeOnInputByString>{t.get("choiceDataGetModeOnInputByString") or "Directly"}</ChoiceDataGetModeOnInputByString>')
|
||||
X(f'{i}<ChoiceHistoryOnInput>{t.get("choiceHistoryOnInput") or "Auto"}</ChoiceHistoryOnInput>')
|
||||
|
||||
# Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38.
|
||||
if default_forms_xml:
|
||||
X(default_forms_xml.rstrip('\r\n'))
|
||||
else:
|
||||
for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'):
|
||||
X(f'{i}<{form_tag}/>')
|
||||
for pres_tag in ('ObjectPresentation', 'ExtendedObjectPresentation', 'RecordPresentation',
|
||||
'ExtendedRecordPresentation', 'ListPresentation', 'ExtendedListPresentation', 'Explanation'):
|
||||
key = pres_tag[0].lower() + pres_tag[1:]
|
||||
emit_mltext(i, pres_tag, t.get(key))
|
||||
X(f'{i}<IncludeHelpInContents>{"true" if t.get("includeHelpInContents") is True else "false"}</IncludeHelpInContents>')
|
||||
X(f'{i}<ReadOnly>{"true" if t.get("readOnly") is True else "false"}</ReadOnly>')
|
||||
X(f'{i}<TransactionsIsolationLevel>{t.get("transactionsIsolationLevel") or "Auto"}</TransactionsIsolationLevel>')
|
||||
emit_eds_field_ref_scalar(i, 'DataVersionField', t.get('dataVersionField'), src_name, table_name)
|
||||
X(f'{i}<EditType>{t.get("editType") or "InDialog"}</EditType>')
|
||||
emit_md_ref_list(i, 'BasedOn', t.get('basedOn'))
|
||||
emit_eds_field_ref_list(i, 'DataLockFields', t.get('dataLockFields'), src_name, table_name)
|
||||
X(f'{i}<DataLockControlMode>{t.get("dataLockControlMode") or "Automatic"}</DataLockControlMode>')
|
||||
|
||||
|
||||
EDS_TABLE_GENERATED_TYPES = (
|
||||
('ExternalDataSourceTableManager', 'Manager'),
|
||||
('ExternalDataSourceTableObject', 'Object'),
|
||||
('ExternalDataSourceTableRef', 'Ref'),
|
||||
('ExternalDataSourceTableList', 'List'),
|
||||
('ExternalDataSourceTableRecord', 'Record'),
|
||||
('ExternalDataSourceTableRecordSet', 'RecordSet'),
|
||||
('ExternalDataSourceTableRecordKey', 'RecordKey'),
|
||||
('ExternalDataSourceTableRecordManager', 'RecordManager'),
|
||||
)
|
||||
|
||||
|
||||
def build_eds_table_xml(src_name, table_name, entry, fields_xml, char_xml, default_forms_xml):
|
||||
"""Отдельный XML-документ таблицы. fields_xml, char_xml, default_forms_xml — уже собранные
|
||||
узлы: их рендерит вызывающий навык своими эмиттерами, поэтому тело не зависит от того,
|
||||
какой это навык.
|
||||
Возвращает строку: X пишет в общий список строк,
|
||||
поэтому «перехват» — запомнить длину, отдать эмиттерам, срезать добавленное
|
||||
(в ps1-порте тот же приём выражен через StringBuilder — различие рантаймов, не логики)."""
|
||||
before = len(lines)
|
||||
|
||||
X('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
X(f'<MetaDataObject {xmlns_decl} version="{format_version}">')
|
||||
X(f'\t<Table uuid="{new_uuid()}">')
|
||||
# InternalInfo у таблицы эмитится здесь, а не через generated_types: имя элемента
|
||||
# трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает.
|
||||
X('\t\t<InternalInfo>')
|
||||
for prefix, category in EDS_TABLE_GENERATED_TYPES:
|
||||
X(f'\t\t\t<xr:GeneratedType name="{prefix}.{src_name}.{table_name}" category="{category}">')
|
||||
X(f'\t\t\t\t<xr:TypeId>{new_uuid()}</xr:TypeId>')
|
||||
X(f'\t\t\t\t<xr:ValueId>{new_uuid()}</xr:ValueId>')
|
||||
X('\t\t\t</xr:GeneratedType>')
|
||||
X('\t\t</InternalInfo>')
|
||||
|
||||
X('\t\t<Properties>')
|
||||
emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props'], char_xml, default_forms_xml)
|
||||
X('\t\t</Properties>')
|
||||
|
||||
if fields_xml:
|
||||
X('\t\t<ChildObjects>')
|
||||
X(fields_xml.rstrip('\r\n'))
|
||||
X('\t\t</ChildObjects>')
|
||||
else:
|
||||
X('\t\t<ChildObjects/>')
|
||||
X('\t</Table>')
|
||||
X('</MetaDataObject>')
|
||||
|
||||
chunk = '\r\n'.join(lines[before:])
|
||||
del lines[before:]
|
||||
return chunk
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 14. Namespaces
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -4447,6 +4742,7 @@ property_emitters = {
|
||||
'Task': emit_task_properties,
|
||||
'HTTPService': emit_http_service_properties,
|
||||
'WebService': emit_web_service_properties,
|
||||
'ExternalDataSource': emit_external_data_source_properties,
|
||||
}
|
||||
|
||||
property_emitters[obj_type]('\t\t\t')
|
||||
@@ -4720,6 +5016,36 @@ if obj_type == 'WebService':
|
||||
else:
|
||||
X('\t\t<ChildObjects/>')
|
||||
|
||||
# --- ExternalDataSource: Tables (именами) + Functions (полными узлами) ---
|
||||
eds_tables = {}
|
||||
if obj_type == 'ExternalDataSource':
|
||||
eds_tables = get_eds_tables(defn.get('tables'))
|
||||
functions = {}
|
||||
if defn.get('functions'):
|
||||
for k, v in defn['functions'].items():
|
||||
functions[k] = v
|
||||
if eds_tables or functions:
|
||||
has_children = True
|
||||
X('\t\t<ChildObjects>')
|
||||
for tbl_name in eds_tables:
|
||||
X(f'\t\t\t<Table>{esc_xml_text(tbl_name)}</Table>')
|
||||
for fn_name, fn_val in functions.items():
|
||||
if isinstance(fn_val, str):
|
||||
fn_returns, fn_no_value = 'String', False
|
||||
else:
|
||||
fn_returns = str(fn_val.get('returns') or fn_val.get('returnType') or 'String')
|
||||
fn_no_value = fn_val.get('returnValue') is not None and fn_val.get('returnValue') is not True
|
||||
fn_type_xml = ''
|
||||
if not fn_no_value:
|
||||
type_before = len(lines)
|
||||
emit_value_type('\t\t\t\t\t', fn_returns)
|
||||
fn_type_xml = '\r\n'.join(lines[type_before:])
|
||||
del lines[type_before:]
|
||||
emit_eds_function('\t\t\t', fn_name, fn_val, fn_type_xml)
|
||||
X('\t\t</ChildObjects>')
|
||||
else:
|
||||
X('\t\t<ChildObjects/>')
|
||||
|
||||
# --- CommonModule: no ChildObjects ---
|
||||
|
||||
X(f'\t</{obj_type}>')
|
||||
@@ -4769,6 +5095,7 @@ type_plural_map = {
|
||||
'WSReference': 'WSReferences',
|
||||
'CommonPicture': 'CommonPictures',
|
||||
'CommonTemplate': 'CommonTemplates',
|
||||
'ExternalDataSource': 'ExternalDataSources',
|
||||
}
|
||||
|
||||
type_plural = type_plural_map[obj_type]
|
||||
@@ -4787,8 +5114,50 @@ os.makedirs(type_dir, exist_ok=True)
|
||||
if obj_type not in types_no_sub_dir:
|
||||
os.makedirs(obj_sub_dir, exist_ok=True)
|
||||
|
||||
# Объект с таким именем уже есть: компиляция заменит его файл ЦЕЛИКОМ и выдаст новый uuid —
|
||||
# ссылки на прежний объект (из кода, состава подсистем, типов реквизитов) станут висячими.
|
||||
# Для доработки существующего объекта есть meta-edit; молчать об этом нельзя.
|
||||
if os.path.exists(main_xml_path):
|
||||
print(f"WARNING: {obj_type} '{obj_name}' уже существует ({type_plural}/{obj_name}.xml) — файл будет перезаписан, объект получит НОВЫЙ uuid, ссылки на прежний сломаются. Для правки существующего объекта используйте meta-edit.", file=sys.stderr)
|
||||
|
||||
write_xml_file_keep_eol(main_xml_path, metadata_xml)
|
||||
|
||||
# Таблицы внешнего источника — отдельными файлами в <Источник>/Tables/.
|
||||
# Единственный вид, у которого объект складывается более чем из одного XML.
|
||||
eds_tables_created = []
|
||||
if obj_type == 'ExternalDataSource' and eds_tables:
|
||||
tables_dir = os.path.join(obj_sub_dir, 'Tables')
|
||||
os.makedirs(tables_dir, exist_ok=True)
|
||||
for tbl_name in eds_tables:
|
||||
entry = eds_tables[tbl_name]
|
||||
fields_before = len(lines)
|
||||
for f in entry['fields']:
|
||||
emit_attribute('\t\t\t', parse_attribute_shorthand(f), 'eds-field', 'Field')
|
||||
fields_xml = '\r\n'.join(lines[fields_before:])
|
||||
del lines[fields_before:]
|
||||
tp = entry['props'] or {}
|
||||
char_before = len(lines)
|
||||
emit_characteristics('\t\t\t', tp.get('characteristics'))
|
||||
char_xml = '\r\n'.join(lines[char_before:])
|
||||
del lines[char_before:]
|
||||
|
||||
# Слот формы: короткое имя разворачивается в полный путь таблицы внешнего источника —
|
||||
# голое имя платформа отвергает («Неизвестный объект метаданных»).
|
||||
forms_before = len(lines)
|
||||
for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'):
|
||||
key = form_tag[0].lower() + form_tag[1:]
|
||||
form_val = tp.get(key)
|
||||
if form_val and '.' not in str(form_val):
|
||||
form_val = f'ExternalDataSource.{obj_name}.Table.{tbl_name}.Form.{form_val}'
|
||||
emit_form_ref('\t\t\t', form_tag, form_val)
|
||||
default_forms_xml = '\r\n'.join(lines[forms_before:])
|
||||
del lines[forms_before:]
|
||||
|
||||
table_xml = build_eds_table_xml(obj_name, tbl_name, entry, fields_xml, char_xml, default_forms_xml)
|
||||
table_path = os.path.join(tables_dir, f'{tbl_name}.xml')
|
||||
write_xml_file_keep_eol(table_path, table_xml)
|
||||
eds_tables_created.append(table_path)
|
||||
|
||||
# Module files
|
||||
modules_created = []
|
||||
|
||||
@@ -5209,10 +5578,106 @@ if commands:
|
||||
# 17. Register in Configuration.xml
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_new_object_position(cfg_dir):
|
||||
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
|
||||
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
остаётся рабочим каталогом проекта.
|
||||
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||
if not pj:
|
||||
return "end"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
proj_dir = os.path.dirname(pj)
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src and db.get("newObjectPosition"):
|
||||
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||
return "byName"
|
||||
return "end"
|
||||
except Exception:
|
||||
return "end"
|
||||
|
||||
|
||||
def is_order_sensitive_type(type_name):
|
||||
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
|
||||
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
Явно названный вид сортируется в любом случае.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||
|
||||
|
||||
def compare_metadata_names(a, b):
|
||||
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
|
||||
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
keys = []
|
||||
for name in (a, b):
|
||||
parts = []
|
||||
for ch in name.lower():
|
||||
if ch == "ё":
|
||||
ch = "е"
|
||||
if ch.isdigit():
|
||||
parts.append("1" + ch)
|
||||
elif ch.isalpha():
|
||||
parts.append("2" + ch)
|
||||
else:
|
||||
parts.append("0" + ch)
|
||||
keys.append("".join(parts))
|
||||
if keys[0] != keys[1]:
|
||||
return -1 if keys[0] < keys[1] else 1
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
return 0
|
||||
|
||||
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
|
||||
]
|
||||
|
||||
|
||||
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
|
||||
"""Регистрация объекта в <ChildObjects> родительского XML.
|
||||
|
||||
Общая реализация: эталон — meta-compile, копия — role-compile.
|
||||
Общая реализация: эталон — meta-compile, копии — role-compile, xdto-compile.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
Возвращает исход: added | already | no-childobj | no-config.
|
||||
"""
|
||||
@@ -5263,6 +5728,19 @@ def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name)
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
# byName: перед первым объектом того же вида, чьё имя больше нового.
|
||||
# Виды с осмысленным порядком в дереве пропускаем — см. is_order_sensitive_type.
|
||||
if (not is_order_sensitive_type(child_tag)
|
||||
and get_new_object_position(os.path.dirname(os.path.abspath(parent_xml_path))) == 'byName'):
|
||||
line_rx = re.compile(rf'(?m)^([ \t]*)<{child_tag}>([^<]*)</{child_tag}>')
|
||||
for m in line_rx.finditer(config_content, block.start(), block.end()):
|
||||
if compare_metadata_names(m.group(2), child_name) > 0:
|
||||
new_content = (config_content[:m.start()]
|
||||
+ f'{m.group(1)}{entry}{eol}'
|
||||
+ config_content[m.start():])
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
close_same = f'</{child_tag}>'
|
||||
last_same = config_content.rfind(close_same, block.start(), block.end())
|
||||
if last_same != -1:
|
||||
@@ -5272,12 +5750,29 @@ def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name)
|
||||
+ f'{eol}\t\t\t{entry}'
|
||||
+ config_content[insert_at:])
|
||||
else:
|
||||
# No element of this type yet: new line before </ChildObjects>,
|
||||
# reusing the block's existing closing indent for </ChildObjects>.
|
||||
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
|
||||
new_content = (config_content[:close_at]
|
||||
+ f'\t{entry}{eol}\t\t'
|
||||
+ config_content[close_at:])
|
||||
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
|
||||
# группой вида старше по CHILD_OBJECT_TYPES. Дописать в конец блока нельзя: платформа
|
||||
# переставит группу при первой же выгрузке и даст диф на ровном месте.
|
||||
anchor = None
|
||||
if child_tag in CHILD_OBJECT_TYPES:
|
||||
own_idx = CHILD_OBJECT_TYPES.index(child_tag)
|
||||
type_rx = re.compile(r'(?m)^([ \t]*)<(\w+)>[^<]*</\2>')
|
||||
for m in type_rx.finditer(config_content, block.start(), block.end()):
|
||||
other = m.group(2)
|
||||
if other in CHILD_OBJECT_TYPES and CHILD_OBJECT_TYPES.index(other) > own_idx:
|
||||
anchor = m
|
||||
break
|
||||
if anchor is not None:
|
||||
new_content = (config_content[:anchor.start()]
|
||||
+ f'{anchor.group(1)}{entry}{eol}'
|
||||
+ config_content[anchor.start():])
|
||||
else:
|
||||
# Видов старше в файле нет — новая строка перед </ChildObjects>,
|
||||
# отступ закрывающего тега переиспользуется.
|
||||
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
|
||||
new_content = (config_content[:close_at]
|
||||
+ f'\t{entry}{eol}\t\t'
|
||||
+ config_content[close_at:])
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-decompile v0.65 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# meta-decompile v0.69 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
#
|
||||
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
|
||||
@@ -93,7 +93,7 @@ foreach ($c in $rootEl.ChildNodes) { if ($c.NodeType -eq 'Element') { $objNode =
|
||||
if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 }
|
||||
$objType = $objNode.LocalName
|
||||
|
||||
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService')) {
|
||||
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService', 'ExternalDataSource')) {
|
||||
[Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3
|
||||
}
|
||||
|
||||
@@ -206,7 +206,24 @@ function Get-TypeShorthand {
|
||||
if ($dq) { $dn = $dq.SelectSingleNode('v8:DateFractions', $nsm); if ($dn) { $fr = $dn.InnerText } }
|
||||
$parts += $fr; break # Date | DateTime
|
||||
}
|
||||
'(^|:)base64Binary$' { $parts += 'ValueStorage'; break }
|
||||
'(^|:)base64Binary$' {
|
||||
# xs:base64Binary — всегда ДвоичныеДанные (ХранилищеЗначения — это v8:ValueStorage).
|
||||
# Узел без квалификаторов встречается только в рукописном XML: замерено на 8.3.24.1691 —
|
||||
# платформа читает его как безлимит (Length 0, Variable) и так же выгружает обратно.
|
||||
$bq = $typeNode.SelectSingleNode('v8:BinaryDataQualifiers', $nsm)
|
||||
if ($bq) {
|
||||
$blen = $bq.SelectSingleNode('v8:Length', $nsm)
|
||||
$bal = $bq.SelectSingleNode('v8:AllowedLength', $nsm)
|
||||
$blenVal = if ($blen) { $blen.InnerText.Trim() } else { "" }
|
||||
$balVal = if ($bal) { $bal.InnerText.Trim() } else { "" }
|
||||
# Голым BinaryData сворачиваем ТОЛЬКО точный дефолт компилятора
|
||||
# (4294967292/Fixed), иначе фиксированная длина терялась на раундтрипе.
|
||||
if ($balVal -eq 'Variable' -and $blenVal) { $parts += "BinaryData($blenVal)" }
|
||||
elseif ($blenVal -and $blenVal -ne '4294967292') { $parts += "BinaryData($blenVal,fixed)" }
|
||||
else { $parts += 'BinaryData' }
|
||||
} else { $parts += 'BinaryData(0)' }
|
||||
break
|
||||
}
|
||||
default { $parts += (Strip-NsPrefix $raw) } # cfg:CatalogRef.X → CatalogRef.X
|
||||
}
|
||||
} elseif ($ln -eq 'TypeSet') {
|
||||
@@ -288,12 +305,19 @@ function Attr-ToDsl {
|
||||
param($attrNode)
|
||||
$ap = $attrNode.SelectSingleNode('md:Properties', $nsm)
|
||||
$nm = ($ap.SelectSingleNode('md:Name', $nsm)).InnerText
|
||||
# Поле внешнего источника: три своих свойства. Имя колонки по умолчанию равно имени поля,
|
||||
# поэтому в DSL попадает только отличающееся.
|
||||
$edsNids = $ap.SelectSingleNode('md:NameInDataSource', $nsm)
|
||||
$edsRo = $ap.SelectSingleNode('md:ReadOnly', $nsm)
|
||||
$edsNull = $ap.SelectSingleNode('md:AllowNull', $nsm)
|
||||
$ts = Get-TypeShorthand ($ap.SelectSingleNode('md:Type', $nsm))
|
||||
$flags = @()
|
||||
$fc = $ap.SelectSingleNode('md:FillChecking', $nsm); if ($fc -and $fc.InnerText -eq 'ShowError') { $flags += 'req' }
|
||||
$ix = $ap.SelectSingleNode('md:Indexing', $nsm)
|
||||
if ($ix) { if ($ix.InnerText -eq 'Index') { $flags += 'index' } elseif ($ix.InnerText -eq 'IndexWithAdditionalOrder') { $flags += 'indexAdditional' } }
|
||||
$ml = $ap.SelectSingleNode('md:MultiLine', $nsm); if ($ml -and $ml.InnerText -eq 'true') { $flags += 'multiline' }
|
||||
if ($edsRo -and $edsRo.InnerText -eq 'true') { $flags += 'readonly' }
|
||||
if ($edsNull -and $edsNull.InnerText -eq 'true') { $flags += 'nullable' }
|
||||
|
||||
# Синоним/подсказка (строка ru-only ИЛИ {ru,en}).
|
||||
$synNode = $ap.SelectSingleNode('md:Synonym', $nsm)
|
||||
@@ -407,6 +431,7 @@ function Attr-ToDsl {
|
||||
# Пустой <Type/> (реквизит без типа / произвольный) → $ts=''. Отличаем от «дефолтного» отсутствия:
|
||||
# заставляем объектную форму с явным type:'' (компилятор без маркера подставил бы xs:string).
|
||||
$typeEmpty = ($ts -eq '')
|
||||
if ($edsNids -and $edsNids.InnerText -and $edsNids.InnerText -cne $nm) { $extra['nameInDataSource'] = $edsNids.InnerText }
|
||||
if ($synCustom -or $synEmpty -or ($null -ne $ttVal) -or $extra.Count -gt 0 -or $typeEmpty) {
|
||||
$o = [ordered]@{ name = $nm }
|
||||
if ($ts) { $o['type'] = $ts } elseif ($typeEmpty) { $o['type'] = '' }
|
||||
@@ -1658,6 +1683,122 @@ if ($objType -eq 'ExchangePlan') {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Внешний источник данных: таблицы (отдельные файлы) и функции (узлы внутри файла) ---
|
||||
if ($objType -eq 'ExternalDataSource') {
|
||||
$dlcmVal = P 'DataLockControlMode'
|
||||
if ($dlcmVal -and $dlcmVal -cne 'Automatic') { $dsl['dataLockControlMode'] = $dlcmVal }
|
||||
|
||||
# Короткое имя из полного пути ExternalDataSource.И.Table.Т.Field.П
|
||||
function Short-FieldRef { param([string]$ref) if ($ref) { return ($ref -split '\.')[-1] } else { return $null } }
|
||||
function Field-RefList { param($parent, [string]$tag)
|
||||
$out = [System.Collections.ArrayList]@()
|
||||
foreach ($f in @($parent.SelectNodes("md:$tag/xr:Field", $nsm))) { [void]$out.Add((Short-FieldRef $f.InnerText)) }
|
||||
# Запятая обязательна: return разворачивает коллекцию из одного элемента в скаляр,
|
||||
# и список ключевых полей из одного поля уехал бы в JSON строкой вместо массива.
|
||||
return ,$out
|
||||
}
|
||||
|
||||
$srcDir = Join-Path (Split-Path -Parent (Resolve-Path -LiteralPath $ObjectPath).Path) $objName
|
||||
$childObjsEds = $objNode.SelectSingleNode('md:ChildObjects', $nsm)
|
||||
if ($childObjsEds) {
|
||||
$tablesMap = [ordered]@{}
|
||||
foreach ($tNode in @($childObjsEds.SelectNodes('md:Table', $nsm))) {
|
||||
$tblName = $tNode.InnerText.Trim()
|
||||
$tblPath = Join-Path (Join-Path $srcDir 'Tables') "$tblName.xml"
|
||||
if (-not (Test-Path -LiteralPath $tblPath)) {
|
||||
[Console]::Error.WriteLine("meta-decompile: файл таблицы не найден: $tblPath")
|
||||
continue
|
||||
}
|
||||
$tdoc = New-Object System.Xml.XmlDocument
|
||||
$tdoc.PreserveWhitespace = $true
|
||||
$tdoc.Load($tblPath)
|
||||
$tObjNode = $null
|
||||
foreach ($c in $tdoc.DocumentElement.ChildNodes) { if ($c.NodeType -eq 'Element') { $tObjNode = $c; break } }
|
||||
$tp = $tObjNode.SelectSingleNode('md:Properties', $nsm)
|
||||
function TP { param([string]$tag) $n = $tp.SelectSingleNode("md:$tag", $nsm); if ($n) { return $n.InnerText } else { return $null } }
|
||||
|
||||
$tbl = [ordered]@{}
|
||||
$tSynNode = $tp.SelectSingleNode('md:Synonym', $nsm)
|
||||
$tSyn = Get-MLValue $tSynNode
|
||||
if ($tSyn -is [string]) { if ($tSyn -cne (Split-CamelWords $tblName)) { $tbl['synonym'] = $tSyn } }
|
||||
elseif ($null -ne $tSyn) { $tbl['synonym'] = $tSyn }
|
||||
# Пустой <Synonym/> ≠ авто-синоним из имени: без явного '' компилятор до-генерит его из имени.
|
||||
elseif ($tSynNode) { $tbl['synonym'] = '' }
|
||||
$tCmt = TP 'Comment'; if ($tCmt) { $tbl['comment'] = $tCmt }
|
||||
$tType = TP 'TableType'; if ($tType -and $tType -cne 'Table') { $tbl['tableType'] = $tType }
|
||||
$nids = TP 'NameInDataSource'; if ($nids -and $nids -cne $tblName) { $tbl['nameInDataSource'] = $nids }
|
||||
$expr = TP 'ExpressionInDataSource'; if ($expr) { $tbl['expressionInDataSource'] = $expr }
|
||||
$tdt = TP 'TableDataType'; if ($tdt -and $tdt -cne 'NonobjectData') { $tbl['tableDataType'] = $tdt }
|
||||
$keys = Field-RefList $tp 'KeyFields'; if ($keys.Count -gt 0) { $tbl['keyFields'] = $keys }
|
||||
foreach ($pair in @(@('PresentationField','presentationField'), @('ParentField','parentField'), @('DataVersionField','dataVersionField'))) {
|
||||
$v = Short-FieldRef (TP $pair[0]); if ($v) { $tbl[$pair[1]] = $v }
|
||||
}
|
||||
$ibs = Field-RefList $tp 'InputByString'
|
||||
# Ввод по строке компилятор выводит из поля представления: совпадающий список не пишем.
|
||||
$ibsAuto = if ($tbl['presentationField']) { @($tbl['presentationField']) } else { @() }
|
||||
if (($ibs -join ',') -cne ($ibsAuto -join ',')) { $tbl['inputByString'] = $ibs }
|
||||
$dlf = Field-RefList $tp 'DataLockFields'; if ($dlf.Count -gt 0) { $tbl['dataLockFields'] = $dlf }
|
||||
if ((TP 'ReadOnly') -eq 'true') { $tbl['readOnly'] = $true }
|
||||
$til = TP 'TransactionsIsolationLevel'; if ($til -and $til -cne 'Auto') { $tbl['transactionsIsolationLevel'] = $til }
|
||||
$tdlcm = TP 'DataLockControlMode'; if ($tdlcm -and $tdlcm -cne 'Automatic') { $tbl['dataLockControlMode'] = $tdlcm }
|
||||
if ((TP 'UseStandardCommands') -eq 'false') { $tbl['useStandardCommands'] = $false }
|
||||
if ((TP 'QuickChoice') -eq 'true') { $tbl['quickChoice'] = $true }
|
||||
$tet = TP 'EditType'; if ($tet -and $tet -cne 'InDialog') { $tbl['editType'] = $tet }
|
||||
# Слоты форм — такая же часть свойств таблицы, как у прочих объектов (сами формы
|
||||
# вне скоупа раундтрипа: это отдельные файлы, их делает навык form-add).
|
||||
foreach ($fp in @(@('DefaultObjectForm','defaultObjectForm'), @('DefaultRecordForm','defaultRecordForm'),
|
||||
@('DefaultListForm','defaultListForm'), @('DefaultChoiceForm','defaultChoiceForm'))) {
|
||||
$fv = TP $fp[0]; if ($fv) { $tbl[$fp[1]] = $fv }
|
||||
}
|
||||
$basedOn = [System.Collections.ArrayList]@()
|
||||
foreach ($it in @($tp.SelectNodes('md:BasedOn/xr:Item', $nsm))) { [void]$basedOn.Add($it.InnerText) }
|
||||
if ($basedOn.Count -gt 0) { $tbl['basedOn'] = $basedOn }
|
||||
|
||||
$fieldsArr = [System.Collections.ArrayList]@()
|
||||
$tChild = $tObjNode.SelectSingleNode('md:ChildObjects', $nsm)
|
||||
if ($tChild) {
|
||||
foreach ($f in @($tChild.SelectNodes('md:Field', $nsm))) { [void]$fieldsArr.Add((Attr-ToDsl $f)) }
|
||||
}
|
||||
# Таблица без собственных свойств — короткая форма: просто массив полей.
|
||||
if ($tbl.Count -eq 0) { $tablesMap[$tblName] = $fieldsArr }
|
||||
else { $tbl['fields'] = $fieldsArr; $tablesMap[$tblName] = $tbl }
|
||||
}
|
||||
if ($tablesMap.Count -gt 0) { $dsl['tables'] = $tablesMap }
|
||||
|
||||
$fnMap = [ordered]@{}
|
||||
foreach ($fnNode in @($childObjsEds.SelectNodes('md:Function', $nsm))) {
|
||||
$fp = $fnNode.SelectSingleNode('md:Properties', $nsm)
|
||||
$fnName = ($fp.SelectSingleNode('md:Name', $nsm)).InnerText
|
||||
$fnExprNode = $fp.SelectSingleNode('md:ExpressionInDataSource', $nsm)
|
||||
$fnExpr = if ($fnExprNode) { $fnExprNode.InnerText } else { '' }
|
||||
$fnRetNode = $fp.SelectSingleNode('md:ReturnValue', $nsm)
|
||||
$fnReturns = Get-TypeShorthand ($fp.SelectSingleNode('md:Type', $nsm))
|
||||
$fnSyn = Get-MLValue ($fp.SelectSingleNode('md:Synonym', $nsm))
|
||||
$fnCmtNode = $fp.SelectSingleNode('md:Comment', $nsm)
|
||||
$fnCmt = if ($fnCmtNode) { $fnCmtNode.InnerText } else { '' }
|
||||
$fnNoValue = ($fnRetNode -and $fnRetNode.InnerText -eq 'false')
|
||||
$synCustomFn = ($fnSyn -isnot [string]) -and ($null -ne $fnSyn)
|
||||
if ($fnSyn -is [string]) { $synCustomFn = ($fnSyn -cne (Split-CamelWords $fnName)) -and ($fnSyn -ne '') }
|
||||
# Умолчание `returns` компилятора — String, а он даёт String(10): с ним и сверяем,
|
||||
# иначе короткая форма (одна строка выражения) не срабатывала бы никогда.
|
||||
if (-not $fnNoValue -and -not $fnCmt -and -not $synCustomFn -and ($fnReturns -cne 'String(10)')) {
|
||||
$fo = [ordered]@{ expression = $fnExpr; returns = $fnReturns }
|
||||
$fnMap[$fnName] = $fo
|
||||
} elseif (-not $fnNoValue -and -not $fnCmt -and -not $synCustomFn) {
|
||||
# Тип по умолчанию String — короткая форма: одна строка выражения.
|
||||
$fnMap[$fnName] = $fnExpr
|
||||
} else {
|
||||
$fo = [ordered]@{ expression = $fnExpr }
|
||||
if ($fnNoValue) { $fo['returnValue'] = $false } elseif ($fnReturns) { $fo['returns'] = $fnReturns }
|
||||
if ($synCustomFn) { $fo['synonym'] = $fnSyn }
|
||||
if ($fnCmt) { $fo['comment'] = $fnCmt }
|
||||
$fnMap[$fnName] = $fo
|
||||
}
|
||||
}
|
||||
if ($fnMap.Count -gt 0) { $dsl['functions'] = $fnMap }
|
||||
}
|
||||
}
|
||||
|
||||
# === Вывод ===
|
||||
$json = ConvertTo-CompactJson $dsl 0
|
||||
if ($OutputPath) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-decompile v0.65 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# meta-decompile v0.69 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
#
|
||||
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
|
||||
@@ -311,7 +311,25 @@ def get_type_shorthand(type_node):
|
||||
fr = _text(dn)
|
||||
parts.append(fr) # Date | DateTime
|
||||
elif re.search(r'(^|:)base64Binary$', raw, re.I):
|
||||
parts.append('ValueStorage')
|
||||
# xs:base64Binary — всегда ДвоичныеДанные (ХранилищеЗначения — это v8:ValueStorage).
|
||||
# Узел без квалификаторов встречается только в рукописном XML: замерено на 8.3.24.1691 —
|
||||
# платформа читает его как безлимит (Length 0, Variable) и так же выгружает обратно.
|
||||
bq = type_node.find('v8:BinaryDataQualifiers', NS)
|
||||
if bq is not None:
|
||||
blen = bq.find('v8:Length', NS)
|
||||
bal = bq.find('v8:AllowedLength', NS)
|
||||
blen_val = _text(blen).strip() if blen is not None else ''
|
||||
bal_val = _text(bal).strip() if bal is not None else ''
|
||||
# Голым BinaryData сворачиваем ТОЛЬКО точный дефолт компилятора
|
||||
# (4294967292/Fixed), иначе фиксированная длина терялась на раундтрипе.
|
||||
if bal_val.lower() == 'variable' and blen_val:
|
||||
parts.append(f'BinaryData({blen_val})')
|
||||
elif blen_val and blen_val != '4294967292':
|
||||
parts.append(f'BinaryData({blen_val},fixed)')
|
||||
else:
|
||||
parts.append('BinaryData')
|
||||
else:
|
||||
parts.append('BinaryData(0)')
|
||||
else:
|
||||
parts.append(strip_ns_prefix(raw)) # cfg:CatalogRef.X → CatalogRef.X
|
||||
elif ln == 'TypeSet':
|
||||
@@ -420,6 +438,11 @@ def parse_choice_parameters(parent, tag):
|
||||
def attr_to_dsl(attr_node):
|
||||
ap = _single(attr_node, 'md:Properties')
|
||||
nm = _text(_single(ap, 'md:Name'))
|
||||
# Поле внешнего источника: три своих свойства. Имя колонки по умолчанию равно имени поля,
|
||||
# поэтому в DSL попадает только отличающееся.
|
||||
eds_nids = _single(ap, 'md:NameInDataSource')
|
||||
eds_ro = _single(ap, 'md:ReadOnly')
|
||||
eds_null = _single(ap, 'md:AllowNull')
|
||||
ts = get_type_shorthand(_single(ap, 'md:Type'))
|
||||
flags = []
|
||||
fc = _single(ap, 'md:FillChecking')
|
||||
@@ -435,6 +458,10 @@ def attr_to_dsl(attr_node):
|
||||
ml = _single(ap, 'md:MultiLine')
|
||||
if ml is not None and _text(ml) == 'true':
|
||||
flags.append('multiline')
|
||||
if eds_ro is not None and _text(eds_ro) == 'true':
|
||||
flags.append('readonly')
|
||||
if eds_null is not None and _text(eds_null) == 'true':
|
||||
flags.append('nullable')
|
||||
|
||||
# Синоним/подсказка (строка ru-only ИЛИ {ru,en}).
|
||||
syn_node = _single(ap, 'md:Synonym')
|
||||
@@ -618,6 +645,8 @@ def attr_to_dsl(attr_node):
|
||||
|
||||
# Пустой <Type/> (реквизит без типа) → ts=''. Отличаем от «дефолтного» отсутствия: явный type:''.
|
||||
type_empty = (ts == '')
|
||||
if eds_nids is not None and _text(eds_nids) and _text(eds_nids) != nm:
|
||||
extra['nameInDataSource'] = _text(eds_nids)
|
||||
if syn_custom or syn_empty or (tt_val is not None) or len(extra) > 0 or type_empty:
|
||||
o = {'name': nm}
|
||||
if ts:
|
||||
@@ -2112,6 +2141,7 @@ SUPPORTED_TYPES = (
|
||||
'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob',
|
||||
'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter',
|
||||
'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService',
|
||||
'ExternalDataSource',
|
||||
)
|
||||
|
||||
|
||||
@@ -2154,6 +2184,159 @@ def main():
|
||||
|
||||
build_dsl()
|
||||
|
||||
# --- Внешний источник данных: таблицы (отдельные файлы) и функции (узлы внутри файла) ---
|
||||
if obj_type == 'ExternalDataSource':
|
||||
dlcm_val = P('DataLockControlMode')
|
||||
if dlcm_val and dlcm_val != 'Automatic':
|
||||
dsl['dataLockControlMode'] = dlcm_val
|
||||
|
||||
def short_field_ref(ref):
|
||||
"""Короткое имя из полного пути ExternalDataSource.И.Table.Т.Field.П"""
|
||||
return ref.split('.')[-1] if ref else None
|
||||
|
||||
def field_ref_list(parent, tag):
|
||||
return [short_field_ref(_text(f)) for f in parent.findall('md:%s/xr:Field' % tag, NS)]
|
||||
|
||||
src_dir = os.path.join(os.path.dirname(os.path.abspath(args.ObjectPath)), obj_name)
|
||||
child_objs_eds = _single(obj_node, 'md:ChildObjects')
|
||||
if child_objs_eds is not None:
|
||||
tables_map = {}
|
||||
for t_node in child_objs_eds.findall('md:Table', NS):
|
||||
tbl_name = (_text(t_node) or '').strip()
|
||||
tbl_path = os.path.join(src_dir, 'Tables', tbl_name + '.xml')
|
||||
if not os.path.isfile(tbl_path):
|
||||
sys.stderr.write("meta-decompile: файл таблицы не найден: %s\n" % tbl_path)
|
||||
continue
|
||||
t_root = etree.parse(tbl_path).getroot()
|
||||
t_obj_node = next((c for c in t_root if isinstance(c.tag, str)), None)
|
||||
tp = _single(t_obj_node, 'md:Properties')
|
||||
|
||||
def TP(tag, _tp=None):
|
||||
n = _single(_tp if _tp is not None else tp, 'md:%s' % tag)
|
||||
return _text(n) if n is not None else None
|
||||
|
||||
tbl = {}
|
||||
t_syn_node = _single(tp, 'md:Synonym')
|
||||
t_syn = get_ml_value(t_syn_node)
|
||||
if isinstance(t_syn, str):
|
||||
if t_syn != split_camel_words(tbl_name):
|
||||
tbl['synonym'] = t_syn
|
||||
elif t_syn is not None:
|
||||
tbl['synonym'] = t_syn
|
||||
elif t_syn_node is not None:
|
||||
# Пустой <Synonym/> != авто-синоним из имени: без явного '' компилятор до-генерит его.
|
||||
tbl['synonym'] = ''
|
||||
t_cmt = TP('Comment')
|
||||
if t_cmt:
|
||||
tbl['comment'] = t_cmt
|
||||
t_type = TP('TableType')
|
||||
if t_type and t_type != 'Table':
|
||||
tbl['tableType'] = t_type
|
||||
nids = TP('NameInDataSource')
|
||||
if nids and nids != tbl_name:
|
||||
tbl['nameInDataSource'] = nids
|
||||
expr = TP('ExpressionInDataSource')
|
||||
if expr:
|
||||
tbl['expressionInDataSource'] = expr
|
||||
tdt = TP('TableDataType')
|
||||
if tdt and tdt != 'NonobjectData':
|
||||
tbl['tableDataType'] = tdt
|
||||
keys = field_ref_list(tp, 'KeyFields')
|
||||
if keys:
|
||||
tbl['keyFields'] = keys
|
||||
for tag, key in (('PresentationField', 'presentationField'), ('ParentField', 'parentField'),
|
||||
('DataVersionField', 'dataVersionField')):
|
||||
v = short_field_ref(TP(tag))
|
||||
if v:
|
||||
tbl[key] = v
|
||||
ibs = field_ref_list(tp, 'InputByString')
|
||||
# Ввод по строке компилятор выводит из поля представления: совпадающий список не пишем.
|
||||
ibs_auto = [tbl['presentationField']] if tbl.get('presentationField') else []
|
||||
if ibs != ibs_auto:
|
||||
tbl['inputByString'] = ibs
|
||||
dlf = field_ref_list(tp, 'DataLockFields')
|
||||
if dlf:
|
||||
tbl['dataLockFields'] = dlf
|
||||
if TP('ReadOnly') == 'true':
|
||||
tbl['readOnly'] = True
|
||||
til = TP('TransactionsIsolationLevel')
|
||||
if til and til != 'Auto':
|
||||
tbl['transactionsIsolationLevel'] = til
|
||||
tdlcm = TP('DataLockControlMode')
|
||||
if tdlcm and tdlcm != 'Automatic':
|
||||
tbl['dataLockControlMode'] = tdlcm
|
||||
if TP('UseStandardCommands') == 'false':
|
||||
tbl['useStandardCommands'] = False
|
||||
if TP('QuickChoice') == 'true':
|
||||
tbl['quickChoice'] = True
|
||||
t_et = TP('EditType')
|
||||
if t_et and t_et != 'InDialog':
|
||||
tbl['editType'] = t_et
|
||||
# Слоты форм — такая же часть свойств таблицы, как у прочих объектов (сами формы
|
||||
# вне скоупа раундтрипа: это отдельные файлы, их делает навык form-add).
|
||||
for xml_tag, dsl_key in (('DefaultObjectForm', 'defaultObjectForm'),
|
||||
('DefaultRecordForm', 'defaultRecordForm'),
|
||||
('DefaultListForm', 'defaultListForm'),
|
||||
('DefaultChoiceForm', 'defaultChoiceForm')):
|
||||
fv = TP(xml_tag)
|
||||
if fv:
|
||||
tbl[dsl_key] = fv
|
||||
based_on = [_text(it) for it in tp.findall('md:BasedOn/xr:Item', NS)]
|
||||
if based_on:
|
||||
tbl['basedOn'] = based_on
|
||||
|
||||
fields_arr = []
|
||||
t_child = _single(t_obj_node, 'md:ChildObjects')
|
||||
if t_child is not None:
|
||||
for f in t_child.findall('md:Field', NS):
|
||||
fields_arr.append(attr_to_dsl(f))
|
||||
# Таблица без собственных свойств — короткая форма: просто массив полей.
|
||||
if not tbl:
|
||||
tables_map[tbl_name] = fields_arr
|
||||
else:
|
||||
tbl['fields'] = fields_arr
|
||||
tables_map[tbl_name] = tbl
|
||||
if tables_map:
|
||||
dsl['tables'] = tables_map
|
||||
|
||||
fn_map = {}
|
||||
for fn_node in child_objs_eds.findall('md:Function', NS):
|
||||
fp = _single(fn_node, 'md:Properties')
|
||||
fn_name = _text(_single(fp, 'md:Name'))
|
||||
fn_expr_node = _single(fp, 'md:ExpressionInDataSource')
|
||||
fn_expr = _text(fn_expr_node) if fn_expr_node is not None else ''
|
||||
fn_ret_node = _single(fp, 'md:ReturnValue')
|
||||
fn_returns = get_type_shorthand(_single(fp, 'md:Type'))
|
||||
fn_syn = get_ml_value(_single(fp, 'md:Synonym'))
|
||||
fn_cmt_node = _single(fp, 'md:Comment')
|
||||
fn_cmt = _text(fn_cmt_node) if fn_cmt_node is not None else ''
|
||||
fn_no_value = fn_ret_node is not None and _text(fn_ret_node) == 'false'
|
||||
if isinstance(fn_syn, str):
|
||||
syn_custom_fn = fn_syn != split_camel_words(fn_name) and fn_syn != ''
|
||||
else:
|
||||
syn_custom_fn = fn_syn is not None
|
||||
# Умолчание `returns` компилятора — String, а он даёт String(10): с ним и сверяем,
|
||||
# иначе короткая форма (одна строка выражения) не срабатывала бы никогда.
|
||||
if not fn_no_value and not fn_cmt and not syn_custom_fn and fn_returns != 'String(10)':
|
||||
fn_map[fn_name] = {'expression': fn_expr, 'returns': fn_returns}
|
||||
elif not fn_no_value and not fn_cmt and not syn_custom_fn:
|
||||
# Тип по умолчанию String — короткая форма: одна строка выражения.
|
||||
fn_map[fn_name] = fn_expr
|
||||
else:
|
||||
fo = {'expression': fn_expr}
|
||||
if fn_no_value:
|
||||
fo['returnValue'] = False
|
||||
elif fn_returns:
|
||||
fo['returns'] = fn_returns
|
||||
if syn_custom_fn:
|
||||
fo['synonym'] = fn_syn
|
||||
if fn_cmt:
|
||||
fo['comment'] = fn_cmt
|
||||
fn_map[fn_name] = fo
|
||||
if fn_map:
|
||||
dsl['functions'] = fn_map
|
||||
|
||||
|
||||
# === Вывод ===
|
||||
json_str = convert_to_compact_json(dsl, 0)
|
||||
if args.OutputPath:
|
||||
|
||||
@@ -35,48 +35,28 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -Def
|
||||
| DefinitionFile | JSON-файл с операциями (альтернатива Operation) |
|
||||
| NoValidate | Не запускать meta-validate после правки |
|
||||
|
||||
## Операции — сводная таблица
|
||||
## Частые операции
|
||||
|
||||
Batch через `;;` во всех операциях. Подробный синтаксис — в файлах по ссылкам.
|
||||
|
||||
### Дочерние элементы — [child-operations.md](child-operations.md)
|
||||
Batch через `;;` во всех операциях.
|
||||
|
||||
| Операция | Формат Value | Пример |
|
||||
|----------|-------------|--------|
|
||||
| `add-attribute` | `Имя: Тип \| флаги` | `"Сумма: Число(15,2) \| req, index"` |
|
||||
| `add-ts` | `ТЧ: Рекв1: Тип1, Рекв2: Тип2` | `"Товары: Ном: CatalogRef.Ном, Кол: Число(15,3)"` |
|
||||
| `add-dimension` | `Имя: Тип \| флаги` | `"Организация: CatalogRef.Организации \| master"` |
|
||||
| `add-resource` | `Имя: Тип` | `"Сумма: Число(15,2)"` |
|
||||
| `add-enumValue` | `Имя` | `"Значение1 ;; Значение2"` |
|
||||
| `add-column` | `Имя: Тип` | `"Тип: EnumRef.ТипыДокументов"` |
|
||||
| `add-form` / `add-template` / `add-command` | `Имя` | `"ФормаЭлемента"` |
|
||||
| `add-ts` | `ТЧ: Рекв1: Тип1, Рекв2: Тип2` | `"Товары: Ном: CatalogRef.Ном, Кол: Число(15,3)"` |
|
||||
| `add-ts-attribute` | `ТЧ.Имя: Тип` | `"Товары.Скидка: Число(15,2)"` |
|
||||
| `remove-*` | `Имя` | `"СтарыйРеквизит ;; ЕщёОдин"` |
|
||||
| `remove-ts-attribute` | `ТЧ.Имя` | `"Товары.УстаревшийРекв"` |
|
||||
| `modify-attribute` | `Имя: ключ=значение` | `"СтароеИмя: name=НовоеИмя, type=Строка(500)"` |
|
||||
| `modify-ts-attribute` | `ТЧ.Имя: ключ=значение` | `"Товары.Рекв: name=НовоеИмя"` |
|
||||
| `modify-ts` | `ТЧ: ключ=значение` | `"Товары: synonym=Товарный состав"` |
|
||||
| `modify-property` | `Ключ=Значение` | `"CodeLength=11 ;; DescriptionLength=150"` |
|
||||
|
||||
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
|
||||
|
||||
`modify-attribute` умеет и структурные свойства реквизита — формат/подсказку, форму и параметры выбора,
|
||||
значение заполнения, границы (`Format`, `ChoiceForm`, `ChoiceParameters`, `FillValue`, `MinValue`/`MaxValue` и др.).
|
||||
|
||||
### Свойства объекта — [properties-reference.md](properties-reference.md)
|
||||
|
||||
| Операция | Формат Value | Пример |
|
||||
|----------|-------------|--------|
|
||||
| `modify-property` | `Ключ=Значение` | `"CodeLength=11 ;; DescriptionLength=150"` |
|
||||
| `add-owner` | `MetaType.Name` | `"Catalog.Контрагенты ;; Catalog.Организации"` |
|
||||
| `add-registerRecord` | `MetaType.Name` | `"AccumulationRegister.ОстаткиТоваров"` |
|
||||
| `add-basedOn` | `MetaType.Name` | `"Document.ЗаказКлиента"` |
|
||||
| `add-inputByString` | `Путь поля` | `"StandardAttribute.Description"` |
|
||||
| `set-owners` / `set-registerRecords` / `set-basedOn` / `set-inputByString` | Замена всего списка | `"Catalog.Орг ;; Catalog.Контр"` |
|
||||
| `remove-owner` / `remove-registerRecord` / ... | Удаление из списка | `"Catalog.Контрагенты"` |
|
||||
|
||||
### JSON DSL — [json-dsl.md](json-dsl.md)
|
||||
|
||||
Для комбинированных операций (add + remove + modify в одном файле), синонимы ключей/типов, таблица поддерживаемых объектов.
|
||||
Составной тип — через `+`: `"Значение: Строка + Число(15,2) + Дата"`.
|
||||
|
||||
## Быстрые примеры
|
||||
|
||||
@@ -84,9 +64,6 @@ Batch через `;;` во всех операциях. Подробный си
|
||||
# Добавить реквизиты
|
||||
-Operation add-attribute -Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index"
|
||||
|
||||
# Составной тип (несколько типов через +)
|
||||
-Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты"
|
||||
|
||||
# Добавить ТЧ с реквизитами
|
||||
-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2)"
|
||||
|
||||
@@ -103,6 +80,21 @@ Batch через `;;` во всех операциях. Подробный си
|
||||
-Operation set-owners -Value "Catalog.Контрагенты ;; Catalog.Организации"
|
||||
```
|
||||
|
||||
## Индекс: что правишь → файл
|
||||
|
||||
| Что нужно | Файл |
|
||||
|-----------|------|
|
||||
| Реквизит, измерение, ресурс, графа: флаги, составные типы, структурные свойства (`Format`, `ChoiceParameters`, `FillValue`, …) | `reference/attributes.md` |
|
||||
| Табличная часть и её реквизиты | `reference/tabular-sections.md` |
|
||||
| Свойства самого объекта, списочные свойства (владельцы, движения, основание, ввод по строке) | `reference/properties.md` |
|
||||
| Предопределённые элементы | `reference/predefined.md` |
|
||||
| Значения перечисления, команды | `reference/other-children.md` |
|
||||
| Внешний источник данных: поля, таблицы, функции | `reference/external-data-source.md` |
|
||||
| Комбинированные операции в одном JSON, синонимы ключей и типов, какие дети допустимы у типа объекта | `reference/json-dsl.md` |
|
||||
|
||||
Форму добавляет и удаляет навык `form-add` / `form-remove`, макет — навык `template-add` /
|
||||
`template-remove`: кроме записи в `ChildObjects` у них есть собственные файлы, и `meta-edit` их не трогает.
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
|
||||
+8
-80
@@ -1,6 +1,7 @@
|
||||
# Inline-операции над дочерними элементами
|
||||
# Реквизиты, измерения, ресурсы, графы
|
||||
|
||||
Подробный справочник операций `add-*` / `remove-*` / `modify-*` для дочерних элементов объекта метаданных.
|
||||
Операции над «плоскими» дочерними элементами объекта: реквизит (`attribute`), измерение (`dimension`),
|
||||
ресурс (`resource`), графа журнала (`column`). Синтаксис у всех один.
|
||||
|
||||
## Общие правила
|
||||
|
||||
@@ -9,9 +10,10 @@
|
||||
-Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index"
|
||||
```
|
||||
|
||||
**Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги`
|
||||
**Shorthand-формат**: `ИмяРеквизита: Тип | флаги`
|
||||
|
||||
Флаги: `req` — обязательное заполнение; `index` — индексировать; `master` — ведущее измерение (только dimensions); `mainFilter` — основной отбор (только dimensions).
|
||||
Флаги: `req` — обязательное заполнение; `index` — индексировать; `master` — ведущее измерение (только
|
||||
измерения); `mainFilter` — основной отбор (только измерения).
|
||||
|
||||
**Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`:
|
||||
```powershell
|
||||
@@ -20,11 +22,10 @@
|
||||
|
||||
## Составные типы
|
||||
|
||||
Для реквизитов с несколькими допустимыми типами — разделитель `+`:
|
||||
Разделитель `+`:
|
||||
```powershell
|
||||
-Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты"
|
||||
-Operation add-attribute -Value "Значение: Строка + Число(15,2) | req"
|
||||
-Operation modify-ts-attribute -Value "Данные.Значение: type=Строка + Число(15,2) + Дата"
|
||||
```
|
||||
|
||||
В JSON DSL — массив в `type`:
|
||||
@@ -43,84 +44,11 @@
|
||||
-Operation add-column -Value "Тип: EnumRef.ТипыДокументов"
|
||||
```
|
||||
|
||||
## add-ts
|
||||
|
||||
Формат: `ИмяТЧ: Реквизит1: Тип1, Реквизит2: Тип2, ...`
|
||||
|
||||
```powershell
|
||||
-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2), Сумма: Число(15,2)"
|
||||
```
|
||||
|
||||
## add-ts-attribute / remove-ts-attribute / modify-ts-attribute
|
||||
|
||||
Операции над реквизитами **внутри существующей ТЧ**. Формат: `ИмяТЧ.ОпределениеРеквизита` (dot-нотация).
|
||||
|
||||
```powershell
|
||||
# Добавить реквизит в ТЧ
|
||||
-Operation add-ts-attribute -Value "Товары.СтавкаНДС: EnumRef.СтавкиНДС"
|
||||
-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) ;; Товары.Бонус: Число(15,2)"
|
||||
|
||||
# Позиционная вставка в ТЧ
|
||||
-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) >> after Цена"
|
||||
|
||||
# Удалить реквизит из ТЧ
|
||||
-Operation remove-ts-attribute -Value "Товары.УстаревшийРекв"
|
||||
-Operation remove-ts-attribute -Value "Товары.Рекв1 ;; Товары.Рекв2"
|
||||
|
||||
# Изменить реквизит в ТЧ (rename, type change и т.д.)
|
||||
-Operation modify-ts-attribute -Value "Товары.СтароеИмя: name=НовоеИмя, type=Строка(500)"
|
||||
```
|
||||
|
||||
Batch через `;;` — можно указать разные ТЧ: `"Товары.А: Строка(50) ;; Услуги.Б: Число(10)"`.
|
||||
|
||||
## modify-ts
|
||||
|
||||
Изменение свойств **самой табличной части** (Synonym, FillChecking, Use и др.):
|
||||
|
||||
```powershell
|
||||
-Operation modify-ts -Value "Товары: synonym=Товарный состав"
|
||||
-Operation modify-ts -Value "Товары: fillChecking=ShowError"
|
||||
```
|
||||
|
||||
Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`.
|
||||
|
||||
## add-predefined
|
||||
|
||||
Добавить предопределённые элементы (Catalog, ChartOfCharacteristicTypes). Существующие элементы и их
|
||||
идентификаторы сохраняются, новые получают свежий id.
|
||||
|
||||
Inline — строка `(Код) Имя [Наименование]` (batch через `;;`; `[Наименование]` необязательно — иначе авто из имени):
|
||||
```powershell
|
||||
-Operation add-predefined -Value "(001) Основной ;; (002) Резервный [Резервный склад]"
|
||||
```
|
||||
|
||||
JSON — строки и/или объекты (для групп с вложенными):
|
||||
```json
|
||||
{ "add": { "predefined": [
|
||||
"(001) Основной",
|
||||
{ "name": "Группа", "isFolder": true, "childItems": ["(002) Вложенный"] }
|
||||
] } }
|
||||
```
|
||||
|
||||
Ключи объекта: `name`, `code`, `description`, `isFolder`, `childItems` (дерево). Тип кода (строковый/числовой)
|
||||
берётся из объекта автоматически.
|
||||
|
||||
## add-enumValue / add-form / add-template / add-command
|
||||
|
||||
Просто имена (batch через `;;`):
|
||||
```powershell
|
||||
-Operation add-enumValue -Value "Значение1 ;; Значение2 ;; Значение3"
|
||||
-Operation add-form -Value "ФормаЭлемента ;; ФормаСписка"
|
||||
-Operation add-template -Value "ПечатнаяФорма"
|
||||
-Operation add-command -Value "Команда1"
|
||||
```
|
||||
|
||||
## remove-*
|
||||
|
||||
Имя элемента (или несколько через `;;`):
|
||||
```powershell
|
||||
-Operation remove-attribute -Value "СтарыйРеквизит ;; ЕщёОдин"
|
||||
-Operation remove-ts -Value "УстаревшаяТЧ"
|
||||
-Operation remove-enumValue -Value "НеиспользуемоеЗначение"
|
||||
```
|
||||
|
||||
@@ -140,7 +68,7 @@ JSON — строки и/или объекты (для групп с вложе
|
||||
-Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение"
|
||||
```
|
||||
|
||||
### Структурные свойства реквизита
|
||||
## Структурные свойства реквизита
|
||||
|
||||
Свойства со сложным значением задавайте через JSON DSL (`{ "modify": { "attributes": { "Имя": { ... } } } }`):
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Внешние источники данных
|
||||
|
||||
Точечные правки внутри уже созданного источника: поле в таблицу, таблица и функция в источник.
|
||||
Источник целиком собирает навык `meta-compile` — его повторный запуск заменяет файл источника,
|
||||
выдаёт новый uuid и оставляет сиротами таблицы, которых нет в описании.
|
||||
|
||||
## add-field — поле таблицы
|
||||
|
||||
Правится файл таблицы: `ExternalDataSources/<Источник>/Tables/<Таблица>.xml`.
|
||||
|
||||
Поле задаётся как обычный реквизит, плюс три своих ключа: `nameInDataSource` (умолчание — имя поля),
|
||||
`readOnly`, `allowNull`. Флаги строковой формы — `readonly`, `nullable`.
|
||||
|
||||
```json
|
||||
{ "add": { "fields": [
|
||||
"barcode: String(20) | nullable",
|
||||
{ "name": "cost", "type": "Number(15,2)", "nameInDataSource": "cost_net", "readOnly": true }
|
||||
] } }
|
||||
```
|
||||
|
||||
Типы поля: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` и ссылка на таблицу того же
|
||||
источника — `ExternalDataSourceTableRef.<Источник>.<Таблица>`. Составной тип платформа запрещает.
|
||||
|
||||
## add-tables / add-functions — в файле источника
|
||||
|
||||
Правится файл источника: `ExternalDataSources/<Источник>.xml`. Таблица — единственная операция
|
||||
навыка, создающая **файл**: `<Источник>/Tables/<Имя>.xml` плюс имя в `ChildObjects` источника.
|
||||
Синтаксис таблицы и функции тот же, что у навыка `meta-compile` (`reference/external-data-source.md` там же).
|
||||
|
||||
```json
|
||||
{ "add": {
|
||||
"tables": { "sales": { "keyFields": ["id"], "fields": ["id: Number(10,0)", "summa: Number(15,2)"] } },
|
||||
"functions": { "nextKey": "NEXT VALUE FOR public.seq_key" }
|
||||
} }
|
||||
```
|
||||
|
||||
При добавлении таблицы не поддержаны ключи `characteristics` и `default*Form` — навык отвергает их,
|
||||
а не проглатывает молча. Характеристики задаются при сборке источника навыком `meta-compile`,
|
||||
форму назначает навык `form-add`.
|
||||
|
||||
## Удаление
|
||||
|
||||
Таблицу удаляет навык `meta-remove`: `ExternalDataSource.<Источник>.Table.<Таблица>` — вместе с файлом
|
||||
и записью в `ChildObjects`. Весь источник — `ExternalDataSource.<Источник>`.
|
||||
+12
-7
@@ -22,8 +22,7 @@ powershell.exe -NoProfile -File ${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1 -Defin
|
||||
{ "name": "Количество", "type": "Число(15,3)" }
|
||||
]
|
||||
}],
|
||||
"forms": ["ФормаЭлемента"],
|
||||
"templates": ["ПечатнаяФорма"]
|
||||
"commands": ["Пересчитать"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -120,6 +119,9 @@ powershell.exe -NoProfile -File ${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1 -Defin
|
||||
| commands | команды |
|
||||
| properties | свойства |
|
||||
|
||||
Формы и макеты `meta-edit` не добавляет и не удаляет: кроме записи в `ChildObjects` у них есть собственные файлы.
|
||||
Форма — навыки `form-add` / `form-remove`, макет — `template-add` / `template-remove`. Команда файла не имеет — её `meta-edit` добавляет сам.
|
||||
|
||||
## Составные типы
|
||||
|
||||
Для полей с несколькими допустимыми типами — массив в `type`:
|
||||
@@ -141,8 +143,11 @@ powershell.exe -NoProfile -File ${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1 -Defin
|
||||
|
||||
| Тип объекта | Допустимые add-типы |
|
||||
|-------------|-------------------|
|
||||
| Catalog, Document, ExchangePlan, ChartOf*, BP, Task, Report, DP | attributes, tabularSections, forms, templates, commands |
|
||||
| Enum | enumValues, forms, templates, commands |
|
||||
| *Register (4 типа) | dimensions, resources, attributes, forms, templates, commands |
|
||||
| DocumentJournal | columns, forms, templates, commands |
|
||||
| Constant | forms |
|
||||
| Catalog, Document, ExchangePlan, ChartOf*, BP, Task, Report, DP | attributes, tabularSections, commands |
|
||||
| Enum | enumValues, commands |
|
||||
| *Register (4 типа) | dimensions, resources, attributes, commands |
|
||||
| DocumentJournal | columns, commands |
|
||||
| ExternalDataSource | tables, functions |
|
||||
| Table (таблица внешнего источника) | fields, commands |
|
||||
|
||||
У `Constant` добавлять нечего: единственный её ребёнок — форма, а форму добавляет навык `form-add`.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Значения перечисления и команды
|
||||
|
||||
## add-enumValue / add-command
|
||||
|
||||
Просто имена (batch через `;;`):
|
||||
```powershell
|
||||
-Operation add-enumValue -Value "Значение1 ;; Значение2 ;; Значение3"
|
||||
-Operation add-command -Value "ОткрытьДосье"
|
||||
```
|
||||
|
||||
Команда создаётся вместе с заготовкой модуля `Commands/<Имя>/Ext/CommandModule.bsl`.
|
||||
|
||||
## Формы и макеты — другими навыками
|
||||
|
||||
Кроме записи в `ChildObjects` у формы и макета есть собственные файлы, поэтому `meta-edit` их
|
||||
не добавляет и не удаляет:
|
||||
|
||||
| Что | Добавить | Удалить |
|
||||
|-----|----------|---------|
|
||||
| Форма | навык `form-add` | навык `form-remove` |
|
||||
| Макет | навык `template-add` | навык `template-remove` |
|
||||
|
||||
Содержимое формы собирает навык `form-compile`.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Предопределённые элементы
|
||||
|
||||
Добавить предопределённые элементы (Catalog, ChartOfCharacteristicTypes). Существующие элементы и их
|
||||
идентификаторы сохраняются, новые получают свежий id.
|
||||
|
||||
Inline — строка `(Код) Имя [Наименование]` (batch через `;;`; `[Наименование]` необязательно — иначе авто из имени):
|
||||
```powershell
|
||||
-Operation add-predefined -Value "(001) Основной ;; (002) Резервный [Резервный склад]"
|
||||
```
|
||||
|
||||
JSON — строки и/или объекты (для групп с вложенными):
|
||||
```json
|
||||
{ "add": { "predefined": [
|
||||
"(001) Основной",
|
||||
{ "name": "Группа", "isFolder": true, "childItems": ["(002) Вложенный"] }
|
||||
] } }
|
||||
```
|
||||
|
||||
Ключи объекта: `name`, `code`, `description`, `isFolder`, `childItems` (дерево). Тип кода (строковый/числовой)
|
||||
берётся из объекта автоматически.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Табличные части
|
||||
|
||||
Операции над табличной частью и её реквизитами. Общие правила shorthand-формата, флагов, batch и
|
||||
позиционной вставки — в `attributes.md`.
|
||||
|
||||
## add-ts
|
||||
|
||||
Формат: `ИмяТЧ: Реквизит1: Тип1, Реквизит2: Тип2, ...`
|
||||
|
||||
```powershell
|
||||
-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2), Сумма: Число(15,2)"
|
||||
```
|
||||
|
||||
## add-ts-attribute / remove-ts-attribute / modify-ts-attribute
|
||||
|
||||
Операции над реквизитами **внутри существующей ТЧ**. Формат: `ИмяТЧ.ОпределениеРеквизита` (dot-нотация).
|
||||
|
||||
```powershell
|
||||
# Добавить реквизит в ТЧ
|
||||
-Operation add-ts-attribute -Value "Товары.СтавкаНДС: EnumRef.СтавкиНДС"
|
||||
-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) ;; Товары.Бонус: Число(15,2)"
|
||||
|
||||
# Позиционная вставка в ТЧ
|
||||
-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) >> after Цена"
|
||||
|
||||
# Удалить реквизит из ТЧ
|
||||
-Operation remove-ts-attribute -Value "Товары.УстаревшийРекв"
|
||||
-Operation remove-ts-attribute -Value "Товары.Рекв1 ;; Товары.Рекв2"
|
||||
|
||||
# Изменить реквизит в ТЧ (rename, type change и т.д.)
|
||||
-Operation modify-ts-attribute -Value "Товары.СтароеИмя: name=НовоеИмя, type=Строка(500)"
|
||||
```
|
||||
|
||||
Batch через `;;` — можно указать разные ТЧ: `"Товары.А: Строка(50) ;; Услуги.Б: Число(10)"`.
|
||||
|
||||
Составной тип реквизита ТЧ:
|
||||
```powershell
|
||||
-Operation modify-ts-attribute -Value "Данные.Значение: type=Строка + Число(15,2) + Дата"
|
||||
```
|
||||
|
||||
## modify-ts
|
||||
|
||||
Изменение свойств **самой табличной части** (Synonym, FillChecking, Use и др.):
|
||||
|
||||
```powershell
|
||||
-Operation modify-ts -Value "Товары: synonym=Товарный состав"
|
||||
-Operation modify-ts -Value "Товары: fillChecking=ShowError"
|
||||
```
|
||||
|
||||
Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`.
|
||||
|
||||
## remove-ts
|
||||
|
||||
```powershell
|
||||
-Operation remove-ts -Value "УстаревшаяТЧ"
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-edit v1.42 — Edit existing 1C metadata object XML
|
||||
# meta-edit v1.50 — Edit existing 1C metadata object XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -139,7 +139,8 @@ $script:validEnumValues = @{
|
||||
"WriteMode" = @("Independent","RecorderSubordinate")
|
||||
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
|
||||
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
|
||||
"DataLockControlMode" = @("Automatic","Managed")
|
||||
# AutomaticAndManaged — только у внешнего источника данных и его таблиц.
|
||||
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
||||
"FullTextSearch" = @("Use","DontUse")
|
||||
"DataHistory" = @("Use","DontUse")
|
||||
"DefaultPresentation" = @("AsDescription","AsCode")
|
||||
@@ -464,6 +465,9 @@ $script:childTypeSynonyms = @{
|
||||
"templates" = "templates"; "макеты" = "templates"
|
||||
"commands" = "commands"; "команды" = "commands"
|
||||
"properties" = "properties"; "свойства" = "properties"
|
||||
"fields" = "fields"; "поля" = "fields"
|
||||
"tables" = "tables"; "таблицы" = "tables"
|
||||
"functions" = "functions"; "функции" = "functions"
|
||||
}
|
||||
|
||||
# Type synonyms (from meta-compile)
|
||||
@@ -745,11 +749,21 @@ function Import-Fragment([string]$xmlString) {
|
||||
}
|
||||
|
||||
function Get-ChildIndent($container) {
|
||||
# В контейнере с детьми первый пробельный узел — это отступ ПЕРЕД первым ребёнком.
|
||||
# В пустом (только что раскрытом) единственный пробельный узел — отступ ЗАКРЫВАЮЩЕГО
|
||||
# тега, то есть уровень самого контейнера: ребёнку нужен на табуляцию глубже. Без этой
|
||||
# поправки первый ребёнок вставал вровень с <ChildObjects>.
|
||||
$hasElements = $false
|
||||
foreach ($child in $container.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element') { $hasElements = $true; break }
|
||||
}
|
||||
foreach ($child in $container.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
|
||||
$text = $child.Value
|
||||
if ($text -match '^\r?\n(\t+)$') { return $Matches[1] }
|
||||
if ($text -match '^\r?\n(\t+)') { return $Matches[1] }
|
||||
$found = $null
|
||||
if ($text -match '^\r?\n(\t+)$') { $found = $Matches[1] }
|
||||
elseif ($text -match '^\r?\n(\t+)') { $found = $Matches[1] }
|
||||
if ($null -ne $found) { return $(if ($hasElements) { $found } else { "$found`t" }) }
|
||||
}
|
||||
}
|
||||
# Fallback: count depth
|
||||
@@ -900,6 +914,9 @@ function Collapse-ChildObjectsIfEmpty {
|
||||
while ($script:childObjectsEl.HasChildNodes) {
|
||||
$script:childObjectsEl.RemoveChild($script:childObjectsEl.FirstChild) | Out-Null
|
||||
}
|
||||
# XmlDocument помнит, что у узла были дети, и пишет <ChildObjects></ChildObjects>.
|
||||
# Платформа и py-порт дают <ChildObjects/> — сбрасываем флаг явно.
|
||||
$script:childObjectsEl.IsEmpty = $true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,6 +971,10 @@ function Parse-AttributeShorthand {
|
||||
indexing = if ($val.indexing) { "$($val.indexing)" } else { "" }
|
||||
after = if ($val.after) { "$($val.after)" } else { "" }
|
||||
before = if ($val.before) { "$($val.before)" } else { "" }
|
||||
# Поле таблицы внешнего источника (контекст eds-field).
|
||||
nameInDataSource = if ($val.nameInDataSource) { "$($val.nameInDataSource)" } else { "" }
|
||||
readOnly = if ($val.readOnly -eq $true) { $true } else { $false }
|
||||
allowNull = if ($val.allowNull -eq $true) { $true } else { $false }
|
||||
}
|
||||
# Map flags to properties
|
||||
if ($result.flags -contains "req" -and -not $result.fillChecking) {
|
||||
@@ -993,6 +1014,7 @@ function Parse-EnumValueShorthand {
|
||||
function Get-AttributeContext {
|
||||
switch ($script:objType) {
|
||||
"Catalog" { return "catalog" }
|
||||
"Table" { return "eds-field" }
|
||||
"Document" { return "document" }
|
||||
{ $_ -in @("InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister") } { return "register" }
|
||||
{ $_ -in @("DataProcessor","Report","ExternalDataProcessor","ExternalReport") } { return "processor" }
|
||||
@@ -1021,7 +1043,7 @@ $script:reservedByContext = @{
|
||||
}
|
||||
|
||||
function Build-AttributeFragment {
|
||||
param($parsed, [string]$context, [string]$indent)
|
||||
param($parsed, [string]$context, [string]$indent, [string]$elemTag = "Attribute")
|
||||
|
||||
if (-not $context) { $context = Get-AttributeContext }
|
||||
|
||||
@@ -1036,7 +1058,7 @@ function Build-AttributeFragment {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
} elseif ($context -notin @("tabular", "processor-tabular") -and
|
||||
} elseif ($context -notin @("tabular", "processor-tabular", "eds-field") -and
|
||||
($script:reservedAttrNames.ContainsKey($attrName) -or ($script:reservedAttrNames.Values -contains $attrName))) {
|
||||
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
|
||||
}
|
||||
@@ -1044,7 +1066,7 @@ function Build-AttributeFragment {
|
||||
$uuid = New-Guid-String
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
|
||||
$sb.AppendLine("$indent<Attribute uuid=`"$uuid`">") | Out-Null
|
||||
$sb.AppendLine("$indent<$elemTag uuid=`"$uuid`">") | Out-Null
|
||||
$sb.AppendLine("$indent`t<Properties>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<Name>$(Esc-XmlText $parsed.name)</Name>") | Out-Null
|
||||
$sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $parsed.synonym)) | Out-Null
|
||||
@@ -1083,22 +1105,39 @@ function Build-AttributeFragment {
|
||||
if ($parsed.fillChecking) { $fillChecking = Normalize-EnumValue "FillChecking" $parsed.fillChecking }
|
||||
$sb.AppendLine("$indent`t`t<FillChecking>$fillChecking</FillChecking>") | Out-Null
|
||||
|
||||
$sb.AppendLine("$indent`t`t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>") | Out-Null
|
||||
# Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm
|
||||
# у него стоит ПОСЛЕ ChoiceHistoryOnInput — порядок снят с выгрузки платформы.
|
||||
if ($context -ne "eds-field") {
|
||||
$sb.AppendLine("$indent`t`t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>") | Out-Null
|
||||
}
|
||||
$sb.AppendLine("$indent`t`t<ChoiceParameterLinks/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<ChoiceParameters/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<QuickChoice>Auto</QuickChoice>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<CreateOnInput>Auto</CreateOnInput>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<ChoiceForm/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<LinkByType/>") | Out-Null
|
||||
if ($context -ne "eds-field") {
|
||||
$sb.AppendLine("$indent`t`t<ChoiceForm/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<LinkByType/>") | Out-Null
|
||||
}
|
||||
$sb.AppendLine("$indent`t`t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>") | Out-Null
|
||||
|
||||
if ($context -eq "eds-field") {
|
||||
$sb.AppendLine("$indent`t`t<ChoiceForm/>") | Out-Null
|
||||
$nids = if ($parsed.nameInDataSource) { "$($parsed.nameInDataSource)" } else { $parsed.name }
|
||||
$sb.AppendLine("$indent`t`t<NameInDataSource>$(Esc-XmlText $nids)</NameInDataSource>") | Out-Null
|
||||
$ro = if ($parsed.readOnly -eq $true -or $parsed.flags -contains "readonly") { "true" } else { "false" }
|
||||
$sb.AppendLine("$indent`t`t<ReadOnly>$ro</ReadOnly>") | Out-Null
|
||||
$an = if ($parsed.allowNull -eq $true -or $parsed.flags -contains "nullable") { "true" } else { "false" }
|
||||
$sb.AppendLine("$indent`t`t<AllowNull>$an</AllowNull>") | Out-Null
|
||||
}
|
||||
|
||||
# Use — catalog only
|
||||
if ($context -eq "catalog") {
|
||||
$sb.AppendLine("$indent`t`t<Use>ForItem</Use>") | Out-Null
|
||||
}
|
||||
|
||||
# Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular)
|
||||
if ($context -notin @("processor", "processor-tabular")) {
|
||||
# Поля внешнего источника: индексами чужой таблицы 1С не владеет.
|
||||
if ($context -notin @("processor", "processor-tabular", "eds-field")) {
|
||||
$indexing = "DontIndex"
|
||||
if ($parsed.flags -contains "index") { $indexing = "Index" }
|
||||
if ($parsed.flags -contains "indexadditional") { $indexing = "IndexWithAdditionalOrder" }
|
||||
@@ -1110,7 +1149,7 @@ function Build-AttributeFragment {
|
||||
}
|
||||
|
||||
$sb.AppendLine("$indent`t</Properties>") | Out-Null
|
||||
$sb.Append("$indent</Attribute>") | Out-Null
|
||||
$sb.Append("$indent</$elemTag>") | Out-Null
|
||||
return $sb.ToString()
|
||||
}
|
||||
|
||||
@@ -1432,35 +1471,29 @@ function Build-ColumnFragment {
|
||||
return $sb.ToString()
|
||||
}
|
||||
|
||||
function Build-SimpleChildFragment {
|
||||
param([string]$tagName, [string]$name, [string]$indent)
|
||||
# For Form, Template, Command — just a name wrapper
|
||||
function Build-CommandFragment {
|
||||
param([string]$name, [string]$indent)
|
||||
# Команда объекта описывается целиком внутри ChildObjects (отдельного файла у неё нет).
|
||||
# Порядок свойств платформенный — переставлять нельзя.
|
||||
$uuid = New-Guid-String
|
||||
$synonym = Split-CamelCase $name
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
$sb.AppendLine("$indent<$tagName uuid=`"$uuid`">") | Out-Null
|
||||
$sb.AppendLine("$indent<Command uuid=`"$uuid`">") | Out-Null
|
||||
$sb.AppendLine("$indent`t<Properties>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<Name>$(Esc-XmlText $name)</Name>") | Out-Null
|
||||
$sb.AppendLine($(Build-MLTextXml "$indent`t`t" "Synonym" $synonym)) | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<Comment/>") | Out-Null
|
||||
# Forms get additional properties
|
||||
if ($tagName -eq "Form") {
|
||||
$sb.AppendLine("$indent`t`t<FormType>Ordinary</FormType>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<IncludeHelpInContents>false</IncludeHelpInContents>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<UsePurposes/>") | Out-Null
|
||||
}
|
||||
if ($tagName -eq "Template") {
|
||||
$sb.AppendLine("$indent`t`t<TemplateType>SpreadsheetDocument</TemplateType>") | Out-Null
|
||||
}
|
||||
if ($tagName -eq "Command") {
|
||||
$sb.AppendLine("$indent`t`t<Group>FormNavigationPanelGoTo</Group>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<Representation>Auto</Representation>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<ToolTip/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<Picture/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<Shortcut/>") | Out-Null
|
||||
}
|
||||
$sb.AppendLine("$indent`t`t<Group>FormNavigationPanelGoTo</Group>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<CommandParameterType/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<ParameterUseMode>Single</ParameterUseMode>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<ModifiesData>false</ModifiesData>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<Representation>Auto</Representation>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<ToolTip/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<Picture/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<Shortcut/>") | Out-Null
|
||||
$sb.AppendLine("$indent`t`t<OnMainServerUnavalableBehavior>Auto</OnMainServerUnavalableBehavior>") | Out-Null
|
||||
$sb.AppendLine("$indent`t</Properties>") | Out-Null
|
||||
$sb.Append("$indent</$tagName>") | Out-Null
|
||||
$sb.Append("$indent</Command>") | Out-Null
|
||||
return $sb.ToString()
|
||||
}
|
||||
|
||||
@@ -1479,7 +1512,14 @@ function Get-AllChildNames {
|
||||
$propsEl = $gc; break
|
||||
}
|
||||
}
|
||||
if (-not $propsEl) { continue }
|
||||
# Часть детей регистрируется голым текстом, без <Properties>: <Table>Имя</Table>,
|
||||
# <Form>Имя</Form>, <Template>Имя</Template>. Пропуская их, проверка «уже существует»
|
||||
# становилась мёртвой — повторное добавление давало второй такой же узел.
|
||||
if (-not $propsEl) {
|
||||
$n = $child.InnerText.Trim()
|
||||
if ($n) { $names[$n] = $child.LocalName }
|
||||
continue
|
||||
}
|
||||
foreach ($gc in $propsEl.ChildNodes) {
|
||||
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq "Name") {
|
||||
$n = $gc.InnerText.Trim()
|
||||
@@ -1537,11 +1577,13 @@ $script:validChildTypes = @{
|
||||
"CalculationRegister" = @("dimensions","resources","attributes","forms","templates","commands")
|
||||
"DocumentJournal" = @("columns","forms","templates","commands")
|
||||
"Constant" = @("forms")
|
||||
"ExternalDataSource" = @("tables","functions")
|
||||
"Table" = @("fields","forms","templates","commands")
|
||||
}
|
||||
|
||||
# Canonical child order in ChildObjects
|
||||
$script:childOrder = @(
|
||||
"Resource", "Dimension", "Attribute", "TabularSection",
|
||||
"Resource", "Dimension", "Attribute", "TabularSection", "Field", "Table", "Function",
|
||||
"AccountingFlag", "ExtDimensionAccountingFlag",
|
||||
"EnumValue", "Column", "AddressingAttribute", "Recalculation",
|
||||
"Form", "Template", "Command"
|
||||
@@ -1558,6 +1600,260 @@ $script:childTypeToXmlTag = @{
|
||||
"forms" = "Form"
|
||||
"templates" = "Template"
|
||||
"commands" = "Command"
|
||||
"fields" = "Field"
|
||||
"tables" = "Table"
|
||||
"functions" = "Function"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Section 8b: Внешние источники данных — копии из meta-compile
|
||||
# ============================================================
|
||||
# Тела ниже скопированы из meta-compile и обязаны совпадать с ним байт в байт:
|
||||
# таблица внешнего источника собирается в ОТДЕЛЬНЫЙ файл, и формат этого файла
|
||||
# должен быть один и тот же, кем бы он ни был создан. Держит check-inline-drift.mjs.
|
||||
|
||||
|
||||
# Шапка пространств имён файла таблицы внешнего источника — копия из meta-compile.
|
||||
$script:xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
|
||||
$script:xml = New-Object System.Text.StringBuilder 32768
|
||||
|
||||
function X {
|
||||
param([string]$text)
|
||||
$script:xml.AppendLine($text) | Out-Null
|
||||
}
|
||||
|
||||
function Emit-MLItems {
|
||||
param([string]$indent, $val)
|
||||
if ($val -is [System.Collections.IDictionary]) {
|
||||
foreach ($k in $val.Keys) {
|
||||
X "$indent<v8:item>"; X "$indent`t<v8:lang>$k</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($val[$k])")</v8:content>"; X "$indent</v8:item>"
|
||||
}
|
||||
} elseif ($val -is [System.Management.Automation.PSCustomObject]) {
|
||||
foreach ($p in $val.PSObject.Properties) {
|
||||
X "$indent<v8:item>"; X "$indent`t<v8:lang>$($p.Name)</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$($p.Value)")</v8:content>"; X "$indent</v8:item>"
|
||||
}
|
||||
} else {
|
||||
X "$indent<v8:item>"; X "$indent`t<v8:lang>ru</v8:lang>"; X "$indent`t<v8:content>$(Esc-XmlText "$val")</v8:content>"; X "$indent</v8:item>"
|
||||
}
|
||||
}
|
||||
|
||||
function Emit-MLText {
|
||||
param([string]$indent, [string]$tag, $text)
|
||||
# Пусто (null / пустая строка) → самозакрывающийся тег.
|
||||
if (($null -eq $text) -or (($text -is [string]) -and ($text -eq ''))) {
|
||||
X "$indent<$tag/>"
|
||||
return
|
||||
}
|
||||
X "$indent<$tag>"
|
||||
Emit-MLItems "$indent`t" $text
|
||||
X "$indent</$tag>"
|
||||
}
|
||||
|
||||
|
||||
function Emit-MDRefList {
|
||||
param([string]$indent, [string]$tag, $items)
|
||||
$arr = @(); if ($items) { $arr = @($items) }
|
||||
if ($arr.Count -gt 0) {
|
||||
X "$indent<$tag>"
|
||||
foreach ($it in $arr) { X "$indent`t<xr:Item xsi:type=`"xr:MDObjectRef`">$(Esc-XmlText (Normalize-MDObjectRef "$it"))</xr:Item>" }
|
||||
X "$indent</$tag>"
|
||||
} else {
|
||||
X "$indent<$tag/>"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-EdsTables {
|
||||
param($val)
|
||||
$tables = [ordered]@{}
|
||||
if (-not $val) { return $tables }
|
||||
function New-EdsTableEntry { param($v)
|
||||
if ($v -is [array] -or $v.GetType().Name -eq 'Object[]') {
|
||||
return @{ props = $null; fields = @($v) }
|
||||
}
|
||||
$f = if ($null -ne $v.fields) { @($v.fields) } elseif ($null -ne $v.columns) { @($v.columns) } else { @() }
|
||||
return @{ props = $v; fields = $f }
|
||||
}
|
||||
if ($val -is [array] -or $val.GetType().Name -eq 'Object[]') {
|
||||
foreach ($t in $val) { $tables["$($t.name)"] = New-EdsTableEntry $t }
|
||||
} else {
|
||||
$val.PSObject.Properties | ForEach-Object { $tables[$_.Name] = New-EdsTableEntry $_.Value }
|
||||
}
|
||||
return $tables
|
||||
}
|
||||
|
||||
function Get-EdsFieldRef {
|
||||
param([string]$srcName, [string]$tableName, [string]$fieldName)
|
||||
if (-not $fieldName) { return "" }
|
||||
if ($fieldName -like "ExternalDataSource.*") { return $fieldName }
|
||||
return "ExternalDataSource.$srcName.Table.$tableName.Field.$fieldName"
|
||||
}
|
||||
|
||||
function Emit-EdsFieldRefList {
|
||||
param([string]$indent, [string]$tag, $names, [string]$srcName, [string]$tableName)
|
||||
$list = @($names | Where-Object { $_ })
|
||||
if ($list.Count -eq 0) { X "$indent<$tag/>"; return }
|
||||
X "$indent<$tag>"
|
||||
foreach ($n in $list) {
|
||||
X "$indent`t<xr:Field>$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$n"))</xr:Field>"
|
||||
}
|
||||
X "$indent</$tag>"
|
||||
}
|
||||
|
||||
function Emit-EdsFieldRefScalar {
|
||||
param([string]$indent, [string]$tag, $name, [string]$srcName, [string]$tableName)
|
||||
if (-not $name) { X "$indent<$tag/>"; return }
|
||||
X "$indent<$tag>$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$name"))</$tag>"
|
||||
}
|
||||
|
||||
function Emit-EdsFunction {
|
||||
# $typeXml — уже собранный узел <Type> возвращаемого значения: его рендерит вызывающий навык
|
||||
# своим эмиттером типов. Так тело функции не зависит от того, какой это навык.
|
||||
param([string]$indent, [string]$fnName, $val, [string]$typeXml)
|
||||
$expr = ""
|
||||
$returns = ""
|
||||
$returnValue = $true
|
||||
$fnSynonym = $null
|
||||
$fnComment = ""
|
||||
if ($val -is [string]) {
|
||||
$expr = "$val"
|
||||
} else {
|
||||
$expr = if ($val.expression) { "$($val.expression)" } elseif ($val.expressionInDataSource) { "$($val.expressionInDataSource)" } else { "" }
|
||||
$returns = if ($val.returns) { "$($val.returns)" } elseif ($val.returnType) { "$($val.returnType)" } else { "" }
|
||||
if ($null -ne $val.returnValue) { $returnValue = ($val.returnValue -eq $true) }
|
||||
$fnSynonym = $val.synonym
|
||||
$fnComment = if ($val.comment) { "$($val.comment)" } else { "" }
|
||||
}
|
||||
if (-not $expr) {
|
||||
Write-Error "Функция '$fnName' внешнего источника данных: не задано выражение (ключ expression)."
|
||||
exit 1
|
||||
}
|
||||
$uuid = New-Guid-String
|
||||
X "$indent<Function uuid=`"$uuid`">"
|
||||
X "$indent`t<Properties>"
|
||||
X "$indent`t`t<Name>$(Esc-XmlText $fnName)</Name>"
|
||||
Emit-MLText "$indent`t`t" "Synonym" $fnSynonym
|
||||
if ($fnComment) { X "$indent`t`t<Comment>$(Esc-XmlText $fnComment)</Comment>" } else { X "$indent`t`t<Comment/>" }
|
||||
X "$indent`t`t<ReturnValue>$(if ($returnValue) { 'true' } else { 'false' })</ReturnValue>"
|
||||
if ($returnValue -and $typeXml) {
|
||||
X $typeXml.TrimEnd("`r", "`n")
|
||||
} else {
|
||||
X "$indent`t`t<Type/>"
|
||||
}
|
||||
X "$indent`t`t<ExpressionInDataSource>$(Esc-XmlText $expr)</ExpressionInDataSource>"
|
||||
X "$indent`t</Properties>"
|
||||
X "$indent</Function>"
|
||||
}
|
||||
|
||||
function Emit-EdsTableProperties {
|
||||
# $charXml и $defaultFormsXml — уже собранные блоки <Characteristics> и четыре слота
|
||||
# <Default*Form>: их рендерит вызывающий навык своим эмиттером. Так тело не зависит
|
||||
# от хелперов конкретного навыка и годится для копирования (check-inline-drift).
|
||||
param([string]$indent, [string]$srcName, [string]$tableName, $t, [string]$charXml, [string]$defaultFormsXml)
|
||||
$i = $indent
|
||||
$tblSynonym = if ($t -and $null -ne $t.synonym) { $t.synonym } else { Split-CamelCase $tableName }
|
||||
X "$i<Name>$(Esc-XmlText $tableName)</Name>"
|
||||
Emit-MLText $i "Synonym" $tblSynonym
|
||||
if ($t -and $t.comment) { X "$i<Comment>$(Esc-XmlText "$($t.comment)")</Comment>" } else { X "$i<Comment/>" }
|
||||
|
||||
$tableType = if ($t -and $t.tableType) { "$($t.tableType)" } else { "Table" }
|
||||
X "$i<TableType>$tableType</TableType>"
|
||||
# Имя в источнике по умолчанию равно имени объекта — так поступает и платформа.
|
||||
$nids = if ($t -and $t.nameInDataSource) { "$($t.nameInDataSource)" } elseif ($tableType -eq "Expression") { "" } else { $tableName }
|
||||
if ($nids) { X "$i<NameInDataSource>$(Esc-XmlText $nids)</NameInDataSource>" } else { X "$i<NameInDataSource/>" }
|
||||
$expr = if ($t -and $t.expressionInDataSource) { "$($t.expressionInDataSource)" } elseif ($t -and $t.expression) { "$($t.expression)" } else { "" }
|
||||
if ($expr) { X "$i<ExpressionInDataSource>$(Esc-XmlText $expr)</ExpressionInDataSource>" } else { X "$i<ExpressionInDataSource/>" }
|
||||
$dataType = if ($t -and $t.tableDataType) { "$($t.tableDataType)" } else { "NonobjectData" }
|
||||
X "$i<TableDataType>$dataType</TableDataType>"
|
||||
|
||||
Emit-EdsFieldRefList $i "KeyFields" $(if ($t) { $t.keyFields } else { $null }) $srcName $tableName
|
||||
Emit-EdsFieldRefScalar $i "PresentationField" $(if ($t) { $t.presentationField } else { $null }) $srcName $tableName
|
||||
Emit-EdsFieldRefScalar $i "ParentField" $(if ($t) { $t.parentField } else { $null }) $srcName $tableName
|
||||
# Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения»
|
||||
# различаются формой самого значения (xsi:nil против типизированного).
|
||||
# ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено
|
||||
# на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы
|
||||
# с полем родителя — пустая строка (как после загрузки), а без него — nil.
|
||||
if ($t -and $t.parentField) { X "$i<UnfilledParentValue xsi:type=`"xs:string`"/>" }
|
||||
else { X "$i<UnfilledParentValue xsi:nil=`"true`"/>" }
|
||||
if ($charXml) { X $charXml.TrimEnd("`r", "`n") } else { X "$i<Characteristics/>" }
|
||||
|
||||
X "$i<UseStandardCommands>$(if ($t -and $t.useStandardCommands -eq $false) { 'false' } else { 'true' })</UseStandardCommands>"
|
||||
X "$i<QuickChoice>$(if ($t -and $t.quickChoice -eq $true) { 'true' } else { 'false' })</QuickChoice>"
|
||||
# Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке).
|
||||
# Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка.
|
||||
$ibsGiven = ($t -and $t.PSObject -and $t.PSObject.Properties -and ($t.PSObject.Properties.Name -contains 'inputByString'))
|
||||
$ibs = if ($ibsGiven) { $t.inputByString } elseif ($t -and $t.presentationField) { @($t.presentationField) } else { $null }
|
||||
Emit-EdsFieldRefList $i "InputByString" $ibs $srcName $tableName
|
||||
X "$i<CreateOnInput>$(if ($t -and $t.createOnInput) { "$($t.createOnInput)" } else { 'Auto' })</CreateOnInput>"
|
||||
X "$i<SearchStringModeOnInputByString>$(if ($t -and $t.searchStringModeOnInputByString) { "$($t.searchStringModeOnInputByString)" } else { 'Begin' })</SearchStringModeOnInputByString>"
|
||||
X "$i<ChoiceDataGetModeOnInputByString>$(if ($t -and $t.choiceDataGetModeOnInputByString) { "$($t.choiceDataGetModeOnInputByString)" } else { 'Directly' })</ChoiceDataGetModeOnInputByString>"
|
||||
X "$i<ChoiceHistoryOnInput>$(if ($t -and $t.choiceHistoryOnInput) { "$($t.choiceHistoryOnInput)" } else { 'Auto' })</ChoiceHistoryOnInput>"
|
||||
|
||||
# Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38.
|
||||
if ($defaultFormsXml) { X $defaultFormsXml.TrimEnd("`r", "`n") }
|
||||
else { foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { X "$i<$formTag/>" } }
|
||||
foreach ($presTag in @("ObjectPresentation","ExtendedObjectPresentation","RecordPresentation",
|
||||
"ExtendedRecordPresentation","ListPresentation","ExtendedListPresentation","Explanation")) {
|
||||
$key = $presTag.Substring(0,1).ToLower() + $presTag.Substring(1)
|
||||
Emit-MLText $i $presTag $(if ($t) { $t.$key } else { $null })
|
||||
}
|
||||
X "$i<IncludeHelpInContents>$(if ($t -and $t.includeHelpInContents -eq $true) { 'true' } else { 'false' })</IncludeHelpInContents>"
|
||||
X "$i<ReadOnly>$(if ($t -and $t.readOnly -eq $true) { 'true' } else { 'false' })</ReadOnly>"
|
||||
X "$i<TransactionsIsolationLevel>$(if ($t -and $t.transactionsIsolationLevel) { "$($t.transactionsIsolationLevel)" } else { 'Auto' })</TransactionsIsolationLevel>"
|
||||
Emit-EdsFieldRefScalar $i "DataVersionField" $(if ($t) { $t.dataVersionField } else { $null }) $srcName $tableName
|
||||
X "$i<EditType>$(if ($t -and $t.editType) { "$($t.editType)" } else { 'InDialog' })</EditType>"
|
||||
Emit-MDRefList $i "BasedOn" $(if ($t) { $t.basedOn } else { $null })
|
||||
Emit-EdsFieldRefList $i "DataLockFields" $(if ($t) { $t.dataLockFields } else { $null }) $srcName $tableName
|
||||
X "$i<DataLockControlMode>$(if ($t -and $t.dataLockControlMode) { "$($t.dataLockControlMode)" } else { 'Automatic' })</DataLockControlMode>"
|
||||
}
|
||||
|
||||
function Build-EdsTableXml {
|
||||
# $fieldsXml, $charXml, $defaultFormsXml — уже собранные узлы: их рендерит вызывающий навык
|
||||
# своими эмиттерами. Так тело функции не зависит от того, какой это навык.
|
||||
param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml, [string]$charXml, [string]$defaultFormsXml)
|
||||
$before = $script:xml.Length
|
||||
|
||||
$tableUuid = New-Guid-String
|
||||
X '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
X "<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">"
|
||||
X "`t<Table uuid=`"$tableUuid`">"
|
||||
# InternalInfo у таблицы эмитится здесь, а не через $script:generatedTypes: имя элемента
|
||||
# трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает.
|
||||
X "`t`t<InternalInfo>"
|
||||
foreach ($pair in @(
|
||||
@("ExternalDataSourceTableManager", "Manager"),
|
||||
@("ExternalDataSourceTableObject", "Object"),
|
||||
@("ExternalDataSourceTableRef", "Ref"),
|
||||
@("ExternalDataSourceTableList", "List"),
|
||||
@("ExternalDataSourceTableRecord", "Record"),
|
||||
@("ExternalDataSourceTableRecordSet", "RecordSet"),
|
||||
@("ExternalDataSourceTableRecordKey", "RecordKey"),
|
||||
@("ExternalDataSourceTableRecordManager", "RecordManager"))) {
|
||||
X "`t`t`t<xr:GeneratedType name=`"$($pair[0]).$srcName.$tableName`" category=`"$($pair[1])`">"
|
||||
X "`t`t`t`t<xr:TypeId>$(New-Guid-String)</xr:TypeId>"
|
||||
X "`t`t`t`t<xr:ValueId>$(New-Guid-String)</xr:ValueId>"
|
||||
X "`t`t`t</xr:GeneratedType>"
|
||||
}
|
||||
X "`t`t</InternalInfo>"
|
||||
|
||||
X "`t`t<Properties>"
|
||||
Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props $charXml $defaultFormsXml
|
||||
X "`t`t</Properties>"
|
||||
|
||||
if ($fieldsXml) {
|
||||
X "`t`t<ChildObjects>"
|
||||
X $fieldsXml.TrimEnd("`r", "`n")
|
||||
X "`t`t</ChildObjects>"
|
||||
} else {
|
||||
X "`t`t<ChildObjects/>"
|
||||
}
|
||||
X "`t</Table>"
|
||||
X "</MetaDataObject>"
|
||||
|
||||
$chunk = $script:xml.ToString($before, $script:xml.Length - $before)
|
||||
[void]$script:xml.Remove($before, $script:xml.Length - $before)
|
||||
return $chunk
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
@@ -1903,11 +2199,15 @@ function Process-Add($addDef) {
|
||||
return
|
||||
}
|
||||
|
||||
# Validate allowed
|
||||
$allowed = $script:validChildTypes[$script:objType]
|
||||
if ($allowed -and $childType -notin $allowed) {
|
||||
Warn "$childType not allowed for $($script:objType), skipping"
|
||||
return
|
||||
# Validate allowed. Проверяем НАЛИЧИЕ ключа, а не истинность списка: пустой список
|
||||
# ($script:objType без допустимых детей) трактовался как «ограничений нет», и чужой
|
||||
# ребёнок молча записывался в объект.
|
||||
if ($script:validChildTypes.ContainsKey($script:objType)) {
|
||||
$allowed = $script:validChildTypes[$script:objType]
|
||||
if ($childType -notin $allowed) {
|
||||
Warn "$childType not allowed for $($script:objType), skipping"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
$xmlTag = $script:childTypeToXmlTag[$childType]
|
||||
@@ -1940,6 +2240,103 @@ function Process-Add($addDef) {
|
||||
$existingNames[$parsed.name] = "Attribute"
|
||||
}
|
||||
}
|
||||
"tables" {
|
||||
# Таблица внешнего источника — ОТДЕЛЬНЫЙ файл рядом с файлом источника плюс имя
|
||||
# в его ChildObjects. Единственная операция навыка, создающая файл: без неё
|
||||
# добавить таблицу в существующий источник было нечем (пересборка источника
|
||||
# целиком меняет его uuid и оставляет файлы выброшенных таблиц сиротами).
|
||||
$srcDir = Join-Path (Split-Path -Parent $resolvedPath) $script:objName
|
||||
$tablesDir = Join-Path $srcDir "Tables"
|
||||
foreach ($entry in (Get-EdsTables $items).GetEnumerator()) {
|
||||
$tblName = $entry.Key
|
||||
# Характеристики и слоты форм точечным добавлением не задаются: первые требуют
|
||||
# эмиттера характеристик (живёт в meta-compile), вторые назначает form-add при
|
||||
# добавлении формы. Молча проглотить ключ нельзя — модель решит, что он сработал.
|
||||
$tv = $entry.Value.props
|
||||
if ($tv) {
|
||||
foreach ($k in @("characteristics","defaultObjectForm","defaultRecordForm","defaultListForm","defaultChoiceForm")) {
|
||||
if ($tv.$k) {
|
||||
Warn "Ключ '$k' не поддержан при добавлении таблицы: форму назначает навык form-add, характеристики — навык meta-compile. Таблица '$tblName' пропущена."
|
||||
$tblName = $null; break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $tblName) { continue }
|
||||
if ($existingNames.ContainsKey($tblName)) {
|
||||
Warn "Table '$tblName' already exists, skipping"
|
||||
continue
|
||||
}
|
||||
$tablePath = Join-Path $tablesDir "$tblName.xml"
|
||||
if (Test-Path $tablePath) {
|
||||
Warn "Файл таблицы уже существует: $tablePath — пропускаю"
|
||||
continue
|
||||
}
|
||||
$fieldParts = @()
|
||||
foreach ($f in @($entry.Value.fields)) {
|
||||
$fieldParts += Build-AttributeFragment (Parse-AttributeShorthand $f) "eds-field" "`t`t`t" "Field"
|
||||
}
|
||||
$fieldsXml = $fieldParts -join "`r`n"
|
||||
$tableXml = Build-EdsTableXml $script:objName $tblName $entry.Value $fieldsXml "" ""
|
||||
if (-not (Test-Path $tablesDir)) { New-Item -ItemType Directory -Path $tablesDir -Force | Out-Null }
|
||||
[System.IO.File]::WriteAllText($tablePath, $tableXml.TrimEnd("`r", "`n"), (New-Object System.Text.UTF8Encoding($true)))
|
||||
$fragmentXml = "$indent<Table>$(Esc-XmlText $tblName)</Table>"
|
||||
$nodes = Import-Fragment $fragmentXml
|
||||
$refNode = Find-InsertionPoint "Table" @{ name = $tblName }
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $script:childObjectsEl $node $refNode $indent
|
||||
}
|
||||
Info "Added table: $tblName ($tablePath)"
|
||||
$script:addCount++
|
||||
$existingNames[$tblName] = "Table"
|
||||
}
|
||||
}
|
||||
"functions" {
|
||||
# Функция живёт узлом внутри файла источника — отдельного файла у неё нет.
|
||||
foreach ($prop in $items.PSObject.Properties) {
|
||||
$fnName = $prop.Name
|
||||
if ($existingNames.ContainsKey($fnName)) {
|
||||
Warn "Function '$fnName' already exists, skipping"
|
||||
continue
|
||||
}
|
||||
$before = $script:xml.Length
|
||||
$fnVal = $prop.Value
|
||||
$fnReturns = if ($fnVal -is [string]) { "String" }
|
||||
elseif ($fnVal.returns) { "$($fnVal.returns)" }
|
||||
elseif ($fnVal.returnType) { "$($fnVal.returnType)" } else { "String" }
|
||||
$fnNoValue = (-not ($fnVal -is [string])) -and ($null -ne $fnVal.returnValue) -and ($fnVal.returnValue -ne $true)
|
||||
$fnTypeXml = if ($fnNoValue) { "" } else { Build-ValueTypeXml "$indent`t`t" $fnReturns }
|
||||
Emit-EdsFunction $indent $fnName $fnVal $fnTypeXml
|
||||
$fragmentXml = $script:xml.ToString($before, $script:xml.Length - $before)
|
||||
[void]$script:xml.Remove($before, $script:xml.Length - $before)
|
||||
$nodes = Import-Fragment $fragmentXml
|
||||
$refNode = Find-InsertionPoint "Function" @{ name = $fnName }
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $script:childObjectsEl $node $refNode $indent
|
||||
}
|
||||
Info "Added function: $fnName"
|
||||
$script:addCount++
|
||||
$existingNames[$fnName] = "Function"
|
||||
}
|
||||
}
|
||||
"fields" {
|
||||
# Поле таблицы внешнего источника: тот же парсер реквизита, свой тег и контекст.
|
||||
foreach ($item in $items) {
|
||||
$parsed = Parse-AttributeShorthand $item
|
||||
if ($existingNames.ContainsKey($parsed.name)) {
|
||||
Warn "Field '$($parsed.name)' already exists, skipping"
|
||||
continue
|
||||
}
|
||||
$fragmentXml = Build-AttributeFragment $parsed "eds-field" $indent "Field"
|
||||
$nodes = Import-Fragment $fragmentXml
|
||||
$refNode = Find-InsertionPoint "Field" $parsed
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $script:childObjectsEl $node $refNode $indent
|
||||
}
|
||||
Info "Added field: $($parsed.name)"
|
||||
$script:addCount++
|
||||
$existingNames[$parsed.name] = "Field"
|
||||
}
|
||||
}
|
||||
"tabularSections" {
|
||||
foreach ($item in $items) {
|
||||
$tsName = if ($item -is [string]) { "$item" } else { "$($item.name)" }
|
||||
@@ -2031,24 +2428,36 @@ function Process-Add($addDef) {
|
||||
$existingNames[$colName] = "Column"
|
||||
}
|
||||
}
|
||||
{ $_ -in @("forms","templates","commands") } {
|
||||
$tagMap = @{ "forms" = "Form"; "templates" = "Template"; "commands" = "Command" }
|
||||
$tag = $tagMap[$childType]
|
||||
{ $_ -in @("forms","templates") } {
|
||||
# Форма и макет регистрируются голым текстом (<Form>Имя</Form>) и требуют ещё и
|
||||
# собственных файлов. И то и другое делают form-add / template-add — дублировать
|
||||
# эту ответственность здесь нельзя: получится висячая регистрация без файла.
|
||||
$skillName = if ($childType -eq "forms") { "form-add" } else { "template-add" }
|
||||
$whatName = if ($childType -eq "forms") { "Форму" } else { "Макет" }
|
||||
Warn "$whatName добавляет навык $skillName (он создаёт и файл, и запись в ChildObjects). meta-edit этого не делает — операция пропущена."
|
||||
}
|
||||
"commands" {
|
||||
foreach ($item in $items) {
|
||||
$itemName = if ($item -is [string]) { "$item" } else { "$($item.name)" }
|
||||
if ($existingNames.ContainsKey($itemName)) {
|
||||
Warn "$tag '$itemName' already exists, skipping"
|
||||
Warn "Command '$itemName' already exists, skipping"
|
||||
continue
|
||||
}
|
||||
$fragmentXml = Build-SimpleChildFragment $tag $itemName $indent
|
||||
# У команды есть модуль обработчика (Commands/<Имя>/Ext/CommandModule.bsl) — в корпусе
|
||||
# он есть у всех команд без исключения. Пишем ту же заготовку, что и meta-compile.
|
||||
$cmdExtDir = Join-Path (Join-Path (Join-Path (Join-Path (Split-Path -Parent $resolvedPath) $script:objName) "Commands") $itemName) "Ext"
|
||||
$cmdModPath = Join-Path $cmdExtDir "CommandModule.bsl"
|
||||
if (-not (Test-Path $cmdExtDir)) { New-Item -ItemType Directory -Path $cmdExtDir -Force | Out-Null }
|
||||
[System.IO.File]::WriteAllText($cmdModPath, "&НаКлиенте`r`nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)`r`n`r`n`t// Вставьте обработчик команды.`r`n`r`nКонецПроцедуры`r`n", (New-Object System.Text.UTF8Encoding($true)))
|
||||
$fragmentXml = Build-CommandFragment $itemName $indent
|
||||
$nodes = Import-Fragment $fragmentXml
|
||||
$refNode = Find-InsertionPoint $tag @{ after = ""; before = "" }
|
||||
$refNode = Find-InsertionPoint "Command" @{ after = ""; before = "" }
|
||||
foreach ($node in $nodes) {
|
||||
Insert-BeforeElement $script:childObjectsEl $node $refNode $indent
|
||||
}
|
||||
Info "Added $($tag.ToLower()): $itemName"
|
||||
Info "Added command: $itemName ($cmdModPath)"
|
||||
$script:addCount++
|
||||
$existingNames[$itemName] = $tag
|
||||
$existingNames[$itemName] = "Command"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2073,6 +2482,13 @@ function Process-Remove($removeDef) {
|
||||
Warn "Cannot remove properties — use modify instead"
|
||||
return
|
||||
}
|
||||
if ($childType -in @("forms","templates")) {
|
||||
# Снять регистрацию мало — надо удалить и файлы; это делают form-remove / template-remove.
|
||||
$skillName = if ($childType -eq "forms") { "form-remove" } else { "template-remove" }
|
||||
$whatName = if ($childType -eq "forms") { "Форму" } else { "Макет" }
|
||||
Warn "$whatName удаляет навык $skillName (он убирает и файлы, и запись в ChildObjects). meta-edit этого не делает — операция пропущена."
|
||||
return
|
||||
}
|
||||
|
||||
$xmlTag = $script:childTypeToXmlTag[$childType]
|
||||
if (-not $xmlTag -or -not $script:childObjectsEl) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-edit v1.42 — Edit existing 1C metadata object XML
|
||||
# meta-edit v1.50 — Edit existing 1C metadata object XML
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -322,6 +322,8 @@ CFG_NS = "http://v8.1c.ru/8.1/data/enterprise/current-config"
|
||||
# Версия формата правимого файла — из его же корня. Нужна эмиттерам: часть свойств
|
||||
# появилась в поздних версиях (напр. <Color> у значения перечисления — в 2.21).
|
||||
is_format_221 = False
|
||||
# Версия формата файла объекта: ею же помечается файл таблицы внешнего источника.
|
||||
format_version = "2.17"
|
||||
# Префикс current-config, объявленный в КОРНЕ правимого файла (у платформы — cfg).
|
||||
# None = корень его не объявляет → эмиттер ссылочных типов остаётся на локальной форме.
|
||||
cfg_prefix = None
|
||||
@@ -427,7 +429,8 @@ valid_enum_values = {
|
||||
'WriteMode': ['Independent', 'RecorderSubordinate'],
|
||||
'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'],
|
||||
'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'],
|
||||
'DataLockControlMode': ['Automatic', 'Managed'],
|
||||
# AutomaticAndManaged — только у внешнего источника данных и его таблиц.
|
||||
'DataLockControlMode': ['Automatic', 'Managed', 'AutomaticAndManaged'],
|
||||
'FullTextSearch': ['Use', 'DontUse'],
|
||||
'DataHistory': ['Use', 'DontUse'],
|
||||
'DefaultPresentation': ['AsDescription', 'AsCode'],
|
||||
@@ -502,6 +505,9 @@ child_type_synonyms = {
|
||||
"templates": "templates", "макеты": "templates",
|
||||
"commands": "commands", "команды": "commands",
|
||||
"properties": "properties", "свойства": "properties",
|
||||
"fields": "fields", "поля": "fields",
|
||||
"tables": "tables", "таблицы": "tables",
|
||||
"functions": "functions", "функции": "functions",
|
||||
}
|
||||
|
||||
type_synonyms = {
|
||||
@@ -751,17 +757,23 @@ def import_fragment(xml_string):
|
||||
|
||||
def get_child_indent(container):
|
||||
"""Detect indentation of children inside a container element."""
|
||||
# В контейнере с детьми первый пробельный узел — это отступ ПЕРЕД первым ребёнком.
|
||||
# В пустом (только что раскрытом) единственный пробельный узел — отступ ЗАКРЫВАЮЩЕГО
|
||||
# тега, то есть уровень самого контейнера: ребёнку нужен на табуляцию глубже. Без этой
|
||||
# поправки первый ребёнок вставал вровень с <ChildObjects>.
|
||||
has_elements = len(container) > 0
|
||||
extra = "" if has_elements else "\t"
|
||||
# Check container.text (text before first child)
|
||||
if container.text and "\n" in container.text:
|
||||
after_nl = container.text.rsplit("\n", 1)[-1]
|
||||
if after_nl and not after_nl.strip():
|
||||
return after_nl
|
||||
return after_nl + extra
|
||||
# Check tail of child elements
|
||||
for child in container:
|
||||
if child.tail and "\n" in child.tail:
|
||||
after_nl = child.tail.rsplit("\n", 1)[-1]
|
||||
if after_nl and not after_nl.strip():
|
||||
return after_nl
|
||||
return after_nl + extra
|
||||
# Fallback: count depth
|
||||
depth = 0
|
||||
current = container
|
||||
@@ -966,6 +978,10 @@ def parse_attribute_shorthand(val):
|
||||
"indexing": normalize_enum_value("Indexing", str(val.get("indexing", ""))) if val.get("indexing") else "",
|
||||
"after": str(val.get("after", "")),
|
||||
"before": str(val.get("before", "")),
|
||||
# Поле таблицы внешнего источника (контекст eds-field).
|
||||
"nameInDataSource": str(val.get("nameInDataSource", "")),
|
||||
"readOnly": val.get("readOnly") is True,
|
||||
"allowNull": val.get("allowNull") is True,
|
||||
}
|
||||
# Map flags to properties
|
||||
if "req" in result["flags"] and not result["fillChecking"]:
|
||||
@@ -1001,6 +1017,8 @@ def get_attribute_context():
|
||||
"""Determine attribute context from object type."""
|
||||
if obj_type == "Catalog":
|
||||
return "catalog"
|
||||
if obj_type == "Table":
|
||||
return "eds-field"
|
||||
if obj_type == "Document":
|
||||
return "document"
|
||||
if obj_type in ("InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister"):
|
||||
@@ -1056,7 +1074,7 @@ RESERVED_BY_CONTEXT = {
|
||||
}
|
||||
|
||||
|
||||
def build_attribute_fragment(parsed, context, indent):
|
||||
def build_attribute_fragment(parsed, context, indent, elem_tag="Attribute"):
|
||||
"""Build XML fragment string for an Attribute element."""
|
||||
if not context:
|
||||
context = get_attribute_context()
|
||||
@@ -1068,13 +1086,13 @@ def build_attribute_fragment(parsed, context, indent):
|
||||
if attr_name.lower() in ctx_reserved:
|
||||
print(f"meta-edit: имя реквизита '{attr_name}' зарезервировано стандартным реквизитом объекта '{context}'. Выберите другое имя.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif context not in ('tabular', 'processor-tabular') and (attr_name in RESERVED_ATTR_NAMES or attr_name in RESERVED_ATTR_NAMES_RU):
|
||||
elif context not in ('tabular', 'processor-tabular', 'eds-field') and (attr_name in RESERVED_ATTR_NAMES or attr_name in RESERVED_ATTR_NAMES_RU):
|
||||
print(f"WARNING: Attribute '{attr_name}' conflicts with a standard attribute name. This may cause errors when loading into 1C.", file=sys.stderr)
|
||||
|
||||
uid = new_uuid()
|
||||
lines = []
|
||||
|
||||
lines.append(f'{indent}<Attribute uuid="{uid}">')
|
||||
lines.append(f'{indent}<{elem_tag} uuid="{uid}">')
|
||||
lines.append(f"{indent}\t<Properties>")
|
||||
lines.append(f"{indent}\t\t<Name>{esc_xml_text(parsed['name'])}</Name>")
|
||||
lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", parsed["synonym"]))
|
||||
@@ -1113,21 +1131,35 @@ def build_attribute_fragment(parsed, context, indent):
|
||||
fill_checking = parsed["fillChecking"]
|
||||
lines.append(f"{indent}\t\t<FillChecking>{fill_checking}</FillChecking>")
|
||||
|
||||
lines.append(f"{indent}\t\t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>")
|
||||
# Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm
|
||||
# у него стоит ПОСЛЕ ChoiceHistoryOnInput — порядок снят с выгрузки платформы.
|
||||
if context != "eds-field":
|
||||
lines.append(f"{indent}\t\t<ChoiceFoldersAndItems>Items</ChoiceFoldersAndItems>")
|
||||
lines.append(f"{indent}\t\t<ChoiceParameterLinks/>")
|
||||
lines.append(f"{indent}\t\t<ChoiceParameters/>")
|
||||
lines.append(f"{indent}\t\t<QuickChoice>Auto</QuickChoice>")
|
||||
lines.append(f"{indent}\t\t<CreateOnInput>Auto</CreateOnInput>")
|
||||
lines.append(f"{indent}\t\t<ChoiceForm/>")
|
||||
lines.append(f"{indent}\t\t<LinkByType/>")
|
||||
if context != "eds-field":
|
||||
lines.append(f"{indent}\t\t<ChoiceForm/>")
|
||||
lines.append(f"{indent}\t\t<LinkByType/>")
|
||||
lines.append(f"{indent}\t\t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>")
|
||||
|
||||
if context == "eds-field":
|
||||
lines.append(f"{indent}\t\t<ChoiceForm/>")
|
||||
nids = parsed.get("nameInDataSource") or parsed["name"]
|
||||
lines.append(f"{indent}\t\t<NameInDataSource>{esc_xml_text(str(nids))}</NameInDataSource>")
|
||||
ro = "true" if (parsed.get("readOnly") is True or "readonly" in parsed["flags"]) else "false"
|
||||
lines.append(f"{indent}\t\t<ReadOnly>{ro}</ReadOnly>")
|
||||
an = "true" if (parsed.get("allowNull") is True or "nullable" in parsed["flags"]) else "false"
|
||||
lines.append(f"{indent}\t\t<AllowNull>{an}</AllowNull>")
|
||||
|
||||
# Use -- catalog only
|
||||
if context == "catalog":
|
||||
lines.append(f"{indent}\t\t<Use>ForItem</Use>")
|
||||
|
||||
# Indexing/FullTextSearch/DataHistory -- not for non-stored objects
|
||||
if context not in ("processor", "processor-tabular"):
|
||||
# Поля внешнего источника: индексами чужой таблицы 1С не владеет.
|
||||
if context not in ("processor", "processor-tabular", "eds-field"):
|
||||
indexing = "DontIndex"
|
||||
if "index" in parsed["flags"]:
|
||||
indexing = "Index"
|
||||
@@ -1140,7 +1172,7 @@ def build_attribute_fragment(parsed, context, indent):
|
||||
lines.append(f"{indent}\t\t<DataHistory>Use</DataHistory>")
|
||||
|
||||
lines.append(f"{indent}\t</Properties>")
|
||||
lines.append(f"{indent}</Attribute>")
|
||||
lines.append(f"{indent}</{elem_tag}>")
|
||||
return "\r\n".join(lines)
|
||||
|
||||
|
||||
@@ -1452,34 +1484,32 @@ def build_column_fragment(col_def, indent):
|
||||
return "\r\n".join(lines)
|
||||
|
||||
|
||||
def build_simple_child_fragment(tag_name, name, indent):
|
||||
"""Build XML fragment for Form, Template, Command -- just a name wrapper."""
|
||||
def build_command_fragment(name, indent):
|
||||
"""Build XML fragment for a Command (it has no separate file -- all inline).
|
||||
|
||||
Property order is the platform's -- do not reorder.
|
||||
"""
|
||||
uid = new_uuid()
|
||||
synonym = split_camel_case(name)
|
||||
lines = []
|
||||
lines.append(f'{indent}<{tag_name} uuid="{uid}">')
|
||||
lines.append(f'{indent}<Command uuid="{uid}">')
|
||||
lines.append(f"{indent}\t<Properties>")
|
||||
lines.append(f"{indent}\t\t<Name>{esc_xml_text(name)}</Name>")
|
||||
lines.append(build_mltext_xml(f"{indent}\t\t", "Synonym", synonym))
|
||||
lines.append(f"{indent}\t\t<Comment/>")
|
||||
# Forms get additional properties
|
||||
if tag_name == "Form":
|
||||
lines.append(f"{indent}\t\t<FormType>Ordinary</FormType>")
|
||||
lines.append(f"{indent}\t\t<IncludeHelpInContents>false</IncludeHelpInContents>")
|
||||
lines.append(f"{indent}\t\t<UsePurposes/>")
|
||||
if tag_name == "Template":
|
||||
lines.append(f"{indent}\t\t<TemplateType>SpreadsheetDocument</TemplateType>")
|
||||
if tag_name == "Command":
|
||||
lines.append(f"{indent}\t\t<Group>FormNavigationPanelGoTo</Group>")
|
||||
lines.append(f"{indent}\t\t<Representation>Auto</Representation>")
|
||||
lines.append(f"{indent}\t\t<ToolTip/>")
|
||||
lines.append(f"{indent}\t\t<Picture/>")
|
||||
lines.append(f"{indent}\t\t<Shortcut/>")
|
||||
lines.append(f"{indent}\t\t<Group>FormNavigationPanelGoTo</Group>")
|
||||
lines.append(f"{indent}\t\t<CommandParameterType/>")
|
||||
lines.append(f"{indent}\t\t<ParameterUseMode>Single</ParameterUseMode>")
|
||||
lines.append(f"{indent}\t\t<ModifiesData>false</ModifiesData>")
|
||||
lines.append(f"{indent}\t\t<Representation>Auto</Representation>")
|
||||
lines.append(f"{indent}\t\t<ToolTip/>")
|
||||
lines.append(f"{indent}\t\t<Picture/>")
|
||||
lines.append(f"{indent}\t\t<Shortcut/>")
|
||||
lines.append(f"{indent}\t\t<OnMainServerUnavalableBehavior>Auto</OnMainServerUnavalableBehavior>")
|
||||
lines.append(f"{indent}\t</Properties>")
|
||||
lines.append(f"{indent}</{tag_name}>")
|
||||
lines.append(f"{indent}</Command>")
|
||||
return "\r\n".join(lines)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Name uniqueness check
|
||||
# ============================================================
|
||||
@@ -1496,7 +1526,13 @@ def get_all_child_names():
|
||||
if localname(gc) == "Properties":
|
||||
props_el = gc
|
||||
break
|
||||
# Часть детей регистрируется голым текстом, без <Properties>: <Table>Имя</Table>,
|
||||
# <Form>Имя</Form>, <Template>Имя</Template>. Пропуская их, проверка «уже существует»
|
||||
# становилась мёртвой — повторное добавление давало второй такой же узел.
|
||||
if props_el is None:
|
||||
n = (child.text or "").strip()
|
||||
if n:
|
||||
names[n] = localname(child)
|
||||
continue
|
||||
for gc in props_el:
|
||||
if localname(gc) == "Name":
|
||||
@@ -1529,11 +1565,13 @@ valid_child_types = {
|
||||
"CalculationRegister": ["dimensions", "resources", "attributes", "forms", "templates", "commands"],
|
||||
"DocumentJournal": ["columns", "forms", "templates", "commands"],
|
||||
"Constant": ["forms"],
|
||||
"ExternalDataSource": ["tables", "functions"],
|
||||
"Table": ["fields", "forms", "templates", "commands"],
|
||||
}
|
||||
|
||||
# Canonical child order in ChildObjects
|
||||
child_order = [
|
||||
"Resource", "Dimension", "Attribute", "TabularSection",
|
||||
"Resource", "Dimension", "Attribute", "TabularSection", "Field", "Table", "Function",
|
||||
"AccountingFlag", "ExtDimensionAccountingFlag",
|
||||
"EnumValue", "Column", "AddressingAttribute", "Recalculation",
|
||||
"Form", "Template", "Command",
|
||||
@@ -1550,11 +1588,277 @@ child_type_to_xml_tag = {
|
||||
"forms": "Form",
|
||||
"templates": "Template",
|
||||
"commands": "Command",
|
||||
"fields": "Field",
|
||||
"tables": "Table",
|
||||
"functions": "Function",
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# DSL key normalization
|
||||
# Section 8b: Внешние источники данных — копии из meta-compile
|
||||
# ============================================================
|
||||
# Тела ниже скопированы из meta-compile и обязаны совпадать с ним байт в байт:
|
||||
# таблица внешнего источника собирается в ОТДЕЛЬНЫЙ файл, и формат этого файла
|
||||
# должен быть один и тот же, кем бы он ни был создан. Держит check-inline-drift.mjs.
|
||||
|
||||
xmlns_decl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||
lines = []
|
||||
|
||||
|
||||
def X(text):
|
||||
lines.append(text)
|
||||
|
||||
# ML-значение: строка → один <v8:item> ru; dict {lang: content} → item на язык (в порядке ключей).
|
||||
|
||||
def emit_ml_items(indent, val):
|
||||
if isinstance(val, dict):
|
||||
for k, v in val.items():
|
||||
X(f'{indent}<v8:item>')
|
||||
X(f'{indent}\t<v8:lang>{k}</v8:lang>')
|
||||
X(f'{indent}\t<v8:content>{esc_xml_text(str(v))}</v8:content>')
|
||||
X(f'{indent}</v8:item>')
|
||||
else:
|
||||
X(f'{indent}<v8:item>')
|
||||
X(f'{indent}\t<v8:lang>ru</v8:lang>')
|
||||
X(f'{indent}\t<v8:content>{esc_xml_text(str(val))}</v8:content>')
|
||||
X(f'{indent}</v8:item>')
|
||||
|
||||
def emit_mltext(indent, tag, text):
|
||||
# Пусто (None / '') → самозакрывающийся тег.
|
||||
if text is None or (isinstance(text, str) and text == ''):
|
||||
X(f'{indent}<{tag}/>')
|
||||
return
|
||||
X(f'{indent}<{tag}>')
|
||||
emit_ml_items(f'{indent}\t', text)
|
||||
X(f'{indent}</{tag}>')
|
||||
|
||||
|
||||
def emit_md_ref_list(indent, tag, items):
|
||||
"""Список MDObjectRef (Documents/RegisterRecords/DocumentMap/…) с <xr:Item>. omit-on-empty."""
|
||||
arr = list(items) if items else []
|
||||
if arr:
|
||||
X(f'{indent}<{tag}>')
|
||||
for it in arr:
|
||||
X(f'{indent}\t<xr:Item xsi:type="xr:MDObjectRef">{esc_xml_text(normalize_md_object_ref(str(it)))}</xr:Item>')
|
||||
X(f'{indent}</{tag}>')
|
||||
else:
|
||||
X(f'{indent}<{tag}/>')
|
||||
|
||||
def get_eds_tables(val):
|
||||
"""Таблицы: dict имя → массив полей ЛИБО объект со свойствами и ключом fields/columns."""
|
||||
tables = {}
|
||||
if not val:
|
||||
return tables
|
||||
|
||||
def entry(v):
|
||||
if isinstance(v, list):
|
||||
return {'props': None, 'fields': list(v)}
|
||||
f = v.get('fields') if v.get('fields') is not None else v.get('columns')
|
||||
return {'props': v, 'fields': list(f) if f else []}
|
||||
|
||||
if isinstance(val, list):
|
||||
for t in val:
|
||||
tables[str(t.get('name'))] = entry(t)
|
||||
else:
|
||||
for k, v in val.items():
|
||||
tables[k] = entry(v)
|
||||
return tables
|
||||
|
||||
def get_eds_field_ref(src_name, table_name, field_name):
|
||||
"""Ссылка на поле таблицы: в DSL короткое имя, в XML — полный путь."""
|
||||
if not field_name:
|
||||
return ''
|
||||
if str(field_name).startswith('ExternalDataSource.'):
|
||||
return str(field_name)
|
||||
return f'ExternalDataSource.{src_name}.Table.{table_name}.Field.{field_name}'
|
||||
|
||||
def emit_eds_field_ref_list(indent, tag, names, src_name, table_name):
|
||||
items = [n for n in (names or []) if n]
|
||||
if not items:
|
||||
X(f'{indent}<{tag}/>')
|
||||
return
|
||||
X(f'{indent}<{tag}>')
|
||||
for n in items:
|
||||
X(f'{indent}\t<xr:Field>{esc_xml_text(get_eds_field_ref(src_name, table_name, n))}</xr:Field>')
|
||||
X(f'{indent}</{tag}>')
|
||||
|
||||
def emit_eds_field_ref_scalar(indent, tag, name, src_name, table_name):
|
||||
if not name:
|
||||
X(f'{indent}<{tag}/>')
|
||||
return
|
||||
X(f'{indent}<{tag}>{esc_xml_text(get_eds_field_ref(src_name, table_name, name))}</{tag}>')
|
||||
|
||||
def emit_eds_function(indent, fn_name, val, type_xml):
|
||||
"""Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо
|
||||
в выражении как &1, &2 (см. reference/external-data-source.md).
|
||||
|
||||
type_xml — уже собранный узел <Type> возвращаемого значения: его рендерит вызывающий навык
|
||||
своим эмиттером типов. Так тело функции не зависит от того, какой это навык."""
|
||||
fn_synonym = None
|
||||
fn_comment = ''
|
||||
returns = ''
|
||||
return_value = True
|
||||
if isinstance(val, str):
|
||||
expr = val
|
||||
else:
|
||||
expr = str(val.get('expression') or val.get('expressionInDataSource') or '')
|
||||
returns = str(val.get('returns') or val.get('returnType') or '')
|
||||
if val.get('returnValue') is not None:
|
||||
return_value = val.get('returnValue') is True
|
||||
fn_synonym = val.get('synonym')
|
||||
fn_comment = str(val['comment']) if val.get('comment') else ''
|
||||
if not expr:
|
||||
print(f"ERROR: Функция '{fn_name}' внешнего источника данных: не задано выражение (ключ expression).",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
X(f'{indent}<Function uuid="{new_uuid()}">')
|
||||
X(f'{indent}\t<Properties>')
|
||||
X(f'{indent}\t\t<Name>{esc_xml_text(fn_name)}</Name>')
|
||||
emit_mltext(f'{indent}\t\t', 'Synonym', fn_synonym)
|
||||
if fn_comment:
|
||||
X(f'{indent}\t\t<Comment>{esc_xml_text(fn_comment)}</Comment>')
|
||||
else:
|
||||
X(f'{indent}\t\t<Comment/>')
|
||||
X(f'{indent}\t\t<ReturnValue>{"true" if return_value else "false"}</ReturnValue>')
|
||||
if return_value and type_xml:
|
||||
X(type_xml.rstrip('\r\n'))
|
||||
else:
|
||||
X(f'{indent}\t\t<Type/>')
|
||||
X(f'{indent}\t\t<ExpressionInDataSource>{esc_xml_text(expr)}</ExpressionInDataSource>')
|
||||
X(f'{indent}\t</Properties>')
|
||||
X(f'{indent}</Function>')
|
||||
|
||||
def emit_eds_table_properties(indent, src_name, table_name, t, char_xml, default_forms_xml):
|
||||
"""Свойства таблицы: 38 узлов в порядке выгрузки платформы.
|
||||
|
||||
char_xml и default_forms_xml — уже собранные блоки <Characteristics> и четыре слота
|
||||
<Default*Form>: их рендерит вызывающий навык своим эмиттером. Так тело не зависит
|
||||
от хелперов конкретного навыка и годится для копирования (check-inline-drift)."""
|
||||
i = indent
|
||||
t = t or {}
|
||||
tbl_synonym = t['synonym'] if t.get('synonym') is not None else split_camel_case(table_name)
|
||||
X(f'{i}<Name>{esc_xml_text(table_name)}</Name>')
|
||||
emit_mltext(i, 'Synonym', tbl_synonym)
|
||||
if t.get('comment'):
|
||||
X(f'{i}<Comment>{esc_xml_text(str(t["comment"]))}</Comment>')
|
||||
else:
|
||||
X(f'{i}<Comment/>')
|
||||
|
||||
table_type = str(t.get('tableType') or 'Table')
|
||||
X(f'{i}<TableType>{table_type}</TableType>')
|
||||
# Имя в источнике по умолчанию равно имени объекта — так поступает и платформа.
|
||||
if t.get('nameInDataSource'):
|
||||
nids = str(t['nameInDataSource'])
|
||||
elif table_type == 'Expression':
|
||||
nids = ''
|
||||
else:
|
||||
nids = table_name
|
||||
X(f'{i}<NameInDataSource>{esc_xml_text(nids)}</NameInDataSource>' if nids else f'{i}<NameInDataSource/>')
|
||||
expr = str(t.get('expressionInDataSource') or t.get('expression') or '')
|
||||
X(f'{i}<ExpressionInDataSource>{esc_xml_text(expr)}</ExpressionInDataSource>' if expr else f'{i}<ExpressionInDataSource/>')
|
||||
X(f'{i}<TableDataType>{t.get("tableDataType") or "NonobjectData"}</TableDataType>')
|
||||
|
||||
emit_eds_field_ref_list(i, 'KeyFields', t.get('keyFields'), src_name, table_name)
|
||||
emit_eds_field_ref_scalar(i, 'PresentationField', t.get('presentationField'), src_name, table_name)
|
||||
emit_eds_field_ref_scalar(i, 'ParentField', t.get('parentField'), src_name, table_name)
|
||||
# Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения»
|
||||
# различаются формой самого значения (xsi:nil против типизированного).
|
||||
# ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено
|
||||
# на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы
|
||||
# с полем родителя — пустая строка (как после загрузки), а без него — nil.
|
||||
if t.get('parentField'):
|
||||
X(f'{i}<UnfilledParentValue xsi:type="xs:string"/>')
|
||||
else:
|
||||
X(f'{i}<UnfilledParentValue xsi:nil="true"/>')
|
||||
if char_xml:
|
||||
X(char_xml.rstrip('\r\n'))
|
||||
else:
|
||||
X(f'{i}<Characteristics/>')
|
||||
|
||||
X(f'{i}<UseStandardCommands>{"false" if t.get("useStandardCommands") is False else "true"}</UseStandardCommands>')
|
||||
X(f'{i}<QuickChoice>{"true" if t.get("quickChoice") is True else "false"}</QuickChoice>')
|
||||
# Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке).
|
||||
# Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка.
|
||||
if 'inputByString' in t:
|
||||
ibs = t.get('inputByString')
|
||||
elif t.get('presentationField'):
|
||||
ibs = [t['presentationField']]
|
||||
else:
|
||||
ibs = None
|
||||
emit_eds_field_ref_list(i, 'InputByString', ibs, src_name, table_name)
|
||||
X(f'{i}<CreateOnInput>{t.get("createOnInput") or "Auto"}</CreateOnInput>')
|
||||
X(f'{i}<SearchStringModeOnInputByString>{t.get("searchStringModeOnInputByString") or "Begin"}</SearchStringModeOnInputByString>')
|
||||
X(f'{i}<ChoiceDataGetModeOnInputByString>{t.get("choiceDataGetModeOnInputByString") or "Directly"}</ChoiceDataGetModeOnInputByString>')
|
||||
X(f'{i}<ChoiceHistoryOnInput>{t.get("choiceHistoryOnInput") or "Auto"}</ChoiceHistoryOnInput>')
|
||||
|
||||
# Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38.
|
||||
if default_forms_xml:
|
||||
X(default_forms_xml.rstrip('\r\n'))
|
||||
else:
|
||||
for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'):
|
||||
X(f'{i}<{form_tag}/>')
|
||||
for pres_tag in ('ObjectPresentation', 'ExtendedObjectPresentation', 'RecordPresentation',
|
||||
'ExtendedRecordPresentation', 'ListPresentation', 'ExtendedListPresentation', 'Explanation'):
|
||||
key = pres_tag[0].lower() + pres_tag[1:]
|
||||
emit_mltext(i, pres_tag, t.get(key))
|
||||
X(f'{i}<IncludeHelpInContents>{"true" if t.get("includeHelpInContents") is True else "false"}</IncludeHelpInContents>')
|
||||
X(f'{i}<ReadOnly>{"true" if t.get("readOnly") is True else "false"}</ReadOnly>')
|
||||
X(f'{i}<TransactionsIsolationLevel>{t.get("transactionsIsolationLevel") or "Auto"}</TransactionsIsolationLevel>')
|
||||
emit_eds_field_ref_scalar(i, 'DataVersionField', t.get('dataVersionField'), src_name, table_name)
|
||||
X(f'{i}<EditType>{t.get("editType") or "InDialog"}</EditType>')
|
||||
emit_md_ref_list(i, 'BasedOn', t.get('basedOn'))
|
||||
emit_eds_field_ref_list(i, 'DataLockFields', t.get('dataLockFields'), src_name, table_name)
|
||||
X(f'{i}<DataLockControlMode>{t.get("dataLockControlMode") or "Automatic"}</DataLockControlMode>')
|
||||
|
||||
EDS_TABLE_GENERATED_TYPES = (
|
||||
('ExternalDataSourceTableManager', 'Manager'),
|
||||
('ExternalDataSourceTableObject', 'Object'),
|
||||
('ExternalDataSourceTableRef', 'Ref'),
|
||||
('ExternalDataSourceTableList', 'List'),
|
||||
('ExternalDataSourceTableRecord', 'Record'),
|
||||
('ExternalDataSourceTableRecordSet', 'RecordSet'),
|
||||
('ExternalDataSourceTableRecordKey', 'RecordKey'),
|
||||
('ExternalDataSourceTableRecordManager', 'RecordManager'),
|
||||
)
|
||||
|
||||
|
||||
def build_eds_table_xml(src_name, table_name, entry, fields_xml, char_xml, default_forms_xml):
|
||||
"""Отдельный XML-документ таблицы. fields_xml, char_xml, default_forms_xml — уже собранные
|
||||
узлы: их рендерит вызывающий навык своими эмиттерами, поэтому тело не зависит от того,
|
||||
какой это навык.
|
||||
Возвращает строку: X пишет в общий список строк,
|
||||
поэтому «перехват» — запомнить длину, отдать эмиттерам, срезать добавленное
|
||||
(в ps1-порте тот же приём выражен через StringBuilder — различие рантаймов, не логики)."""
|
||||
before = len(lines)
|
||||
|
||||
X('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
X(f'<MetaDataObject {xmlns_decl} version="{format_version}">')
|
||||
X(f'\t<Table uuid="{new_uuid()}">')
|
||||
# InternalInfo у таблицы эмитится здесь, а не через generated_types: имя элемента
|
||||
# трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает.
|
||||
X('\t\t<InternalInfo>')
|
||||
for prefix, category in EDS_TABLE_GENERATED_TYPES:
|
||||
X(f'\t\t\t<xr:GeneratedType name="{prefix}.{src_name}.{table_name}" category="{category}">')
|
||||
X(f'\t\t\t\t<xr:TypeId>{new_uuid()}</xr:TypeId>')
|
||||
X(f'\t\t\t\t<xr:ValueId>{new_uuid()}</xr:ValueId>')
|
||||
X('\t\t\t</xr:GeneratedType>')
|
||||
X('\t\t</InternalInfo>')
|
||||
|
||||
X('\t\t<Properties>')
|
||||
emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props'], char_xml, default_forms_xml)
|
||||
X('\t\t</Properties>')
|
||||
|
||||
if fields_xml:
|
||||
X('\t\t<ChildObjects>')
|
||||
X(fields_xml.rstrip('\r\n'))
|
||||
X('\t\t</ChildObjects>')
|
||||
else:
|
||||
X('\t\t<ChildObjects/>')
|
||||
X('\t</Table>')
|
||||
X('</MetaDataObject>')
|
||||
|
||||
chunk = '\r\n'.join(lines[before:])
|
||||
del lines[before:]
|
||||
return chunk
|
||||
|
||||
|
||||
def resolve_operation_key(key):
|
||||
@@ -1854,11 +2158,13 @@ def process_add(add_def):
|
||||
warn(f"Unknown add child type: {raw_key}")
|
||||
continue
|
||||
|
||||
# Validate allowed
|
||||
allowed = valid_child_types.get(obj_type)
|
||||
if allowed and child_type not in allowed:
|
||||
warn(f"{child_type} not allowed for {obj_type}, skipping")
|
||||
continue
|
||||
# Validate allowed. Проверяем НАЛИЧИЕ ключа, а не истинность списка: пустой список
|
||||
# (объект без допустимых детей) трактовался как «ограничений нет», и чужой ребёнок
|
||||
# молча записывался в объект.
|
||||
if obj_type in valid_child_types:
|
||||
if child_type not in valid_child_types[obj_type]:
|
||||
warn(f"{child_type} not allowed for {obj_type}, skipping")
|
||||
continue
|
||||
|
||||
xml_tag = child_type_to_xml_tag.get(child_type)
|
||||
if not xml_tag:
|
||||
@@ -1885,6 +2191,86 @@ def process_add(add_def):
|
||||
add_count += 1
|
||||
existing_names[parsed["name"]] = "Attribute"
|
||||
|
||||
elif child_type == "tables":
|
||||
# Таблица внешнего источника — ОТДЕЛЬНЫЙ файл рядом с файлом источника плюс имя
|
||||
# в его ChildObjects. Единственная операция навыка, создающая файл: без неё
|
||||
# добавить таблицу в существующий источник было нечем (пересборка источника
|
||||
# целиком меняет его uuid и оставляет файлы выброшенных таблиц сиротами).
|
||||
src_dir = os.path.join(os.path.dirname(resolved_path), obj_name)
|
||||
tables_dir = os.path.join(src_dir, "Tables")
|
||||
for tbl_name, entry in get_eds_tables(items).items():
|
||||
# Характеристики и слоты форм точечным добавлением не задаются: первые требуют
|
||||
# эмиттера характеристик (живёт в meta-compile), вторые назначает form-add при
|
||||
# добавлении формы. Молча проглотить ключ нельзя — модель решит, что он сработал.
|
||||
tv = entry['props'] or {}
|
||||
bad_key = next((k for k in ('characteristics', 'defaultObjectForm', 'defaultRecordForm',
|
||||
'defaultListForm', 'defaultChoiceForm') if tv.get(k)), None)
|
||||
if bad_key:
|
||||
warn(f"Ключ '{bad_key}' не поддержан при добавлении таблицы: форму назначает навык form-add, характеристики — навык meta-compile. Таблица '{tbl_name}' пропущена.")
|
||||
continue
|
||||
if tbl_name in existing_names:
|
||||
warn(f"Table '{tbl_name}' already exists, skipping")
|
||||
continue
|
||||
table_path = os.path.join(tables_dir, f"{tbl_name}.xml")
|
||||
if os.path.exists(table_path):
|
||||
warn(f"\u0424\u0430\u0439\u043b \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442: {table_path} \u2014 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u044e")
|
||||
continue
|
||||
field_parts = []
|
||||
for f in entry["fields"]:
|
||||
field_parts.append(build_attribute_fragment(parse_attribute_shorthand(f), "eds-field", "\t\t\t", "Field"))
|
||||
fields_xml = "\r\n".join(field_parts)
|
||||
table_xml = build_eds_table_xml(obj_name, tbl_name, entry, fields_xml, '', '')
|
||||
os.makedirs(tables_dir, exist_ok=True)
|
||||
with open(table_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||
fh.write(table_xml.rstrip("\r\n"))
|
||||
nodes = import_fragment(f"{indent}<Table>{esc_xml_text(tbl_name)}</Table>")
|
||||
ref_node = find_insertion_point("Table", {"name": tbl_name})
|
||||
for node in nodes:
|
||||
insert_before_element(child_objects_el, node, ref_node, indent)
|
||||
info(f"Added table: {tbl_name} ({table_path})")
|
||||
add_count += 1
|
||||
existing_names[tbl_name] = "Table"
|
||||
|
||||
elif child_type == "functions":
|
||||
# Функция живёт узлом внутри файла источника — отдельного файла у неё нет.
|
||||
for fn_name, fn_val in items.items():
|
||||
if fn_name in existing_names:
|
||||
warn(f"Function '{fn_name}' already exists, skipping")
|
||||
continue
|
||||
if isinstance(fn_val, str):
|
||||
fn_returns, fn_no_value = "String", False
|
||||
else:
|
||||
fn_returns = str(fn_val.get("returns") or fn_val.get("returnType") or "String")
|
||||
fn_no_value = fn_val.get("returnValue") is not None and fn_val.get("returnValue") is not True
|
||||
fn_type_xml = "" if fn_no_value else build_value_type_xml(f"{indent}\t\t", fn_returns)
|
||||
before = len(lines)
|
||||
emit_eds_function(indent, fn_name, fn_val, fn_type_xml)
|
||||
fragment_xml = "\r\n".join(lines[before:])
|
||||
del lines[before:]
|
||||
nodes = import_fragment(fragment_xml)
|
||||
ref_node = find_insertion_point("Function", {"name": fn_name})
|
||||
for node in nodes:
|
||||
insert_before_element(child_objects_el, node, ref_node, indent)
|
||||
info(f"Added function: {fn_name}")
|
||||
add_count += 1
|
||||
existing_names[fn_name] = "Function"
|
||||
|
||||
elif child_type == "fields":
|
||||
# Поле таблицы внешнего источника: тот же парсер реквизита, свой тег и контекст.
|
||||
for item in items:
|
||||
parsed = parse_attribute_shorthand(item)
|
||||
if parsed["name"] in existing_names:
|
||||
warn(f"Field '{parsed['name']}' already exists, skipping")
|
||||
continue
|
||||
fragment_xml = build_attribute_fragment(parsed, "eds-field", indent, "Field")
|
||||
nodes = import_fragment(fragment_xml)
|
||||
ref_node = find_insertion_point("Field", parsed)
|
||||
for node in nodes:
|
||||
insert_before_element(child_objects_el, node, ref_node, indent)
|
||||
info(f"Added field: {parsed['name']}")
|
||||
add_count += 1
|
||||
existing_names[parsed["name"]] = "Field"
|
||||
|
||||
elif child_type == "tabularSections":
|
||||
for item in items:
|
||||
if isinstance(item, str):
|
||||
@@ -1968,26 +2354,42 @@ def process_add(add_def):
|
||||
add_count += 1
|
||||
existing_names[col_name] = "Column"
|
||||
|
||||
elif child_type in ("forms", "templates", "commands"):
|
||||
tag_map = {"forms": "Form", "templates": "Template", "commands": "Command"}
|
||||
tag = tag_map[child_type]
|
||||
elif child_type in ("forms", "templates"):
|
||||
# Форма и макет регистрируются голым текстом (<Form>Имя</Form>) и требуют ещё и
|
||||
# собственных файлов. И то и другое делают form-add / template-add -- дублировать
|
||||
# эту ответственность здесь нельзя: получится висячая регистрация без файла.
|
||||
skill_name = "form-add" if child_type == "forms" else "template-add"
|
||||
what_name = "Форму" if child_type == "forms" else "Макет"
|
||||
warn(
|
||||
f"{what_name} добавляет навык {skill_name} "
|
||||
"(он создаёт и файл, и запись в ChildObjects). "
|
||||
"meta-edit этого не делает — операция пропущена."
|
||||
)
|
||||
|
||||
elif child_type == "commands":
|
||||
for item in items:
|
||||
if isinstance(item, str):
|
||||
item_name = item
|
||||
else:
|
||||
item_name = str(item.get("name", ""))
|
||||
if item_name in existing_names:
|
||||
warn(f"{tag} '{item_name}' already exists, skipping")
|
||||
warn(f"Command '{item_name}' already exists, skipping")
|
||||
continue
|
||||
fragment_xml = build_simple_child_fragment(tag, item_name, indent)
|
||||
# У команды есть модуль обработчика (Commands/<Имя>/Ext/CommandModule.bsl) — в корпусе
|
||||
# он есть у всех команд без исключения. Пишем ту же заготовку, что и meta-compile.
|
||||
cmd_ext_dir = os.path.join(os.path.dirname(resolved_path), obj_name, "Commands", item_name, "Ext")
|
||||
cmd_mod_path = os.path.join(cmd_ext_dir, "CommandModule.bsl")
|
||||
os.makedirs(cmd_ext_dir, exist_ok=True)
|
||||
with open(cmd_mod_path, "w", encoding="utf-8-sig", newline="") as fh:
|
||||
fh.write("&НаКлиенте\r\nПроцедура ОбработкаКоманды(ПараметрКоманды, ПараметрыВыполненияКоманды)\r\n\r\n\t// Вставьте обработчик команды.\r\n\r\nКонецПроцедуры\r\n")
|
||||
fragment_xml = build_command_fragment(item_name, indent)
|
||||
nodes = import_fragment(fragment_xml)
|
||||
ref_node = find_insertion_point(tag, {"after": "", "before": ""})
|
||||
ref_node = find_insertion_point("Command", {"after": "", "before": ""})
|
||||
for node in nodes:
|
||||
insert_before_element(child_objects_el, node, ref_node, indent)
|
||||
info(f"Added {tag.lower()}: {item_name}")
|
||||
info(f"Added command: {item_name} ({cmd_mod_path})")
|
||||
add_count += 1
|
||||
existing_names[item_name] = tag
|
||||
|
||||
existing_names[item_name] = "Command"
|
||||
|
||||
# ============================================================
|
||||
# REMOVE operations
|
||||
@@ -2006,6 +2408,16 @@ def process_remove(remove_def):
|
||||
if child_type == "properties":
|
||||
warn("Cannot remove properties -- use modify instead")
|
||||
continue
|
||||
if child_type in ("forms", "templates"):
|
||||
# Снять регистрацию мало — надо удалить и файлы; это делают form-remove / template-remove.
|
||||
skill_name = "form-remove" if child_type == "forms" else "template-remove"
|
||||
what_name = "Форму" if child_type == "forms" else "Макет"
|
||||
warn(
|
||||
f"{what_name} удаляет навык {skill_name} "
|
||||
"(он убирает и файлы, и запись в ChildObjects). "
|
||||
"meta-edit этого не делает — операция пропущена."
|
||||
)
|
||||
continue
|
||||
|
||||
xml_tag = child_type_to_xml_tag.get(child_type)
|
||||
if not xml_tag or child_objects_el is None:
|
||||
@@ -3354,11 +3766,12 @@ def main():
|
||||
xml_root = xml_tree.getroot()
|
||||
|
||||
# Префикс current-config берём из объявлений корня — им и пишем ссылочные типы.
|
||||
global cfg_prefix, is_format_221
|
||||
global cfg_prefix, is_format_221, format_version
|
||||
cfg_prefix = next((p for p, u in (xml_root.nsmap or {}).items() if u == CFG_NS and p), None)
|
||||
_fv = xml_root.get("version") or "2.17"
|
||||
_m = re.match(r'^(\d+)\.(\d+)$', _fv)
|
||||
is_format_221 = bool(_m) and int(_m.group(1)) * 100 + int(_m.group(2)) >= 221
|
||||
format_version = _fv
|
||||
|
||||
# --- Detect object type ---
|
||||
if localname(xml_root) != "MetaDataObject":
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-info v1.11 — Compact summary of 1C metadata object
|
||||
# meta-info v1.14 — Compact summary of 1C metadata object
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -104,6 +104,7 @@ $typeNameMap = @{
|
||||
"DefinedType"="Определяемый тип"; "CommonModule"="Общий модуль"
|
||||
"ScheduledJob"="Регламентное задание"; "EventSubscription"="Подписка на событие"
|
||||
"HTTPService"="HTTP-сервис"; "WebService"="Веб-сервис"
|
||||
"ExternalDataSource"="Внешний источник данных"; "Table"="Таблица внешнего источника"
|
||||
}
|
||||
|
||||
$refTypeMap = @{
|
||||
@@ -112,6 +113,7 @@ $refTypeMap = @{
|
||||
"ChartOfCharacteristicTypesRef"="ПВХСсылка"; "ChartOfCalculationTypesRef"="ПВРСсылка"
|
||||
"ExchangePlanRef"="ПланОбменаСсылка"; "BusinessProcessRef"="БизнесПроцессСсылка"
|
||||
"TaskRef"="ЗадачаСсылка"
|
||||
"ExternalDataSourceTableRef"="ВнешнийИсточникДанныхТаблицаСсылка"
|
||||
}
|
||||
|
||||
$regTypeMap = @{
|
||||
@@ -231,6 +233,10 @@ function Format-SingleType([string]$raw, $parentNode) {
|
||||
}
|
||||
"v8:ValueStorage" { return "ХранилищеЗначения" }
|
||||
"v8:UUID" { return "УникальныйИдентификатор" }
|
||||
# xs:base64Binary — всегда ДвоичныеДанные, и без квалификаторов тоже: замерено
|
||||
# на 8.3.24.1691 — такой узел платформа загружает и выгружает обратно как безлимитные
|
||||
# двоичные данные (Length 0, AllowedLength Variable), а не как ХранилищеЗначения.
|
||||
"xs:base64Binary" { return "ДвоичныеДанные" }
|
||||
"v8:Null" { return "Null" }
|
||||
default {
|
||||
# Normalize d5p1:/dNpN: -> cfg:
|
||||
@@ -1347,6 +1353,80 @@ if (-not $drillDone) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Внешний источник данных: таблицы и функции ---
|
||||
if ($mdType -eq "ExternalDataSource") {
|
||||
$dlcm = $props.SelectSingleNode("md:DataLockControlMode", $ns)
|
||||
if ($dlcm) { Out "Блокировка данных: $($dlcm.InnerText)" }
|
||||
if ($childObjs) {
|
||||
$tables = @(Get-SimpleChildren $childObjs "Table")
|
||||
if ($tables.Count -gt 0) {
|
||||
Out ""
|
||||
Out "Таблицы ($($tables.Count)): $($tables -join ', ')"
|
||||
}
|
||||
$fns = @($childObjs.SelectNodes("md:Function", $ns))
|
||||
if ($fns.Count -gt 0) {
|
||||
Out ""
|
||||
Out "Функции ($($fns.Count)):"
|
||||
foreach ($fn in $fns) {
|
||||
$fp = $fn.SelectSingleNode("md:Properties", $ns)
|
||||
$fnName = $fp.SelectSingleNode("md:Name", $ns).InnerText
|
||||
$fnExpr = $fp.SelectSingleNode("md:ExpressionInDataSource", $ns)
|
||||
$fnType = Format-Type $fp.SelectSingleNode("md:Type", $ns)
|
||||
$retNode = $fp.SelectSingleNode("md:ReturnValue", $ns)
|
||||
$ret = if ($retNode -and $retNode.InnerText -eq "false") { "процедура" } else { $fnType }
|
||||
Out " $fnName → $(if ($fnExpr) { $fnExpr.InnerText })$(if ($ret) { " : $ret" })"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Таблица внешнего источника: свойства и поля ---
|
||||
if ($mdType -eq "Table") {
|
||||
$g = { param($tag) $n = $props.SelectSingleNode("md:$tag", $ns); if ($n) { $n.InnerText } else { "" } }
|
||||
$short = { param($ref) if ($ref) { ($ref -split '\.')[-1] } else { "" } }
|
||||
|
||||
$head = @()
|
||||
$tableType = & $g "TableType"
|
||||
$head += "Вид: $(if ($tableType -eq 'Expression') { 'выражение' } else { 'таблица' })"
|
||||
$src = if ($tableType -eq "Expression") { & $g "ExpressionInDataSource" } else { & $g "NameInDataSource" }
|
||||
if ($src) { $head += "в источнике: $src" }
|
||||
$head += "данные: $(if ((& $g 'TableDataType') -eq 'ObjectData') { 'объектные' } else { 'необъектные' })"
|
||||
if ((& $g "ReadOnly") -eq "true") { $head += "только чтение" }
|
||||
Out ($head -join " | ")
|
||||
|
||||
$keys = @($props.SelectNodes("md:KeyFields/xr:Field", $ns) | ForEach-Object { & $short $_.InnerText })
|
||||
$refs = @()
|
||||
if ($keys.Count -gt 0) { $refs += "ключ: $($keys -join ', ')" }
|
||||
foreach ($pair in @(@("PresentationField","представление"), @("ParentField","родитель"), @("DataVersionField","версия данных"))) {
|
||||
$v = & $short (& $g $pair[0])
|
||||
if ($v) { $refs += "$($pair[1]): $v" }
|
||||
}
|
||||
$ibs = @($props.SelectNodes("md:InputByString/xr:Field", $ns) | ForEach-Object { & $short $_.InnerText })
|
||||
if ($ibs.Count -gt 0) { $refs += "ввод по строке: $($ibs -join ', ')" }
|
||||
if ($refs.Count -gt 0) { Out ($refs -join " | ") }
|
||||
|
||||
if ($childObjs) {
|
||||
$fields = @(Get-Attributes $childObjs "Field")
|
||||
if ($fields.Count -gt 0) {
|
||||
Out ""
|
||||
Out "Поля ($($fields.Count)):"
|
||||
$ml = Get-MaxNameLen $fields
|
||||
foreach ($f in $fields) {
|
||||
$fp = $f.Props
|
||||
$marks = @()
|
||||
$nids = $fp.SelectSingleNode("md:NameInDataSource", $ns)
|
||||
if ($nids -and $nids.InnerText -and $nids.InnerText -ne $f.Name) { $marks += "→ $($nids.InnerText)" }
|
||||
$ro = $fp.SelectSingleNode("md:ReadOnly", $ns)
|
||||
if ($ro -and $ro.InnerText -eq "true") { $marks += "только чтение" }
|
||||
$an = $fp.SelectSingleNode("md:AllowNull", $ns)
|
||||
if ($an -and $an.InnerText -eq "true") { $marks += "NULL" }
|
||||
$tail = if ($marks.Count -gt 0) { " [$($marks -join ', ')]" } else { "" }
|
||||
Out (" $($f.Name.PadRight($ml)) $($f.Type)$tail")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Attributes ---
|
||||
if ($childObjs -and $mdType -ne "Enum") {
|
||||
$attrs = @(Get-Attributes $childObjs)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-info v1.11 — Compact summary of 1C metadata object (Python port)
|
||||
# meta-info v1.14 — Compact summary of 1C metadata object (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -177,6 +177,7 @@ type_name_map = {
|
||||
"DefinedType": "Определяемый тип", "CommonModule": "Общий модуль",
|
||||
"ScheduledJob": "Регламентное задание", "EventSubscription": "Подписка на событие",
|
||||
"HTTPService": "HTTP-сервис", "WebService": "Веб-сервис",
|
||||
"ExternalDataSource": "Внешний источник данных", "Table": "Таблица внешнего источника",
|
||||
}
|
||||
|
||||
ref_type_map = {
|
||||
@@ -185,6 +186,7 @@ ref_type_map = {
|
||||
"ChartOfCharacteristicTypesRef": "ПВХСсылка", "ChartOfCalculationTypesRef": "ПВРСсылка",
|
||||
"ExchangePlanRef": "ПланОбменаСсылка", "BusinessProcessRef": "БизнесПроцессСсылка",
|
||||
"TaskRef": "ЗадачаСсылка",
|
||||
"ExternalDataSourceTableRef": "ВнешнийИсточникДанныхТаблицаСсылка",
|
||||
}
|
||||
|
||||
reg_type_map = {
|
||||
@@ -317,6 +319,11 @@ def format_single_type(raw, parent_node):
|
||||
return "ХранилищеЗначения"
|
||||
if raw == "v8:UUID":
|
||||
return "УникальныйИдентификатор"
|
||||
if raw == "xs:base64Binary":
|
||||
# xs:base64Binary — всегда ДвоичныеДанные, и без квалификаторов тоже: замерено
|
||||
# на 8.3.24.1691 — такой узел платформа загружает и выгружает обратно как безлимитные
|
||||
# двоичные данные (Length 0, AllowedLength Variable), а не как ХранилищеЗначения.
|
||||
return "ДвоичныеДанные"
|
||||
if raw == "v8:Null":
|
||||
return "Null"
|
||||
# Normalize d5p1:/dNpN: → cfg: (both map to same namespace)
|
||||
@@ -1363,6 +1370,85 @@ if not drill_done:
|
||||
for r in res:
|
||||
out(format_attr_line(r, ml))
|
||||
|
||||
# Внешний источник данных: таблицы и функции
|
||||
if md_type == "ExternalDataSource":
|
||||
dlcm = props.find("md:DataLockControlMode", NS)
|
||||
if dlcm is not None:
|
||||
out(f"Блокировка данных: {dlcm.text or ''}")
|
||||
if child_objs is not None:
|
||||
tables = get_simple_children(child_objs, "Table")
|
||||
if tables:
|
||||
out("")
|
||||
out(f"Таблицы ({len(tables)}): {', '.join(tables)}")
|
||||
fns = child_objs.findall("md:Function", NS)
|
||||
if fns:
|
||||
out("")
|
||||
out(f"Функции ({len(fns)}):")
|
||||
for fn in fns:
|
||||
fp = fn.find("md:Properties", NS)
|
||||
fn_name = fp.find("md:Name", NS).text or ""
|
||||
fn_expr = fp.find("md:ExpressionInDataSource", NS)
|
||||
fn_type = format_type(fp.find("md:Type", NS))
|
||||
ret_node = fp.find("md:ReturnValue", NS)
|
||||
ret = "процедура" if (ret_node is not None and ret_node.text == "false") else fn_type
|
||||
expr = (fn_expr.text or "") if fn_expr is not None else ""
|
||||
out(f" {fn_name} → {expr}" + (f" : {ret}" if ret else ""))
|
||||
|
||||
# Таблица внешнего источника: свойства и поля
|
||||
if md_type == "Table":
|
||||
def _g(tag):
|
||||
n = props.find(f"md:{tag}", NS)
|
||||
return (n.text or "") if n is not None else ""
|
||||
|
||||
def _short(ref):
|
||||
return ref.split(".")[-1] if ref else ""
|
||||
|
||||
table_type = _g("TableType")
|
||||
head = ["Вид: " + ("выражение" if table_type == "Expression" else "таблица")]
|
||||
src = _g("ExpressionInDataSource") if table_type == "Expression" else _g("NameInDataSource")
|
||||
if src:
|
||||
head.append(f"в источнике: {src}")
|
||||
head.append("данные: " + ("объектные" if _g("TableDataType") == "ObjectData" else "необъектные"))
|
||||
if _g("ReadOnly") == "true":
|
||||
head.append("только чтение")
|
||||
out(" | ".join(head))
|
||||
|
||||
keys = [_short(f.text) for f in props.findall("md:KeyFields/xr:Field", NS)]
|
||||
refs = []
|
||||
if keys:
|
||||
refs.append(f"ключ: {', '.join(keys)}")
|
||||
for tag, label in (("PresentationField", "представление"), ("ParentField", "родитель"),
|
||||
("DataVersionField", "версия данных")):
|
||||
v = _short(_g(tag))
|
||||
if v:
|
||||
refs.append(f"{label}: {v}")
|
||||
ibs = [_short(f.text) for f in props.findall("md:InputByString/xr:Field", NS)]
|
||||
if ibs:
|
||||
refs.append(f"ввод по строке: {', '.join(ibs)}")
|
||||
if refs:
|
||||
out(" | ".join(refs))
|
||||
|
||||
if child_objs is not None:
|
||||
fields = get_attributes(child_objs, "Field")
|
||||
if fields:
|
||||
out("")
|
||||
out(f"Поля ({len(fields)}):")
|
||||
ml = get_max_name_len(fields)
|
||||
for f in fields:
|
||||
fp = f["Props"]
|
||||
marks = []
|
||||
nids = fp.find("md:NameInDataSource", NS)
|
||||
if nids is not None and nids.text and nids.text != f["Name"]:
|
||||
marks.append(f"→ {nids.text}")
|
||||
ro = fp.find("md:ReadOnly", NS)
|
||||
if ro is not None and ro.text == "true":
|
||||
marks.append("только чтение")
|
||||
an = fp.find("md:AllowNull", NS)
|
||||
if an is not None and an.text == "true":
|
||||
marks.append("NULL")
|
||||
tail = f" [{', '.join(marks)}]" if marks else ""
|
||||
out(f" {f['Name'].ljust(ml)} {f['Type']}{tail}")
|
||||
|
||||
# Attributes
|
||||
if child_objs is not None and md_type != "Enum":
|
||||
attrs = get_attributes(child_objs)
|
||||
|
||||
@@ -24,7 +24,7 @@ allowed-tools:
|
||||
| Параметр | Обязательный | Описание |
|
||||
|------------|:------------:|-------------------------------------------------|
|
||||
| ConfigDir | да | Корневая директория выгрузки (где Configuration.xml) |
|
||||
| Object | да | Тип и имя объекта: `Catalog.Товары`, `Document.Заказ` и т.д. |
|
||||
| Object | да | Тип и имя объекта: `Catalog.Товары`, `Document.Заказ` и т.д. Таблица внешнего источника — четырьмя частями: `ExternalDataSource.PG.Table.products` |
|
||||
| DryRun | нет | Только показать что будет удалено, без изменений |
|
||||
| KeepFiles | нет | Не удалять файлы, только дерегистрировать |
|
||||
| Force | нет | Удалить несмотря на найденные ссылки; ссылки на формы объекта при этом очищаются |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-remove v1.11 — Remove metadata object from 1C configuration dump
|
||||
# meta-remove v1.13 — Remove metadata object from 1C configuration dump
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -60,6 +60,7 @@ $typePluralMap = @{
|
||||
"WSReference" = "WSReferences"
|
||||
"StyleItem" = "StyleItems"
|
||||
"Language" = "Languages"
|
||||
"ExternalDataSource" = "ExternalDataSources"
|
||||
}
|
||||
|
||||
# --- Resolve paths ---
|
||||
@@ -216,21 +217,47 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
||||
|
||||
# --- Parse object spec ---
|
||||
|
||||
$parts = $Object -split "\.", 2
|
||||
if ($parts.Count -ne 2 -or -not $parts[0] -or -not $parts[1]) {
|
||||
Write-Host "[ERROR] Invalid object format '$Object'. Expected: Type.Name (e.g. Catalog.Товары)"
|
||||
exit 1
|
||||
# Таблица внешнего источника — единственный объект с четырёхчастным именем: она лежит не в
|
||||
# каталоге вида, а внутри источника, и числится в ChildObjects файла источника, не конфигурации.
|
||||
$edsSource = ""
|
||||
if ($Object -match '^ExternalDataSource\.([^.]+)\.Table\.(.+)$') {
|
||||
$edsSource = $Matches[1]
|
||||
$objType = "Table"
|
||||
$objName = $Matches[2]
|
||||
} else {
|
||||
$parts = $Object -split "\.", 2
|
||||
if ($parts.Count -ne 2 -or -not $parts[0] -or -not $parts[1]) {
|
||||
Write-Host "[ERROR] Invalid object format '$Object'. Expected: Type.Name (e.g. Catalog.Товары) or ExternalDataSource.Источник.Table.Таблица"
|
||||
exit 1
|
||||
}
|
||||
$objType = $parts[0]
|
||||
$objName = $parts[1]
|
||||
}
|
||||
|
||||
$objType = $parts[0]
|
||||
$objName = $parts[1]
|
||||
|
||||
if (-not $typePluralMap.ContainsKey($objType)) {
|
||||
Write-Host "[ERROR] Unknown type '$objType'. Supported: $($typePluralMap.Keys -join ', ')"
|
||||
exit 1
|
||||
if ($edsSource) {
|
||||
$typePlural = Join-Path (Join-Path "ExternalDataSources" $edsSource) "Tables"
|
||||
} else {
|
||||
if (-not $typePluralMap.ContainsKey($objType)) {
|
||||
Write-Host "[ERROR] Unknown type '$objType'. Supported: $($typePluralMap.Keys -join ', ')"
|
||||
exit 1
|
||||
}
|
||||
$typePlural = $typePluralMap[$objType]
|
||||
}
|
||||
|
||||
$typePlural = $typePluralMap[$objType]
|
||||
# Реестр, где объект числится: обычно ChildObjects конфигурации, а для таблицы — файл источника.
|
||||
if ($edsSource) {
|
||||
$registryXml = Join-Path (Join-Path $ConfigDir "ExternalDataSources") "$edsSource.xml"
|
||||
$registryRoot = "ExternalDataSource"
|
||||
$registryLabel = "ExternalDataSources/$edsSource.xml"
|
||||
if (-not (Test-Path $registryXml)) {
|
||||
Write-Host "[ERROR] Внешний источник '$edsSource' не найден: $registryLabel"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
$registryXml = $configXml
|
||||
$registryRoot = "Configuration"
|
||||
$registryLabel = "Configuration.xml"
|
||||
}
|
||||
|
||||
Write-Host "=== meta-remove: ${objType}.${objName} ==="
|
||||
Write-Host ""
|
||||
@@ -304,10 +331,10 @@ if (-not $hasXml -and -not $hasDir) {
|
||||
# Check if registered in Configuration.xml before proceeding
|
||||
$cfgCheckDoc = New-Object System.Xml.XmlDocument
|
||||
$cfgCheckDoc.PreserveWhitespace = $true
|
||||
$cfgCheckDoc.Load($configXml)
|
||||
$cfgCheckDoc.Load($registryXml)
|
||||
$cfgCheckNs = New-Object System.Xml.XmlNamespaceManager($cfgCheckDoc.NameTable)
|
||||
$cfgCheckNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$cfgCheckNode = $cfgCheckDoc.DocumentElement.SelectSingleNode("md:Configuration/md:ChildObjects", $cfgCheckNs)
|
||||
$cfgCheckNode = $cfgCheckDoc.DocumentElement.SelectSingleNode("md:$registryRoot/md:ChildObjects", $cfgCheckNs)
|
||||
$registeredInCfg = $false
|
||||
if ($cfgCheckNode) {
|
||||
foreach ($child in @($cfgCheckNode.ChildNodes)) {
|
||||
@@ -318,7 +345,7 @@ if (-not $hasXml -and -not $hasDir) {
|
||||
}
|
||||
}
|
||||
if (-not $registeredInCfg) {
|
||||
Write-Host "[ERROR] Object not found: $typePlural/$objName.xml and not registered in Configuration.xml"
|
||||
Write-Host "[ERROR] Object not found: $typePlural/$objName.xml and not registered in $registryLabel"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[WARN] Object files not found: $typePlural/$objName.xml"
|
||||
@@ -390,6 +417,18 @@ if ($ruMgr) {
|
||||
# English manager = plural directory name
|
||||
$searchPatterns += "$typePlural.$objName"
|
||||
|
||||
# 2а) Внешний источник данных: ссылки на сам источник и на его таблицы
|
||||
if ($objType -eq "ExternalDataSource") {
|
||||
$searchPatterns += "ExternalDataSource.$objName."
|
||||
$searchPatterns += "ВнешниеИсточникиДанных.$objName"
|
||||
$searchPatterns += "ExternalDataSources.$objName"
|
||||
}
|
||||
if ($edsSource) {
|
||||
$searchPatterns += "ExternalDataSource.$edsSource.Table.$objName"
|
||||
$searchPatterns += "ExternalDataSourceTableRef.$edsSource.$objName"
|
||||
$searchPatterns += "ВнешниеИсточникиДанных.$edsSource.Таблицы.$objName"
|
||||
}
|
||||
|
||||
# 3) CommonModule: method calls in BSL (ModuleName.)
|
||||
if ($objType -eq "CommonModule") {
|
||||
$searchPatterns += "$objName."
|
||||
@@ -504,22 +543,22 @@ if ($references.Count -gt 0) {
|
||||
Write-Host "[OK] No references found"
|
||||
}
|
||||
|
||||
# --- 3. Remove from Configuration.xml ChildObjects ---
|
||||
# --- 3. Remove from registry ChildObjects (Configuration.xml или файл внешнего источника) ---
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "--- Configuration.xml ---"
|
||||
Write-Host "--- $registryLabel ---"
|
||||
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $true
|
||||
$xmlDoc.Load($configXml)
|
||||
$xmlDoc.Load($registryXml)
|
||||
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
|
||||
$cfgNode = $xmlDoc.DocumentElement.SelectSingleNode("md:Configuration", $ns)
|
||||
$cfgNode = $xmlDoc.DocumentElement.SelectSingleNode("md:$registryRoot", $ns)
|
||||
if (-not $cfgNode) {
|
||||
Write-Host "[ERROR] Configuration element not found in Configuration.xml"
|
||||
Write-Host "[ERROR] $registryRoot element not found in $registryLabel"
|
||||
$errors++
|
||||
} else {
|
||||
$childObjects = $cfgNode.SelectSingleNode("md:ChildObjects", $ns)
|
||||
@@ -570,10 +609,10 @@ if (-not $cfgNode) {
|
||||
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $configXml) -and ([System.IO.File]::ReadAllText($configXml) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$targetEol = if ((Test-Path -LiteralPath $registryXml) -and ([System.IO.File]::ReadAllText($registryXml) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($configXml, $xmlText, $enc)
|
||||
Write-Host "[OK] Configuration.xml saved"
|
||||
[System.IO.File]::WriteAllText($registryXml, $xmlText, $enc)
|
||||
Write-Host "[OK] $registryLabel saved"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# meta-remove v1.11 — Remove metadata object from 1C configuration dump
|
||||
# meta-remove v1.13 — Remove metadata object from 1C configuration dump
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
|
||||
import argparse
|
||||
@@ -250,6 +250,7 @@ TYPE_PLURAL_MAP = {
|
||||
"WSReference": "WSReferences",
|
||||
"StyleItem": "StyleItems",
|
||||
"Language": "Languages",
|
||||
"ExternalDataSource": "ExternalDataSources",
|
||||
}
|
||||
|
||||
# Type -> reference type names (used in XML <v8:Type> elements)
|
||||
@@ -388,19 +389,42 @@ def main():
|
||||
sys.exit(1)
|
||||
|
||||
# --- Parse object spec ---
|
||||
parts = args.Object.split(".", 1)
|
||||
if len(parts) != 2 or not parts[0] or not parts[1]:
|
||||
print(f"[ERROR] Invalid object format '{args.Object}'. Expected: Type.Name (e.g. Catalog.\u0422\u043e\u0432\u0430\u0440\u044b)")
|
||||
sys.exit(1)
|
||||
# Таблица внешнего источника — единственный объект с четырёхчастным именем: она лежит не в
|
||||
# каталоге вида, а внутри источника, и числится в ChildObjects файла источника, не конфигурации.
|
||||
eds_source = ""
|
||||
m_eds = re.match(r'^ExternalDataSource\.([^.]+)\.Table\.(.+)$', args.Object)
|
||||
if m_eds:
|
||||
eds_source = m_eds.group(1)
|
||||
obj_type = "Table"
|
||||
obj_name = m_eds.group(2)
|
||||
else:
|
||||
parts = args.Object.split(".", 1)
|
||||
if len(parts) != 2 or not parts[0] or not parts[1]:
|
||||
print(f"[ERROR] Invalid object format '{args.Object}'. Expected: Type.Name (e.g. Catalog.\u0422\u043e\u0432\u0430\u0440\u044b) or ExternalDataSource.\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a.Table.\u0422\u0430\u0431\u043b\u0438\u0446\u0430")
|
||||
sys.exit(1)
|
||||
obj_type = parts[0]
|
||||
obj_name = parts[1]
|
||||
|
||||
obj_type = parts[0]
|
||||
obj_name = parts[1]
|
||||
if eds_source:
|
||||
type_plural = os.path.join("ExternalDataSources", eds_source, "Tables")
|
||||
else:
|
||||
if obj_type not in TYPE_PLURAL_MAP:
|
||||
print(f"[ERROR] Unknown type '{obj_type}'. Supported: {', '.join(TYPE_PLURAL_MAP.keys())}")
|
||||
sys.exit(1)
|
||||
type_plural = TYPE_PLURAL_MAP[obj_type]
|
||||
|
||||
if obj_type not in TYPE_PLURAL_MAP:
|
||||
print(f"[ERROR] Unknown type '{obj_type}'. Supported: {', '.join(TYPE_PLURAL_MAP.keys())}")
|
||||
sys.exit(1)
|
||||
|
||||
type_plural = TYPE_PLURAL_MAP[obj_type]
|
||||
# Реестр, где объект числится: обычно ChildObjects конфигурации, а для таблицы — файл источника.
|
||||
if eds_source:
|
||||
registry_xml = os.path.join(config_dir, "ExternalDataSources", f"{eds_source}.xml")
|
||||
registry_root = "ExternalDataSource"
|
||||
registry_label = f"ExternalDataSources/{eds_source}.xml"
|
||||
if not os.path.isfile(registry_xml):
|
||||
print(f"[ERROR] \u0412\u043d\u0435\u0448\u043d\u0438\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a '{eds_source}' \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d: {registry_label}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
registry_xml = config_xml
|
||||
registry_root = "Configuration"
|
||||
registry_label = "Configuration.xml"
|
||||
|
||||
print(f"=== meta-remove: {obj_type}.{obj_name} ===")
|
||||
print()
|
||||
@@ -425,9 +449,9 @@ def main():
|
||||
|
||||
if not has_xml and not has_dir:
|
||||
# Check if registered in Configuration.xml before proceeding
|
||||
cfg_check_tree = etree.parse(config_xml, etree.XMLParser(remove_blank_text=False))
|
||||
cfg_check_tree = etree.parse(registry_xml, etree.XMLParser(remove_blank_text=False))
|
||||
cfg_check_root = cfg_check_tree.getroot()
|
||||
child_objects = cfg_check_root.find(f"{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects")
|
||||
child_objects = cfg_check_root.find(f"{{{MD_NS}}}{registry_root}/{{{MD_NS}}}ChildObjects")
|
||||
registered_in_cfg = False
|
||||
if child_objects is not None:
|
||||
for child in child_objects:
|
||||
@@ -435,7 +459,7 @@ def main():
|
||||
registered_in_cfg = True
|
||||
break
|
||||
if not registered_in_cfg:
|
||||
print(f"[ERROR] Object not found: {type_plural}/{obj_name}.xml and not registered in Configuration.xml")
|
||||
print(f"[ERROR] Object not found: {type_plural}/{obj_name}.xml and not registered in {registry_label}")
|
||||
sys.exit(1)
|
||||
print(f"[WARN] Object files not found: {type_plural}/{obj_name}.xml")
|
||||
print(" Proceeding with deregistration only...")
|
||||
@@ -463,6 +487,16 @@ def main():
|
||||
search_patterns.append(f"{ru_mgr}.{obj_name}")
|
||||
search_patterns.append(f"{type_plural}.{obj_name}")
|
||||
|
||||
# 2а) Внешний источник данных: ссылки на сам источник и на его таблицы
|
||||
if obj_type == "ExternalDataSource":
|
||||
search_patterns.append(f"ExternalDataSource.{obj_name}.")
|
||||
search_patterns.append(f"\u0412\u043d\u0435\u0448\u043d\u0438\u0435\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438\u0414\u0430\u043d\u043d\u044b\u0445.{obj_name}")
|
||||
search_patterns.append(f"ExternalDataSources.{obj_name}")
|
||||
if eds_source:
|
||||
search_patterns.append(f"ExternalDataSource.{eds_source}.Table.{obj_name}")
|
||||
search_patterns.append(f"ExternalDataSourceTableRef.{eds_source}.{obj_name}")
|
||||
search_patterns.append(f"\u0412\u043d\u0435\u0448\u043d\u0438\u0435\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438\u0414\u0430\u043d\u043d\u044b\u0445.{eds_source}.\u0422\u0430\u0431\u043b\u0438\u0446\u044b.{obj_name}")
|
||||
|
||||
# 3) CommonModule: method calls
|
||||
if obj_type == "CommonModule":
|
||||
search_patterns.append(f"{obj_name}.")
|
||||
@@ -578,17 +612,17 @@ def main():
|
||||
else:
|
||||
print("[OK] No references found")
|
||||
|
||||
# --- 3. Remove from Configuration.xml ChildObjects ---
|
||||
# --- 3. Remove from registry ChildObjects (Configuration.xml или файл внешнего источника) ---
|
||||
print()
|
||||
print("--- Configuration.xml ---")
|
||||
print(f"--- {registry_label} ---")
|
||||
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
tree = etree.parse(config_xml, xml_parser)
|
||||
tree = etree.parse(registry_xml, xml_parser)
|
||||
xml_root = tree.getroot()
|
||||
|
||||
cfg_node = xml_root.find(f"{{{MD_NS}}}Configuration")
|
||||
cfg_node = xml_root.find(f"{{{MD_NS}}}{registry_root}")
|
||||
if cfg_node is None:
|
||||
print("[ERROR] Configuration element not found in Configuration.xml")
|
||||
print(f"[ERROR] {registry_root} element not found in {registry_label}")
|
||||
errors += 1
|
||||
else:
|
||||
child_objects = cfg_node.find(f"{{{MD_NS}}}ChildObjects")
|
||||
@@ -611,8 +645,8 @@ def main():
|
||||
|
||||
# Save Configuration.xml
|
||||
if actions > 0 and not args.DryRun:
|
||||
save_xml_bom(tree, config_xml)
|
||||
print("[OK] Configuration.xml saved")
|
||||
save_xml_bom(tree, registry_xml)
|
||||
print(f"[OK] {registry_label} saved")
|
||||
|
||||
# --- 4. Remove from subsystem Content ---
|
||||
print()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-validate v1.24 — Validate 1C metadata object structure
|
||||
# meta-validate v1.27 — Validate 1C metadata object structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -179,7 +179,9 @@ $validTypes = @(
|
||||
"BusinessProcess","Task","ExchangePlan","DocumentJournal",
|
||||
"Report","DataProcessor",
|
||||
"CommonModule","ScheduledJob","EventSubscription",
|
||||
"HTTPService","WebService","DefinedType"
|
||||
"HTTPService","WebService","DefinedType",
|
||||
# Внешний источник данных и его таблица (корень файла таблицы — <Table>).
|
||||
"ExternalDataSource","Table"
|
||||
)
|
||||
|
||||
# Валидные типы метаданных без глубоких правил валидации — раньше падали как "Unrecognized"
|
||||
@@ -212,6 +214,10 @@ $generatedTypeCategories = @{
|
||||
"Report" = @("Object","Manager")
|
||||
"DataProcessor" = @("Object","Manager")
|
||||
"DefinedType" = @("DefinedType")
|
||||
"ExternalDataSource" = @("Manager","TablesManager","CubesManager")
|
||||
# Таблица внешнего источника: имя элемента трёхчастное (Префикс.Источник.Таблица),
|
||||
# но проверка «имя оканчивается на .ИмяОбъекта» на нём работает как есть.
|
||||
"Table" = @("Manager","Object","Ref","List","Record","RecordSet","RecordKey","RecordManager")
|
||||
}
|
||||
|
||||
# Types that have NO InternalInfo / GeneratedType
|
||||
@@ -273,6 +279,9 @@ $childObjectRules = @{
|
||||
"DocumentJournal" = @("Column","Form","Template","Command")
|
||||
"HTTPService" = @("URLTemplate")
|
||||
"WebService" = @("Operation")
|
||||
# Внешний источник: таблицы перечислены именами, функции лежат полными узлами.
|
||||
"ExternalDataSource" = @("Table","Function","Cube")
|
||||
"Table" = @("Field","Form","Template","Command")
|
||||
"Constant" = @("Form")
|
||||
"DefinedType" = @()
|
||||
"CommonModule" = @()
|
||||
@@ -295,7 +304,8 @@ $validPropertyValues = @{
|
||||
"RealTimePosting" = @("Allow","Deny")
|
||||
"RegisterRecordsDeletion" = @("AutoDelete","AutoDeleteOnUnpost","AutoDeleteOff")
|
||||
"RegisterRecordsWritingOnPost" = @("WriteModified","WriteSelected","WriteAll")
|
||||
"DataLockControlMode" = @("Automatic","Managed")
|
||||
# AutomaticAndManaged — только у внешнего источника данных и его таблиц.
|
||||
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
||||
"FullTextSearch" = @("Use","DontUse")
|
||||
"DefaultPresentation" = @("AsDescription","AsCode")
|
||||
"HierarchyType" = @("HierarchyFoldersAndItems","HierarchyOfItems")
|
||||
@@ -1761,6 +1771,13 @@ if (-not $isAdopted) {
|
||||
continue
|
||||
}
|
||||
|
||||
# Ссылка на форму таблицы внешнего источника — шестичастная:
|
||||
# ExternalDataSource.<Источник>.Table.<Таблица>.Form.<Форма>. Сводим её к четырём частям
|
||||
# (вид = Table, объект = имя таблицы), дальше проверка общая.
|
||||
if ($parts.Count -eq 6 -and $parts[0] -eq "ExternalDataSource" -and $parts[2] -eq "Table" -and $parts[4] -eq "Form") {
|
||||
$parts = @("Table", $parts[3], "Form", $parts[5])
|
||||
}
|
||||
|
||||
if ($parts.Count -ne 4 -or $parts[2] -ne "Form") {
|
||||
Report-Warn "20. $tag '$ref' — неожиданный вид ссылки на форму (ожидается 'CommonForm.Имя' или '<Вид>.<Объект>.Form.<Форма>')"
|
||||
continue
|
||||
@@ -1819,6 +1836,60 @@ if (-not $isAdopted) {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 21: таблица внешнего источника — ссылки на поля и наличие ключа ---
|
||||
# Свойства таблицы ссылаются на её же поля полным путём. Опечатка в имени поля даёт
|
||||
# «Неизвестный объект метаданных» при загрузке, а найти её глазами в шестичастном пути трудно.
|
||||
if ($mdType -eq "Table") {
|
||||
$fieldNames = @{}
|
||||
if ($childObjNode) {
|
||||
foreach ($f in $childObjNode.SelectNodes("md:Field/md:Properties/md:Name", $ns)) {
|
||||
$fieldNames[$f.InnerText] = $true
|
||||
}
|
||||
}
|
||||
$edsRefsChecked = 0
|
||||
$edsRefsBad = $false
|
||||
foreach ($spec in @(
|
||||
@("KeyFields", "xr:Field"), @("InputByString", "xr:Field"), @("DataLockFields", "xr:Field"),
|
||||
@("PresentationField", $null), @("ParentField", $null), @("DataVersionField", $null))) {
|
||||
$tag = $spec[0]
|
||||
$refs = @()
|
||||
if ($spec[1]) {
|
||||
foreach ($n in $propsNode.SelectNodes("md:$tag/$($spec[1])", $ns)) { $refs += $n.InnerText }
|
||||
} else {
|
||||
$n = $propsNode.SelectSingleNode("md:$tag", $ns)
|
||||
if ($n -and $n.InnerText) { $refs += $n.InnerText }
|
||||
}
|
||||
foreach ($ref in $refs) {
|
||||
$edsRefsChecked++
|
||||
$parts = $ref -split '\.'
|
||||
# Ожидается ExternalDataSource.<Источник>.Table.<Таблица>.Field.<Поле>
|
||||
if ($parts.Count -ne 6 -or $parts[0] -ne "ExternalDataSource" -or $parts[2] -ne "Table" -or $parts[4] -ne "Field") {
|
||||
Report-Error "21. $tag '$ref' — ожидается ExternalDataSource.<Источник>.Table.<Таблица>.Field.<Поле>"
|
||||
$edsRefsBad = $true
|
||||
continue
|
||||
}
|
||||
if ($parts[3] -ne $objName) {
|
||||
Report-Error "21. $tag '$ref' — ссылка на поле ЧУЖОЙ таблицы (эта: $objName)"
|
||||
$edsRefsBad = $true
|
||||
continue
|
||||
}
|
||||
if (-not $fieldNames.ContainsKey($parts[5])) {
|
||||
$known = if ($fieldNames.Count -gt 0) { ($fieldNames.Keys | Sort-Object) -join ", " } else { "полей нет" }
|
||||
Report-Error "21. $tag '$ref' — поля '$($parts[5])' нет в таблице (есть: $known)"
|
||||
$edsRefsBad = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($edsRefsChecked -gt 0 -and -not $edsRefsBad) { Report-OK "21. Field refs: $edsRefsChecked resolved" }
|
||||
|
||||
# Ключ: загрузка XML таблицу без ключа принимает (проверено на платформе), а Конфигуратор
|
||||
# интерактивно требует. Отсюда предупреждение, а не ошибка: рабочие конфигурации без ключа есть.
|
||||
$keyNodes = @($propsNode.SelectNodes("md:KeyFields/xr:Field", $ns))
|
||||
if ($keyNodes.Count -eq 0) {
|
||||
Report-Warn "21. KeyFields пуст — платформа такую таблицу загрузит, но форма записи и набор записей будут недоступны"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Final output ---
|
||||
|
||||
& $finalize
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# meta-validate v1.24 — Validate 1C metadata object structure (Python port)
|
||||
# meta-validate v1.27 — Validate 1C metadata object structure (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
@@ -226,6 +226,8 @@ valid_types = (
|
||||
"Report", "DataProcessor",
|
||||
"CommonModule", "ScheduledJob", "EventSubscription",
|
||||
"HTTPService", "WebService", "DefinedType",
|
||||
# Внешний источник данных и его таблица (корень файла таблицы — <Table>).
|
||||
"ExternalDataSource", "Table",
|
||||
)
|
||||
|
||||
# Валидные типы метаданных без глубоких правил валидации — раньше падали как "Unrecognized"
|
||||
@@ -258,6 +260,10 @@ generated_type_categories = {
|
||||
"Report": ["Object", "Manager"],
|
||||
"DataProcessor": ["Object", "Manager"],
|
||||
"DefinedType": ["DefinedType"],
|
||||
"ExternalDataSource": ["Manager", "TablesManager", "CubesManager"],
|
||||
# Таблица внешнего источника: имя элемента трёхчастное (Префикс.Источник.Таблица),
|
||||
# но проверка «имя оканчивается на .ИмяОбъекта» на нём работает как есть.
|
||||
"Table": ["Manager", "Object", "Ref", "List", "Record", "RecordSet", "RecordKey", "RecordManager"],
|
||||
}
|
||||
|
||||
# Types that have NO InternalInfo / GeneratedType
|
||||
@@ -319,6 +325,9 @@ child_object_rules = {
|
||||
"DocumentJournal": ["Column", "Form", "Template", "Command"],
|
||||
"HTTPService": ["URLTemplate"],
|
||||
"WebService": ["Operation"],
|
||||
# Внешний источник: таблицы перечислены именами, функции лежат полными узлами.
|
||||
"ExternalDataSource": ["Table", "Function", "Cube"],
|
||||
"Table": ["Field", "Form", "Template", "Command"],
|
||||
"Constant": ["Form"],
|
||||
"DefinedType": [],
|
||||
"CommonModule": [],
|
||||
@@ -343,7 +352,8 @@ valid_property_values = {
|
||||
"RealTimePosting": ["Allow", "Deny"],
|
||||
"RegisterRecordsDeletion": ["AutoDelete", "AutoDeleteOnUnpost", "AutoDeleteOff"],
|
||||
"RegisterRecordsWritingOnPost": ["WriteModified", "WriteSelected", "WriteAll"],
|
||||
"DataLockControlMode": ["Automatic", "Managed"],
|
||||
# AutomaticAndManaged — только у внешнего источника данных и его таблиц.
|
||||
"DataLockControlMode": ["Automatic", "Managed", "AutomaticAndManaged"],
|
||||
"FullTextSearch": ["Use", "DontUse"],
|
||||
"DefaultPresentation": ["AsDescription", "AsCode"],
|
||||
"HierarchyType": ["HierarchyFoldersAndItems", "HierarchyOfItems"],
|
||||
@@ -1706,6 +1716,13 @@ if not is_adopted:
|
||||
form_ref_bad = True
|
||||
continue
|
||||
|
||||
# Ссылка на форму таблицы внешнего источника — шестичастная:
|
||||
# ExternalDataSource.<Источник>.Table.<Таблица>.Form.<Форма>. Сводим её к четырём частям
|
||||
# (вид = Table, объект = имя таблицы), дальше проверка общая.
|
||||
if (len(parts) == 6 and parts[0] == "ExternalDataSource"
|
||||
and parts[2] == "Table" and parts[4] == "Form"):
|
||||
parts = ["Table", parts[3], "Form", parts[5]]
|
||||
|
||||
if len(parts) != 4 or parts[2] != "Form":
|
||||
report_warn(f"20. {tag} '{ref}' — неожиданный вид ссылки на форму "
|
||||
f"(ожидается 'CommonForm.Имя' или '<Вид>.<Объект>.Form.<Форма>')")
|
||||
@@ -1761,6 +1778,48 @@ if not is_adopted:
|
||||
if form_refs_checked > 0 and not form_ref_bad:
|
||||
report_ok(f"20. Form refs: {form_refs_checked} resolved")
|
||||
|
||||
# ── Check 21: таблица внешнего источника — ссылки на поля и наличие ключа ──
|
||||
# Свойства таблицы ссылаются на её же поля полным путём. Опечатка в имени поля даёт
|
||||
# «Неизвестный объект метаданных» при загрузке, а найти её глазами в шестичастном пути трудно.
|
||||
if md_type == "Table":
|
||||
field_names = set()
|
||||
if child_obj_node is not None:
|
||||
for f in find_all(child_obj_node, "md:Field/md:Properties/md:Name"):
|
||||
field_names.add(inner_text(f))
|
||||
eds_refs_checked = 0
|
||||
eds_refs_bad = False
|
||||
for tag, is_list in (("KeyFields", True), ("InputByString", True), ("DataLockFields", True),
|
||||
("PresentationField", False), ("ParentField", False), ("DataVersionField", False)):
|
||||
if is_list:
|
||||
refs = [inner_text(n) for n in find_all(props_node, f"md:{tag}/xr:Field")]
|
||||
else:
|
||||
n = find(props_node, f"md:{tag}")
|
||||
refs = [inner_text(n)] if n is not None and inner_text(n) else []
|
||||
for ref in refs:
|
||||
eds_refs_checked += 1
|
||||
parts = ref.split(".")
|
||||
# Ожидается ExternalDataSource.<Источник>.Table.<Таблица>.Field.<Поле>
|
||||
if len(parts) != 6 or parts[0] != "ExternalDataSource" or parts[2] != "Table" or parts[4] != "Field":
|
||||
report_error(f"21. {tag} '{ref}' — ожидается ExternalDataSource.<Источник>.Table.<Таблица>.Field.<Поле>")
|
||||
eds_refs_bad = True
|
||||
continue
|
||||
if parts[3] != obj_name:
|
||||
report_error(f"21. {tag} '{ref}' — ссылка на поле ЧУЖОЙ таблицы (эта: {obj_name})")
|
||||
eds_refs_bad = True
|
||||
continue
|
||||
if parts[5] not in field_names:
|
||||
known = ", ".join(sorted(field_names)) if field_names else "полей нет"
|
||||
report_error(f"21. {tag} '{ref}' — поля '{parts[5]}' нет в таблице (есть: {known})")
|
||||
eds_refs_bad = True
|
||||
if eds_refs_checked > 0 and not eds_refs_bad:
|
||||
report_ok(f"21. Field refs: {eds_refs_checked} resolved")
|
||||
|
||||
# Ключ: загрузка XML таблицу без ключа принимает (проверено на платформе), а Конфигуратор
|
||||
# интерактивно требует. Отсюда предупреждение, а не ошибка: рабочие конфигурации без ключа есть.
|
||||
if not find_all(props_node, "md:KeyFields/xr:Field"):
|
||||
report_warn("21. KeyFields пуст — платформа такую таблицу загрузит, но форма записи и набор записей будут недоступны")
|
||||
|
||||
|
||||
# ── Final output ──────────────────────────────────────────────
|
||||
|
||||
finalize()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# role-compile v1.32 — Compile 1C role from JSON
|
||||
# role-compile v1.36 — Compile 1C role from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -1185,9 +1185,102 @@ $enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
# --- 12. Register in Configuration.xml ---
|
||||
|
||||
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
# остаётся рабочим каталогом проекта.
|
||||
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Get-NewObjectPosition([string]$cfgDir) {
|
||||
try {
|
||||
if (-not $cfgDir) { $cfgDir = "." }
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||
if (-not $pj) { return "end" }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc -and $db.newObjectPosition) {
|
||||
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
} catch { return "end" }
|
||||
}
|
||||
|
||||
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
# Явно названный вид сортируется в любом случае.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Test-OrderSensitiveType([string]$typeName) {
|
||||
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||
}
|
||||
|
||||
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||
$keys = @("", "")
|
||||
$names = @($a, $b)
|
||||
for ($i = 0; $i -lt 2; $i++) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||
else { [void]$sb.Append('0') }
|
||||
[void]$sb.Append($ch)
|
||||
}
|
||||
$keys[$i] = $sb.ToString()
|
||||
}
|
||||
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||
if ($r -lt 0) { return -1 }
|
||||
if ($r -gt 0) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
# Регистрация объекта в <ChildObjects> родительского XML. Общая реализация: эталон —
|
||||
# meta-compile, копия — role-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
# meta-compile, копии — role-compile, xdto-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
# Возвращает исход: added | already | no-childobj | no-config.
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
|
||||
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
|
||||
|
||||
@@ -1206,56 +1299,73 @@ function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [st
|
||||
if ($e.InnerText -eq $ChildName) { return "already" }
|
||||
}
|
||||
|
||||
$newElem = $doc.CreateElement($ChildTag, "http://v8.1c.ru/8.3/MDClasses")
|
||||
$newElem.InnerText = $ChildName
|
||||
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
|
||||
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт —
|
||||
# дельта ровно в одну строку. Правим чужой файл, значит наследуем его стиль (#44/#46/#47).
|
||||
# DOM выше — только на чтение: найти ChildObjects и отсечь дубликат.
|
||||
$configContent = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
|
||||
$eol = if ($configContent.Contains("`r`n")) { "`r`n" } else { "`n" }
|
||||
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
if ($existing.Count -gt 0) {
|
||||
# Insert after last existing element of same type
|
||||
$lastElem = $existing[$existing.Count - 1]
|
||||
$newWs = $doc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertAfter($newWs, $lastElem) | Out-Null
|
||||
$childObjects.InsertAfter($newElem, $newWs) | Out-Null
|
||||
} else {
|
||||
# No existing elements of this type — insert before closing whitespace.
|
||||
# Самозакрытый <ChildObjects/> попадает сюда же: LastChild пуст, идёт ветка AppendChild.
|
||||
$lastChild = $childObjects.LastChild
|
||||
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
||||
$newWs = $doc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
|
||||
$childObjects.InsertBefore($newElem, $lastChild) | Out-Null
|
||||
} else {
|
||||
$childObjects.AppendChild($doc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||
$childObjects.AppendChild($newElem) | Out-Null
|
||||
$childObjects.AppendChild($doc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
$block = [regex]::Match($configContent, '(?s)<ChildObjects\s*>.*?</ChildObjects>')
|
||||
if (-not $block.Success) {
|
||||
# Самозакрытый <ChildObjects/> раскрываем первой записью
|
||||
$empty = [regex]::Match($configContent, '<ChildObjects\s*/>')
|
||||
if (-not $empty.Success) { return "no-childobj" }
|
||||
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
|
||||
$newContent = $configContent.Substring(0, $empty.Index) + $replacement + $configContent.Substring($empty.Index + $empty.Length)
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
|
||||
# byName: перед первым объектом того же вида, чьё имя больше нового.
|
||||
# Виды с осмысленным порядком в дереве пропускаем — см. Test-OrderSensitiveType.
|
||||
if (-not (Test-OrderSensitiveType $ChildTag) -and (Get-NewObjectPosition ([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($ParentXmlPath)))) -eq "byName") {
|
||||
$lineRx = [regex]"(?m)^([ \t]*)<$ChildTag>([^<]*)</$ChildTag>"
|
||||
$m = $lineRx.Match($configContent, $block.Index, $block.Length)
|
||||
while ($m.Success) {
|
||||
if ((Compare-MetadataNames $m.Groups[2].Value $ChildName) -gt 0) {
|
||||
$newContent = $configContent.Substring(0, $m.Index) + $m.Groups[1].Value + $entry + $eol + $configContent.Substring($m.Index)
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
$m = $m.NextMatch()
|
||||
}
|
||||
}
|
||||
|
||||
# Save. Пишем через MemoryStream, а не прямо в файл: нужен шаг пост-обработки
|
||||
# строки — XmlWriter отдаёт `encoding="utf-8"` и `<a />`, Конфигуратор пишет
|
||||
# `encoding="UTF-8"` и `<a/>`.
|
||||
$cfgSettings = New-Object System.Xml.XmlWriterSettings
|
||||
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$cfgSettings.Indent = $false
|
||||
$cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
|
||||
$doc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
|
||||
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ([System.IO.File]::ReadAllText($ParentXmlPath) -notmatch "`r`n") { "`n" } else { "`r`n" }
|
||||
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
|
||||
|
||||
$closeSame = "</$ChildTag>"
|
||||
$blockEnd = $block.Index + $block.Length
|
||||
$lastSame = $configContent.LastIndexOf($closeSame, $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
|
||||
if ($lastSame -ge 0) {
|
||||
# После последнего объекта того же вида (группы по видам сохраняются)
|
||||
$insertAt = $lastSame + $closeSame.Length
|
||||
$newContent = $configContent.Substring(0, $insertAt) + "$eol`t`t`t$entry" + $configContent.Substring($insertAt)
|
||||
} else {
|
||||
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
|
||||
# группой вида старше по $childObjectTypes. Дописать в конец блока нельзя: платформа
|
||||
# переставит группу при первой же выгрузке и даст диф на ровном месте.
|
||||
$ownIdx = $childObjectTypes.IndexOf($ChildTag)
|
||||
$anchor = $null
|
||||
if ($ownIdx -ge 0) {
|
||||
$typeRx = [regex]"(?m)^([ \t]*)<(\w+)>[^<]*</\2>"
|
||||
$tm = $typeRx.Match($configContent, $block.Index, $block.Length)
|
||||
while ($tm.Success) {
|
||||
$otherIdx = $childObjectTypes.IndexOf($tm.Groups[2].Value)
|
||||
if ($otherIdx -gt $ownIdx) { $anchor = $tm; break }
|
||||
$tm = $tm.NextMatch()
|
||||
}
|
||||
}
|
||||
if ($anchor) {
|
||||
$newContent = $configContent.Substring(0, $anchor.Index) + $anchor.Groups[1].Value + $entry + $eol + $configContent.Substring($anchor.Index)
|
||||
} else {
|
||||
# Видов старше в файле нет — новая строка перед </ChildObjects>,
|
||||
# отступ закрывающего тега переиспользуется
|
||||
$closeAt = $configContent.LastIndexOf("</ChildObjects>", $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
|
||||
$newContent = $configContent.Substring(0, $closeAt) + "`t$entry$eol`t`t" + $configContent.Substring($closeAt)
|
||||
}
|
||||
}
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# role-compile v1.32 — Compile 1C role from JSON
|
||||
# role-compile v1.36 — Compile 1C role from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -1056,10 +1056,106 @@ def parse_object_entry(entry):
|
||||
return {'Name': obj_name, 'Rights': rights}
|
||||
|
||||
|
||||
def get_new_object_position(cfg_dir):
|
||||
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
|
||||
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
остаётся рабочим каталогом проекта.
|
||||
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||
if not pj:
|
||||
return "end"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
proj_dir = os.path.dirname(pj)
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src and db.get("newObjectPosition"):
|
||||
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||
return "byName"
|
||||
return "end"
|
||||
except Exception:
|
||||
return "end"
|
||||
|
||||
|
||||
def is_order_sensitive_type(type_name):
|
||||
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
|
||||
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
Явно названный вид сортируется в любом случае.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||
|
||||
|
||||
def compare_metadata_names(a, b):
|
||||
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
|
||||
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
keys = []
|
||||
for name in (a, b):
|
||||
parts = []
|
||||
for ch in name.lower():
|
||||
if ch == "ё":
|
||||
ch = "е"
|
||||
if ch.isdigit():
|
||||
parts.append("1" + ch)
|
||||
elif ch.isalpha():
|
||||
parts.append("2" + ch)
|
||||
else:
|
||||
parts.append("0" + ch)
|
||||
keys.append("".join(parts))
|
||||
if keys[0] != keys[1]:
|
||||
return -1 if keys[0] < keys[1] else 1
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
return 0
|
||||
|
||||
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
|
||||
]
|
||||
|
||||
|
||||
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
|
||||
"""Регистрация объекта в <ChildObjects> родительского XML.
|
||||
|
||||
Общая реализация: эталон — meta-compile, копия — role-compile.
|
||||
Общая реализация: эталон — meta-compile, копии — role-compile, xdto-compile.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
Возвращает исход: added | already | no-childobj | no-config.
|
||||
"""
|
||||
@@ -1110,6 +1206,19 @@ def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name)
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
# byName: перед первым объектом того же вида, чьё имя больше нового.
|
||||
# Виды с осмысленным порядком в дереве пропускаем — см. is_order_sensitive_type.
|
||||
if (not is_order_sensitive_type(child_tag)
|
||||
and get_new_object_position(os.path.dirname(os.path.abspath(parent_xml_path))) == 'byName'):
|
||||
line_rx = re.compile(rf'(?m)^([ \t]*)<{child_tag}>([^<]*)</{child_tag}>')
|
||||
for m in line_rx.finditer(config_content, block.start(), block.end()):
|
||||
if compare_metadata_names(m.group(2), child_name) > 0:
|
||||
new_content = (config_content[:m.start()]
|
||||
+ f'{m.group(1)}{entry}{eol}'
|
||||
+ config_content[m.start():])
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
close_same = f'</{child_tag}>'
|
||||
last_same = config_content.rfind(close_same, block.start(), block.end())
|
||||
if last_same != -1:
|
||||
@@ -1119,12 +1228,29 @@ def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name)
|
||||
+ f'{eol}\t\t\t{entry}'
|
||||
+ config_content[insert_at:])
|
||||
else:
|
||||
# No element of this type yet: new line before </ChildObjects>,
|
||||
# reusing the block's existing closing indent for </ChildObjects>.
|
||||
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
|
||||
new_content = (config_content[:close_at]
|
||||
+ f'\t{entry}{eol}\t\t'
|
||||
+ config_content[close_at:])
|
||||
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
|
||||
# группой вида старше по CHILD_OBJECT_TYPES. Дописать в конец блока нельзя: платформа
|
||||
# переставит группу при первой же выгрузке и даст диф на ровном месте.
|
||||
anchor = None
|
||||
if child_tag in CHILD_OBJECT_TYPES:
|
||||
own_idx = CHILD_OBJECT_TYPES.index(child_tag)
|
||||
type_rx = re.compile(r'(?m)^([ \t]*)<(\w+)>[^<]*</\2>')
|
||||
for m in type_rx.finditer(config_content, block.start(), block.end()):
|
||||
other = m.group(2)
|
||||
if other in CHILD_OBJECT_TYPES and CHILD_OBJECT_TYPES.index(other) > own_idx:
|
||||
anchor = m
|
||||
break
|
||||
if anchor is not None:
|
||||
new_content = (config_content[:anchor.start()]
|
||||
+ f'{anchor.group(1)}{entry}{eol}'
|
||||
+ config_content[anchor.start():])
|
||||
else:
|
||||
# Видов старше в файле нет — новая строка перед </ChildObjects>,
|
||||
# отступ закрывающего тега переиспользуется.
|
||||
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
|
||||
new_content = (config_content[:close_at]
|
||||
+ f'\t{entry}{eol}\t\t'
|
||||
+ config_content[close_at:])
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# subsystem-compile v1.30 — Create 1C subsystem from JSON definition
|
||||
# subsystem-compile v1.33 — Create 1C subsystem from JSON definition
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -677,6 +677,25 @@ if ($children.Count -gt 0) {
|
||||
# внутри подсистемы нечего — потомок там всегда один и тот же.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
# Возвращает исход: added | already | no-childobj | no-config.
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
|
||||
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
|
||||
|
||||
@@ -690,77 +709,54 @@ function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [st
|
||||
$childObjects = $doc.SelectSingleNode("//md:$ParentTag/md:ChildObjects", $ns)
|
||||
if (-not $childObjects) { return "no-childobj" }
|
||||
|
||||
# Check for self-closing tag
|
||||
$isSelfClosing = (-not $childObjects.HasChildNodes) -or ($childObjects.IsEmpty)
|
||||
|
||||
# Check if already registered
|
||||
foreach ($child in $childObjects.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Element' -and $child.LocalName -eq $ChildTag -and $child.InnerText -eq $ChildName) {
|
||||
return "already"
|
||||
}
|
||||
}
|
||||
|
||||
$newEl = $doc.CreateElement($ChildTag, "http://v8.1c.ru/8.3/MDClasses")
|
||||
$newEl.InnerText = $ChildName
|
||||
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
|
||||
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт.
|
||||
# Правим чужой файл, значит наследуем его стиль (#44/#46/#47). DOM выше — только на чтение.
|
||||
$rawText = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
|
||||
$eol = if ($rawText.Contains("`r`n")) { "`r`n" } else { "`n" }
|
||||
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
|
||||
|
||||
if ($isSelfClosing) {
|
||||
# Expand self-closing tag
|
||||
$parentIndent = ""
|
||||
$prev = $childObjects.PreviousSibling
|
||||
if ($prev -and ($prev.NodeType -eq 'Whitespace' -or $prev.NodeType -eq 'SignificantWhitespace')) {
|
||||
if ($prev.Value -match '(\t+)$') { $parentIndent = $Matches[1] }
|
||||
}
|
||||
$childIndent = "$parentIndent`t"
|
||||
$ws1 = $doc.CreateWhitespace("`r`n$childIndent")
|
||||
$ws2 = $doc.CreateWhitespace("`r`n$parentIndent")
|
||||
$childObjects.AppendChild($ws1) | Out-Null
|
||||
$childObjects.AppendChild($newEl) | Out-Null
|
||||
$childObjects.AppendChild($ws2) | Out-Null
|
||||
$empty = [regex]::Match($rawText, '<ChildObjects\s*/>')
|
||||
if ($empty.Success) {
|
||||
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
|
||||
$rawText = $rawText.Substring(0, $empty.Index) + $replacement + $rawText.Substring($empty.Index + $empty.Length)
|
||||
} else {
|
||||
# Insert before trailing whitespace
|
||||
$childIndent = "`t`t`t"
|
||||
foreach ($child in $childObjects.ChildNodes) {
|
||||
if ($child.NodeType -eq 'Whitespace' -or $child.NodeType -eq 'SignificantWhitespace') {
|
||||
if ($child.Value -match '^\r?\n(\t+)') { $childIndent = $Matches[1]; break }
|
||||
# В корне подсистема встаёт перед первой группой вида старше по $childObjectTypes:
|
||||
# так она попадает и в канонический порядок видов, и в конец своей группы, если та уже
|
||||
# есть. Дописать в конец блока нельзя вдвойне: платформа переставит новую группу при
|
||||
# первой же выгрузке, а существующую подсистема покинула бы, уехав за виды ниже.
|
||||
# Во вложенном Subsystem.xml порядок видов неприменим — потомок там всегда один.
|
||||
$anchor = $null
|
||||
if ($ParentTag -eq "Configuration") {
|
||||
$ownIdx = $childObjectTypes.IndexOf($ChildTag)
|
||||
if ($ownIdx -ge 0) {
|
||||
$typeRx = [regex]"(?m)^([ \t]*)<(\w+)>[^<]*</\2>"
|
||||
$tm = $typeRx.Match($rawText)
|
||||
while ($tm.Success) {
|
||||
$otherIdx = $childObjectTypes.IndexOf($tm.Groups[2].Value)
|
||||
if ($otherIdx -gt $ownIdx) { $anchor = $tm; break }
|
||||
$tm = $tm.NextMatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
$trailing = $childObjects.LastChild
|
||||
$ws = $doc.CreateWhitespace("`r`n$childIndent")
|
||||
if ($trailing -and ($trailing.NodeType -eq 'Whitespace' -or $trailing.NodeType -eq 'SignificantWhitespace')) {
|
||||
$childObjects.InsertBefore($ws, $trailing) | Out-Null
|
||||
$childObjects.InsertBefore($newEl, $trailing) | Out-Null
|
||||
if ($anchor) {
|
||||
$rawText = $rawText.Substring(0, $anchor.Index) + $anchor.Groups[1].Value + $entry + $eol + $rawText.Substring($anchor.Index)
|
||||
} else {
|
||||
$childObjects.AppendChild($ws) | Out-Null
|
||||
$childObjects.AppendChild($newEl) | Out-Null
|
||||
# Отступ вставки берём у закрывающего тега +1 уровень: подстановка по голому
|
||||
# '</ChildObjects>' удваивала бы уже присутствующий отступ строки.
|
||||
$cm = [regex]::Match($rawText, '([ ]*)</ChildObjects>')
|
||||
if (-not $cm.Success) { return "no-childobj" }
|
||||
$rawText = $rawText.Substring(0, $cm.Index) + $cm.Groups[1].Value + "`t" + $entry + $eol + $cm.Groups[1].Value + "</ChildObjects>" + $rawText.Substring($cm.Index + $cm.Length)
|
||||
}
|
||||
}
|
||||
|
||||
# Save parent XML
|
||||
$settings = New-Object System.Xml.XmlWriterSettings
|
||||
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$settings.Indent = $false
|
||||
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
|
||||
$doc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
|
||||
$bytes = $memStream.ToArray()
|
||||
$memStream.Close()
|
||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
|
||||
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
|
||||
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $ParentXmlPath) -and ([System.IO.File]::ReadAllText($ParentXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $text, (New-Object System.Text.UTF8Encoding($true)))
|
||||
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $rawText, (New-Object System.Text.UTF8Encoding($true)))
|
||||
return "added"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# subsystem-compile v1.30 — Create 1C subsystem from JSON definition
|
||||
# subsystem-compile v1.33 — Create 1C subsystem from JSON definition
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -450,6 +450,26 @@ def write_child_subsystem_stub(child_path, child_name, format_version):
|
||||
write_utf8_bom(child_path, '\r\n'.join(lines))
|
||||
|
||||
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
|
||||
]
|
||||
|
||||
|
||||
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
|
||||
"""Регистрация объекта в <ChildObjects> родительского XML.
|
||||
|
||||
@@ -489,16 +509,37 @@ def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name)
|
||||
# Правку ведём по сырому тексту, а не сериализацией ET: она не сохраняет отступы
|
||||
# и теряет xmlns, объявленные только внутри значений атрибутов (#38).
|
||||
entry = f'<{child_tag}>{esc_xml_text(child_name)}</{child_tag}>'
|
||||
if '<ChildObjects/>' in raw_text:
|
||||
empty = re.search(r'<ChildObjects\s*/>', raw_text)
|
||||
if empty is not None:
|
||||
replacement = '<ChildObjects>' + eol + f'\t\t\t{entry}' + eol + '\t\t</ChildObjects>'
|
||||
raw_text = raw_text.replace('<ChildObjects/>', replacement, 1)
|
||||
elif '</ChildObjects>' in raw_text:
|
||||
# Отступ вставки берём у закрывающего тега +1 уровень: подстановка
|
||||
# по голому '</ChildObjects>' удваивала бы уже присутствующий отступ
|
||||
# строки (получалось 5 табов вместо 3 — PS-порт через DOM даёт 3).
|
||||
raw_text = re.sub(r'([ \t]*)</ChildObjects>',
|
||||
lambda m: m.group(1) + '\t' + entry + eol + m.group(1) + '</ChildObjects>',
|
||||
raw_text, count=1)
|
||||
raw_text = raw_text[:empty.start()] + replacement + raw_text[empty.end():]
|
||||
else:
|
||||
# В корне подсистема встаёт перед первой группой вида старше по CHILD_OBJECT_TYPES:
|
||||
# так она попадает и в канонический порядок видов, и в конец своей группы, если та уже
|
||||
# есть. Дописать в конец блока нельзя вдвойне: платформа переставит новую группу при
|
||||
# первой же выгрузке, а существующую подсистема покинула бы, уехав за виды ниже.
|
||||
# Во вложенном Subsystem.xml порядок видов неприменим — потомок там всегда один.
|
||||
anchor = None
|
||||
if parent_tag == 'Configuration' and child_tag in CHILD_OBJECT_TYPES:
|
||||
own_idx = CHILD_OBJECT_TYPES.index(child_tag)
|
||||
type_rx = re.compile(r'(?m)^([ \t]*)<(\w+)>[^<]*</\2>')
|
||||
for m in type_rx.finditer(raw_text):
|
||||
other = m.group(2)
|
||||
if other in CHILD_OBJECT_TYPES and CHILD_OBJECT_TYPES.index(other) > own_idx:
|
||||
anchor = m
|
||||
break
|
||||
if anchor is not None:
|
||||
raw_text = (raw_text[:anchor.start()] + anchor.group(1) + entry + eol
|
||||
+ raw_text[anchor.start():])
|
||||
else:
|
||||
# Отступ вставки берём у закрывающего тега +1 уровень: подстановка
|
||||
# по голому '</ChildObjects>' удваивала бы уже присутствующий отступ
|
||||
# строки (получалось 5 табов вместо 3 — PS-порт через DOM даёт 3).
|
||||
cm = re.search(r'([ \t]*)</ChildObjects>', raw_text)
|
||||
if cm is None:
|
||||
return 'no-childobj'
|
||||
raw_text = (raw_text[:cm.start()] + cm.group(1) + '\t' + entry + eol
|
||||
+ cm.group(1) + '</ChildObjects>' + raw_text[cm.end():])
|
||||
|
||||
write_utf8_bom(parent_xml_path, raw_text)
|
||||
return 'added'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# xdto-compile v1.12 — Build a 1C XDTO package from an XML Schema (XSD) (+support-guard: общая реализация вместо урезанной)
|
||||
# xdto-compile v1.16 — Build a 1C XDTO package from an XML Schema (XSD) (+support-guard: общая реализация вместо урезанной)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
[CmdletBinding(PositionalBinding=$false)]
|
||||
param(
|
||||
@@ -988,69 +988,198 @@ if ($declaredImports.Count -gt 0 -and (Test-Path $xdtoRootDir)) {
|
||||
}
|
||||
}
|
||||
|
||||
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
|
||||
$regResult = "no-config"
|
||||
if (Test-Path $configXmlPath) {
|
||||
$configDoc = New-Object System.Xml.XmlDocument
|
||||
$configDoc.PreserveWhitespace = $true
|
||||
$configDoc.Load($configXmlPath)
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable)
|
||||
$nsMgr.AddNamespace("md", $MD_NS)
|
||||
$childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr)
|
||||
if ($childObjects) {
|
||||
$existing = $childObjects.SelectNodes("md:XDTOPackage", $nsMgr)
|
||||
$already = $false
|
||||
foreach ($e in $existing) { if ($e.InnerText -eq $Name) { $already = $true; break } }
|
||||
if ($already) {
|
||||
$regResult = "already"
|
||||
} else {
|
||||
$newElem = $configDoc.CreateElement("XDTOPackage", $MD_NS)
|
||||
$newElem.InnerText = $Name
|
||||
if ($existing.Count -gt 0) {
|
||||
$lastElem = $existing[$existing.Count - 1]
|
||||
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertAfter($newWs, $lastElem) | Out-Null
|
||||
$childObjects.InsertAfter($newElem, $newWs) | Out-Null
|
||||
} else {
|
||||
$lastChild = $childObjects.LastChild
|
||||
if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
||||
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
|
||||
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
|
||||
$childObjects.InsertBefore($newElem, $lastChild) | Out-Null
|
||||
} else {
|
||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||
$childObjects.AppendChild($newElem) | Out-Null
|
||||
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||
function Esc-XmlText {
|
||||
param([string]$s)
|
||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
||||
}
|
||||
|
||||
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
# остаётся рабочим каталогом проекта.
|
||||
# configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Get-NewObjectPosition([string]$cfgDir) {
|
||||
try {
|
||||
if (-not $cfgDir) { $cfgDir = "." }
|
||||
$pj = Find-V8Project (Get-Location).Path
|
||||
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
|
||||
if (-not $pj) { return "end" }
|
||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
||||
$projDir = [System.IO.Path]::GetDirectoryName($pj)
|
||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
||||
if ($proj.databases) {
|
||||
foreach ($db in $proj.databases) {
|
||||
if ($db.configSrc -and $db.newObjectPosition) {
|
||||
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
|
||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
||||
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
}
|
||||
}
|
||||
}
|
||||
$cfgSettings = New-Object System.Xml.XmlWriterSettings
|
||||
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
|
||||
$cfgSettings.Indent = $false
|
||||
$cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
|
||||
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
|
||||
$memStream = New-Object System.IO.MemoryStream
|
||||
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
|
||||
$configDoc.Save($writer)
|
||||
$writer.Flush(); $writer.Close()
|
||||
}
|
||||
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
|
||||
return "end"
|
||||
} catch { return "end" }
|
||||
}
|
||||
|
||||
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
|
||||
$memStream.Close()
|
||||
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
|
||||
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
|
||||
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
|
||||
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
|
||||
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
|
||||
$cfgText = [regex]::Replace($cfgText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
|
||||
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
|
||||
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
|
||||
$targetEol = if ((Test-Path -LiteralPath $configXmlPath) -and ([System.IO.File]::ReadAllText($configXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
|
||||
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
|
||||
[System.IO.File]::WriteAllText($configXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
|
||||
$regResult = "added"
|
||||
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
# Явно названный вид сортируется в любом случае.
|
||||
# Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Test-OrderSensitiveType([string]$typeName) {
|
||||
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
|
||||
}
|
||||
|
||||
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
function Compare-MetadataNames([string]$a, [string]$b) {
|
||||
$keys = @("", "")
|
||||
$names = @($a, $b)
|
||||
for ($i = 0; $i -lt 2; $i++) {
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
|
||||
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
|
||||
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
|
||||
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
|
||||
else { [void]$sb.Append('0') }
|
||||
[void]$sb.Append($ch)
|
||||
}
|
||||
$keys[$i] = $sb.ToString()
|
||||
}
|
||||
$r = [string]::CompareOrdinal($keys[0], $keys[1])
|
||||
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
|
||||
if ($r -lt 0) { return -1 }
|
||||
if ($r -gt 0) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
# Регистрация объекта в <ChildObjects> родительского XML. Общая реализация: эталон —
|
||||
# meta-compile, копии — role-compile, xdto-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
# Возвращает исход: added | already | no-childobj | no-config.
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
$childObjectTypes = @(
|
||||
"Language","Subsystem","StyleItem","Style",
|
||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
|
||||
"Constant","CommonForm","Catalog","Document",
|
||||
"DocumentNumerator","Sequence","DocumentJournal","Enum",
|
||||
"Report","DataProcessor","InformationRegister","AccumulationRegister",
|
||||
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
|
||||
"ChartOfCalculationTypes","CalculationRegister",
|
||||
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
|
||||
)
|
||||
|
||||
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
|
||||
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $true
|
||||
$doc.Load($ParentXmlPath)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
|
||||
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
|
||||
$childObjects = $doc.SelectSingleNode("//md:$ParentTag/md:ChildObjects", $nsMgr)
|
||||
if (-not $childObjects) { return "no-childobj" }
|
||||
|
||||
$existing = $childObjects.SelectNodes("md:$ChildTag", $nsMgr)
|
||||
foreach ($e in $existing) {
|
||||
if ($e.InnerText -eq $ChildName) { return "already" }
|
||||
}
|
||||
|
||||
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
|
||||
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт —
|
||||
# дельта ровно в одну строку. Правим чужой файл, значит наследуем его стиль (#44/#46/#47).
|
||||
# DOM выше — только на чтение: найти ChildObjects и отсечь дубликат.
|
||||
$configContent = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
|
||||
$eol = if ($configContent.Contains("`r`n")) { "`r`n" } else { "`n" }
|
||||
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
|
||||
$block = [regex]::Match($configContent, '(?s)<ChildObjects\s*>.*?</ChildObjects>')
|
||||
if (-not $block.Success) {
|
||||
# Самозакрытый <ChildObjects/> раскрываем первой записью
|
||||
$empty = [regex]::Match($configContent, '<ChildObjects\s*/>')
|
||||
if (-not $empty.Success) { return "no-childobj" }
|
||||
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
|
||||
$newContent = $configContent.Substring(0, $empty.Index) + $replacement + $configContent.Substring($empty.Index + $empty.Length)
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
|
||||
# byName: перед первым объектом того же вида, чьё имя больше нового.
|
||||
# Виды с осмысленным порядком в дереве пропускаем — см. Test-OrderSensitiveType.
|
||||
if (-not (Test-OrderSensitiveType $ChildTag) -and (Get-NewObjectPosition ([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($ParentXmlPath)))) -eq "byName") {
|
||||
$lineRx = [regex]"(?m)^([ \t]*)<$ChildTag>([^<]*)</$ChildTag>"
|
||||
$m = $lineRx.Match($configContent, $block.Index, $block.Length)
|
||||
while ($m.Success) {
|
||||
if ((Compare-MetadataNames $m.Groups[2].Value $ChildName) -gt 0) {
|
||||
$newContent = $configContent.Substring(0, $m.Index) + $m.Groups[1].Value + $entry + $eol + $configContent.Substring($m.Index)
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
$m = $m.NextMatch()
|
||||
}
|
||||
}
|
||||
|
||||
$closeSame = "</$ChildTag>"
|
||||
$blockEnd = $block.Index + $block.Length
|
||||
$lastSame = $configContent.LastIndexOf($closeSame, $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
|
||||
if ($lastSame -ge 0) {
|
||||
# После последнего объекта того же вида (группы по видам сохраняются)
|
||||
$insertAt = $lastSame + $closeSame.Length
|
||||
$newContent = $configContent.Substring(0, $insertAt) + "$eol`t`t`t$entry" + $configContent.Substring($insertAt)
|
||||
} else {
|
||||
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
|
||||
# группой вида старше по $childObjectTypes. Дописать в конец блока нельзя: платформа
|
||||
# переставит группу при первой же выгрузке и даст диф на ровном месте.
|
||||
$ownIdx = $childObjectTypes.IndexOf($ChildTag)
|
||||
$anchor = $null
|
||||
if ($ownIdx -ge 0) {
|
||||
$typeRx = [regex]"(?m)^([ \t]*)<(\w+)>[^<]*</\2>"
|
||||
$tm = $typeRx.Match($configContent, $block.Index, $block.Length)
|
||||
while ($tm.Success) {
|
||||
$otherIdx = $childObjectTypes.IndexOf($tm.Groups[2].Value)
|
||||
if ($otherIdx -gt $ownIdx) { $anchor = $tm; break }
|
||||
$tm = $tm.NextMatch()
|
||||
}
|
||||
}
|
||||
if ($anchor) {
|
||||
$newContent = $configContent.Substring(0, $anchor.Index) + $anchor.Groups[1].Value + $entry + $eol + $configContent.Substring($anchor.Index)
|
||||
} else {
|
||||
# Видов старше в файле нет — новая строка перед </ChildObjects>,
|
||||
# отступ закрывающего тега переиспользуется
|
||||
$closeAt = $configContent.LastIndexOf("</ChildObjects>", $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
|
||||
$newContent = $configContent.Substring(0, $closeAt) + "`t$entry$eol`t`t" + $configContent.Substring($closeAt)
|
||||
}
|
||||
}
|
||||
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
|
||||
return "added"
|
||||
}
|
||||
|
||||
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
|
||||
$regResult = Register-InChildObjects $configXmlPath "Configuration" "XDTOPackage" $Name
|
||||
|
||||
# --- Report ---
|
||||
|
||||
$typeCount = 0
|
||||
@@ -1069,5 +1198,6 @@ if ($script:warnings.Count -gt 0) {
|
||||
switch ($regResult) {
|
||||
"added" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> добавлен в ChildObjects" }
|
||||
"already" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> уже зарегистрирован" }
|
||||
"no-childobj" { [Console]::Error.WriteLine("ПРЕДУПРЕЖДЕНИЕ: Configuration.xml найден, но <ChildObjects> не найден") }
|
||||
"no-config" { Write-Host " Configuration.xml не найден — регистрация пропущена" }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# xdto-compile v1.12 — Build a 1C XDTO package from an XML Schema (XSD) (Python port) (+support-guard: общая реализация вместо урезанной)
|
||||
# xdto-compile v1.16 — Build a 1C XDTO package from an XML Schema (XSD) (Python port) (+support-guard: общая реализация вместо урезанной)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -6,6 +6,7 @@ import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from lxml import etree
|
||||
|
||||
@@ -1093,43 +1094,219 @@ if declared_imports and os.path.isdir(xdto_root_dir):
|
||||
warn(f'Импорт "{imp}" не разрешается: пакета с таким namespace в конфигурации нет. '
|
||||
"Платформа отвергнет пакет при обновлении — соберите зависимость первой")
|
||||
|
||||
config_xml = os.path.join(args.OutputDir, "Configuration.xml")
|
||||
reg_result = "no-config"
|
||||
if os.path.exists(config_xml):
|
||||
with open(config_xml, "rb") as f:
|
||||
raw = f.read()
|
||||
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
cfg_doc = _parse_xml(config_xml)
|
||||
child_objects = cfg_doc.find(f".//{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects")
|
||||
if child_objects is not None:
|
||||
existing = child_objects.findall(f"{{{MD_NS}}}XDTOPackage")
|
||||
if any((e.text or "") == name for e in existing):
|
||||
reg_result = "already"
|
||||
else:
|
||||
new_elem = etree.SubElement(child_objects, f"{{{MD_NS}}}XDTOPackage")
|
||||
new_elem.text = name
|
||||
if existing:
|
||||
last = existing[-1]
|
||||
new_elem.tail = last.tail
|
||||
child_objects.remove(new_elem)
|
||||
last.addnext(new_elem)
|
||||
def esc_xml_text(s):
|
||||
# Эскейп ТЕКСТА элемента: только & < > (кавычки в тексте 1С держит raw).
|
||||
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
def write_utf8_bom(path, content):
|
||||
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
|
||||
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
|
||||
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def get_new_object_position(cfg_dir):
|
||||
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
|
||||
|
||||
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
|
||||
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
|
||||
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
|
||||
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
|
||||
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
|
||||
остаётся рабочим каталогом проекта.
|
||||
configSrc считается от каталога .v8-project.json, как задокументировано в
|
||||
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
try:
|
||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
|
||||
if not pj:
|
||||
return "end"
|
||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
||||
proj_dir = os.path.dirname(pj)
|
||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
|
||||
for db in proj.get("databases", []):
|
||||
src = db.get("configSrc")
|
||||
if src and db.get("newObjectPosition"):
|
||||
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
|
||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
||||
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
|
||||
if str(proj.get("newObjectPosition") or "").lower() == "byname":
|
||||
return "byName"
|
||||
return "end"
|
||||
except Exception:
|
||||
return "end"
|
||||
|
||||
|
||||
def is_order_sensitive_type(type_name):
|
||||
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
|
||||
|
||||
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
|
||||
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
|
||||
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
|
||||
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
|
||||
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
|
||||
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
|
||||
Явно названный вид сортируется в любом случае.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
|
||||
|
||||
|
||||
def compare_metadata_names(a, b):
|
||||
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
|
||||
|
||||
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
|
||||
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
|
||||
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
|
||||
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
|
||||
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
"""
|
||||
keys = []
|
||||
for name in (a, b):
|
||||
parts = []
|
||||
for ch in name.lower():
|
||||
if ch == "ё":
|
||||
ch = "е"
|
||||
if ch.isdigit():
|
||||
parts.append("1" + ch)
|
||||
elif ch.isalpha():
|
||||
parts.append("2" + ch)
|
||||
else:
|
||||
new_elem.tail = child_objects.text
|
||||
data = etree.tostring(cfg_doc, xml_declaration=True, encoding="UTF-8")
|
||||
# lxml пишет декларацию в ОДИНАРНЫХ кавычках, платформа и PS-порт — в двойных.
|
||||
data = data.replace(b"<?xml version='1.0' encoding='UTF-8'?>",
|
||||
b'<?xml version="1.0" encoding="UTF-8"?>')
|
||||
# Парсер XML по спецификации схлопывает CRLF в LF, поэтому tostring отдаёт
|
||||
# LF-документ. Возвращаем EOL исходного файла: правка существующего файла
|
||||
# сохраняет его стиль (#44/#46/#47). Правило то же, что у _detect_xml_style
|
||||
# и у $targetEol в PS-порту: есть CRLF → CRLF.
|
||||
src_eol = b"\r\n" if b"\r\n" in raw else b"\n"
|
||||
data = data.replace(b"\r\n", b"\n").replace(b"\n", src_eol)
|
||||
if had_bom:
|
||||
data = b"\xef\xbb\xbf" + data
|
||||
with open(config_xml, "wb") as f:
|
||||
f.write(data)
|
||||
reg_result = "added"
|
||||
parts.append("0" + ch)
|
||||
keys.append("".join(parts))
|
||||
if keys[0] != keys[1]:
|
||||
return -1 if keys[0] < keys[1] else 1
|
||||
if a != b:
|
||||
return -1 if a < b else 1
|
||||
return 0
|
||||
|
||||
|
||||
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
|
||||
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
|
||||
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
|
||||
# Реестр карт: tests/skills/check-type-maps.mjs.
|
||||
CHILD_OBJECT_TYPES = [
|
||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
|
||||
'Constant', 'CommonForm', 'Catalog', 'Document',
|
||||
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
|
||||
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
|
||||
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
|
||||
'ChartOfCalculationTypes', 'CalculationRegister',
|
||||
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
|
||||
]
|
||||
|
||||
|
||||
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
|
||||
"""Регистрация объекта в <ChildObjects> родительского XML.
|
||||
|
||||
Общая реализация: эталон — meta-compile, копии — role-compile, xdto-compile.
|
||||
Реестр семьи: tests/skills/check-inline-drift.mjs.
|
||||
Возвращает исход: added | already | no-childobj | no-config.
|
||||
"""
|
||||
if not os.path.isfile(parent_xml_path):
|
||||
return 'no-config'
|
||||
|
||||
# Read raw content, preserving BOM/EOL byte-for-byte (newline='' => no translation)
|
||||
with open(parent_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
|
||||
config_content = f.read()
|
||||
|
||||
ns = 'http://v8.1c.ru/8.3/MDClasses'
|
||||
# ET is used ONLY read-only here: to locate ChildObjects and detect a duplicate.
|
||||
# We deliberately do NOT re-serialize Configuration.xml with ElementTree.write():
|
||||
# it drops every xmlns declaration used only inside attribute VALUES (e.g.
|
||||
# xsi:type="app:ApplicationUsePurpose" in UsePurposes) because ET never sees such
|
||||
# prefixes in element/attribute names. The dropped declaration makes XDTO read the
|
||||
# value as anyType and Designer refuses to load the file (issue #38). Registration is
|
||||
# therefore done by raw-text insertion, preserving BOM, EOL and all namespaces
|
||||
# byte-for-byte (same approach as subsystem-compile).
|
||||
tree = ET.parse(parent_xml_path)
|
||||
root = tree.getroot()
|
||||
|
||||
child_objects = root.find(f'{{{ns}}}{parent_tag}/{{{ns}}}ChildObjects')
|
||||
if child_objects is None:
|
||||
# Try direct path
|
||||
parent_elem = root.find(f'{{{ns}}}{parent_tag}')
|
||||
if parent_elem is not None:
|
||||
child_objects = parent_elem.find(f'{{{ns}}}ChildObjects')
|
||||
|
||||
if child_objects is None:
|
||||
return 'no-childobj'
|
||||
|
||||
existing = child_objects.findall(f'{{{ns}}}{child_tag}')
|
||||
if any((e.text or '').strip() == child_name for e in existing):
|
||||
return 'already'
|
||||
|
||||
eol = '\r\n' if '\r\n' in config_content else '\n'
|
||||
entry = f'<{child_tag}>{esc_xml_text(child_name)}</{child_tag}>'
|
||||
|
||||
block = re.search(r'<ChildObjects\s*>.*?</ChildObjects>', config_content, re.S)
|
||||
if block is None:
|
||||
# Empty self-closing <ChildObjects/> => open it with the first entry.
|
||||
empty = re.search(r'<ChildObjects\s*/>', config_content)
|
||||
if empty is None:
|
||||
return 'no-childobj'
|
||||
replacement = f'<ChildObjects>{eol}\t\t\t{entry}{eol}\t\t</ChildObjects>'
|
||||
new_content = config_content[:empty.start()] + replacement + config_content[empty.end():]
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
# byName: перед первым объектом того же вида, чьё имя больше нового.
|
||||
# Виды с осмысленным порядком в дереве пропускаем — см. is_order_sensitive_type.
|
||||
if (not is_order_sensitive_type(child_tag)
|
||||
and get_new_object_position(os.path.dirname(os.path.abspath(parent_xml_path))) == 'byName'):
|
||||
line_rx = re.compile(rf'(?m)^([ \t]*)<{child_tag}>([^<]*)</{child_tag}>')
|
||||
for m in line_rx.finditer(config_content, block.start(), block.end()):
|
||||
if compare_metadata_names(m.group(2), child_name) > 0:
|
||||
new_content = (config_content[:m.start()]
|
||||
+ f'{m.group(1)}{entry}{eol}'
|
||||
+ config_content[m.start():])
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
close_same = f'</{child_tag}>'
|
||||
last_same = config_content.rfind(close_same, block.start(), block.end())
|
||||
if last_same != -1:
|
||||
# After the last element of the same type (keeps them grouped).
|
||||
insert_at = last_same + len(close_same)
|
||||
new_content = (config_content[:insert_at]
|
||||
+ f'{eol}\t\t\t{entry}'
|
||||
+ config_content[insert_at:])
|
||||
else:
|
||||
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
|
||||
# группой вида старше по CHILD_OBJECT_TYPES. Дописать в конец блока нельзя: платформа
|
||||
# переставит группу при первой же выгрузке и даст диф на ровном месте.
|
||||
anchor = None
|
||||
if child_tag in CHILD_OBJECT_TYPES:
|
||||
own_idx = CHILD_OBJECT_TYPES.index(child_tag)
|
||||
type_rx = re.compile(r'(?m)^([ \t]*)<(\w+)>[^<]*</\2>')
|
||||
for m in type_rx.finditer(config_content, block.start(), block.end()):
|
||||
other = m.group(2)
|
||||
if other in CHILD_OBJECT_TYPES and CHILD_OBJECT_TYPES.index(other) > own_idx:
|
||||
anchor = m
|
||||
break
|
||||
if anchor is not None:
|
||||
new_content = (config_content[:anchor.start()]
|
||||
+ f'{anchor.group(1)}{entry}{eol}'
|
||||
+ config_content[anchor.start():])
|
||||
else:
|
||||
# Видов старше в файле нет — новая строка перед </ChildObjects>,
|
||||
# отступ закрывающего тега переиспользуется.
|
||||
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
|
||||
new_content = (config_content[:close_at]
|
||||
+ f'\t{entry}{eol}\t\t'
|
||||
+ config_content[close_at:])
|
||||
write_utf8_bom(parent_xml_path, new_content)
|
||||
return 'added'
|
||||
|
||||
|
||||
config_xml = os.path.join(args.OutputDir, "Configuration.xml")
|
||||
reg_result = register_in_childobjects(config_xml, "Configuration", "XDTOPackage", name)
|
||||
|
||||
type_count = sum(1 for c in pkg_node.children if c.tag in ("objectType", "valueType"))
|
||||
print(f"✓ Пакет XDTO собран: {name}")
|
||||
@@ -1147,5 +1324,7 @@ if reg_result == "added":
|
||||
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> добавлен в ChildObjects")
|
||||
elif reg_result == "already":
|
||||
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> уже зарегистрирован")
|
||||
elif reg_result == "no-childobj":
|
||||
print("ПРЕДУПРЕЖДЕНИЕ: Configuration.xml найден, но <ChildObjects> не найден", file=sys.stderr)
|
||||
else:
|
||||
print(" Configuration.xml не найден — регистрация пропущена")
|
||||
|
||||
@@ -27,6 +27,9 @@ __pycache__/
|
||||
|
||||
# Локальный реестр баз данных 1С
|
||||
.v8-project.json
|
||||
# ...но в кейсах он — вход теста (настройка newObjectPosition), и эталон без него
|
||||
# на чистом клоне не сойдётся с результатом прогона.
|
||||
!tests/skills/cases/**/.v8-project.json
|
||||
|
||||
# web-test: Node.js зависимости и runtime-артефакты
|
||||
.claude/skills/web-test/scripts/node_modules/
|
||||
|
||||
@@ -71,7 +71,7 @@ python tools/cc-1c-skills/scripts/switch.py
|
||||
| Подсистемы (Subsystem) | 4 навыка `/subsystem-*` | Анализ, создание, редактирование, валидация подсистем конфигурации | [Подробнее](docs/subsystem-guide.md) |
|
||||
| Командный интерфейс (CI) | 2 навыка `/interface-*` | Редактирование и валидация CommandInterface.xml подсистем | [Подробнее](docs/subsystem-guide.md) |
|
||||
| Пакеты XDTO | 5 навыков `/xdto-*` | Анализ, создание из XML-схемы, выгрузка в схему, точечное редактирование, валидация пакетов XDTO | [Подробнее](docs/xdto-guide.md) |
|
||||
| Базы данных (DB) | 9 навыков `/db-*` | Создание баз, загрузка/выгрузка конфигураций, обновление БД, загрузка из Git | [Подробнее](docs/db-guide.md) |
|
||||
| Базы данных (DB) | 13 навыков `/db-*` | Создание баз, загрузка/выгрузка конфигураций, обновление БД, загрузка из Git, хранилище конфигурации, управление расширениями в базе | [Подробнее](docs/db-guide.md) |
|
||||
| Веб-публикация (Web) | 4 навыка `/web-*` | Публикация баз через Apache, статус, остановка, удаление публикаций | [Подробнее](docs/web-guide.md) |
|
||||
| Тестирование (Web) | `/web-test` | Взаимодействие с веб-клиентом 1С — навигация, формы, таблицы, отчёты, тестирование | [Подробнее](docs/web-test-guide.md) |
|
||||
| Запись видео (Web) | `/web-test` | Запись видеоинструкций с субтитрами, подсветкой и TTS-озвучкой | [Подробнее](docs/web-test-recording-guide.md) |
|
||||
@@ -233,9 +233,13 @@ python scripts/switch.py --runtime powershell # вернуть на PowerShell
|
||||
├── db-load-cf/ # Загрузка конфигурации из CF
|
||||
├── db-dump-xml/ # Выгрузка конфигурации в XML
|
||||
├── db-load-xml/ # Загрузка конфигурации из XML
|
||||
├── db-dump-dt/ # Выгрузка информационной базы в DT
|
||||
├── db-load-dt/ # Загрузка информационной базы из DT
|
||||
├── db-update/ # Обновление конфигурации БД
|
||||
├── db-run/ # Запуск 1С:Предприятие
|
||||
├── db-load-git/ # Загрузка изменений из Git
|
||||
├── db-repo/ # Хранилище конфигурации
|
||||
├── db-cfe-admin/ # Расширения в базе: состав, проверки, свойства, удаление
|
||||
├── web-publish/ # Публикация базы через Apache
|
||||
├── web-info/ # Статус Apache и публикаций
|
||||
├── web-stop/ # Остановка Apache
|
||||
|
||||
@@ -1696,7 +1696,181 @@ XML-элемент: `<WebService>`. Трёхуровневая вложенно
|
||||
|
||||
---
|
||||
|
||||
## 26. Различия версий платформы
|
||||
## 26. Внешние источники данных (ExternalDataSources)
|
||||
|
||||
XML-элемент: `<ExternalDataSource>`. Двухуровневая вложенность: источник → таблица → поле,
|
||||
но **уровней каталогов только два**:
|
||||
|
||||
```
|
||||
ExternalDataSources/PG.xml источник
|
||||
ExternalDataSources/PG/Tables/products.xml таблица (поля — внутри этого файла)
|
||||
ExternalDataSources/PG/Tables/products/Ext/ManagerModule.bsl
|
||||
ExternalDataSources/PG/Tables/products/Forms/ФормаСписка.xml (+ Forms/<Форма>/Ext/Form.xml)
|
||||
```
|
||||
|
||||
Модули таблицы — `ObjectModule.bsl`, `RecordSetModule.bsl`, `ManagerModule.bsl`; в выгрузку попадают
|
||||
только непустые. Формы — как у справочника.
|
||||
|
||||
**Параметров соединения в метаданных нет.** Строка соединения, пользователь, пароль и тип СУБД
|
||||
задаются в режиме «Предприятие» (стандартная функция «Управление внешними источниками данных») и
|
||||
хранятся в информационной базе. В конфигурации у источника всего четыре свойства.
|
||||
|
||||
### 26.1. Свойства источника
|
||||
|
||||
```xml
|
||||
<Properties>
|
||||
<Name>PG</Name>
|
||||
<Synonym>...</Synonym>
|
||||
<Comment/>
|
||||
<DataLockControlMode>AutomaticAndManaged</DataLockControlMode>
|
||||
</Properties>
|
||||
```
|
||||
|
||||
`DataLockControlMode`: `Automatic` | `Managed` | `AutomaticAndManaged`. При конкретном значении
|
||||
одноимённое свойство таблицы игнорируется; `AutomaticAndManaged` отдаёт решение таблицам.
|
||||
|
||||
### 26.2. ChildObjects источника: Table (именем) + Function (целиком)
|
||||
|
||||
Смешанный список: таблицы перечислены **именами** (их содержимое — в отдельных файлах), функции
|
||||
лежат **полными узлами** прямо здесь.
|
||||
|
||||
```xml
|
||||
<ChildObjects>
|
||||
<Table>products</Table>
|
||||
<Table>prices</Table>
|
||||
<Function uuid="...">
|
||||
<Properties>
|
||||
<Name>total</Name>
|
||||
<Synonym/>
|
||||
<Comment/>
|
||||
<ReturnValue>true</ReturnValue>
|
||||
<Type>
|
||||
<v8:Type>xs:decimal</v8:Type>
|
||||
<v8:NumberQualifiers>...</v8:NumberQualifiers>
|
||||
</Type>
|
||||
<ExpressionInDataSource>public.f_total(&1, &2)</ExpressionInDataSource>
|
||||
</Properties>
|
||||
</Function>
|
||||
</ChildObjects>
|
||||
```
|
||||
|
||||
Свойств у функции шесть. **Параметров как объектов метаданных не существует**: формальные параметры
|
||||
записываются прямо в выражении как `&1`, `&2`; необязательные — в фигурных скобках (`f(&1{, &2})`),
|
||||
переменное число — `&n[]` (последним в списке). `ReturnValue=false` описывает процедуру, тогда
|
||||
`<Type/>` пуст.
|
||||
|
||||
Куб (`<Cube>`) — четвёртый вид дочернего объекта источника, здесь не описан.
|
||||
|
||||
### 26.3. Свойства таблицы
|
||||
|
||||
38 свойств в фиксированном порядке:
|
||||
|
||||
`Name`, `Synonym`, `Comment`, `TableType`, `NameInDataSource`, `ExpressionInDataSource`,
|
||||
`TableDataType`, `KeyFields`, `PresentationField`, `ParentField`, `UnfilledParentValue`,
|
||||
`Characteristics`, `UseStandardCommands`, `QuickChoice`, `InputByString`, `CreateOnInput`,
|
||||
`SearchStringModeOnInputByString`, `ChoiceDataGetModeOnInputByString`, `ChoiceHistoryOnInput`,
|
||||
`DefaultObjectForm`, `DefaultRecordForm`, `DefaultListForm`, `DefaultChoiceForm`,
|
||||
`ObjectPresentation`, `ExtendedObjectPresentation`, `RecordPresentation`,
|
||||
`ExtendedRecordPresentation`, `ListPresentation`, `ExtendedListPresentation`, `Explanation`,
|
||||
`IncludeHelpInContents`, `ReadOnly`, `TransactionsIsolationLevel`, `DataVersionField`, `EditType`,
|
||||
`BasedOn`, `DataLockFields`, `DataLockControlMode`.
|
||||
|
||||
Специфичные для внешнего источника:
|
||||
|
||||
| Свойство | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `TableType` | enum | `Table` — реальная таблица или представление; `Expression` — табличная функция/выражение |
|
||||
| `NameInDataSource` | string | Имя физической таблицы. Три части: `<база>.<схема>.<таблица>`. У `TableType=Expression` — **пусто** |
|
||||
| `ExpressionInDataSource` | string | Выражение для `TableType=Expression`, напр. `public.f_by_parent(&1)`. Здесь имя базы **не указывается** |
|
||||
| `TableDataType` | enum | `ObjectData` — запись идентифицируется одним полем; `NonobjectData` — составным ключом |
|
||||
| `KeyFields` | список | Ключевые поля, `<xr:Field>` с полным путём |
|
||||
| `PresentationField` | скаляр | Поле представления (только `ObjectData`) |
|
||||
| `ParentField` | скаляр | Поле родителя для иерархии; тип поля должен быть ссылкой на эту же таблицу |
|
||||
| `UnfilledParentValue` | значение | Значение «нет родителя», типизировано (`xsi:type`) |
|
||||
| `ReadOnly` | boolean | Запрет записи. Платформа сама ставит `true` представлениям и таблицам вида `Expression` |
|
||||
| `TransactionsIsolationLevel` | enum | `Auto` \| `ReadUncommitted` \| `ReadCommitted` \| `RepeatableRead` \| `Serializable` |
|
||||
| `DataVersionField` | скаляр | Поле, растущее при каждой записи, — по нему ловится конкурентное изменение |
|
||||
| `DataLockFields` | список | Поля управляемой блокировки |
|
||||
|
||||
`BasedOn`, `Characteristics`, `EditType`, представления и формы — как у справочника.
|
||||
|
||||
**Признака незаполненного родителя отдельным узлом нет.** «NULL» против «Заданное значение»
|
||||
различаются формой самого `UnfilledParentValue`:
|
||||
|
||||
```xml
|
||||
<UnfilledParentValue xsi:nil="true"/> <!-- NULL -->
|
||||
<UnfilledParentValue xsi:type="xs:decimal">0</UnfilledParentValue> <!-- Заданное значение -->
|
||||
```
|
||||
|
||||
Тип значения — тип **ключевого** поля таблицы, а не ссылочный тип поля родителя.
|
||||
|
||||
### 26.4. Скаляры против списков
|
||||
|
||||
Путь внутри одинаковый, оборачивание разное:
|
||||
|
||||
```xml
|
||||
<PresentationField>ExternalDataSource.PG.Table.products.Field.name</PresentationField>
|
||||
|
||||
<KeyFields>
|
||||
<xr:Field>ExternalDataSource.PG.Table.products.Field.id</xr:Field>
|
||||
</KeyFields>
|
||||
```
|
||||
|
||||
Скаляры: `PresentationField`, `ParentField`, `DataVersionField`.
|
||||
Списки `<xr:Field>`: `KeyFields`, `InputByString`, `DataLockFields`.
|
||||
|
||||
### 26.5. Поле таблицы
|
||||
|
||||
26 свойств: канонический блок реквизита (см. §6.1) без `ChoiceFoldersAndItems`, `LinkByType`,
|
||||
`Indexing`, `Use`, `FullTextSearch`, `DataHistory`, плюс три своих в конце:
|
||||
|
||||
| Свойство | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `NameInDataSource` | string | Имя колонки. В одинарных кавычках попадает в SQL как есть, без них — экранируется двойными при спецсимволах |
|
||||
| `ReadOnly` | boolean | Поле не записывается (вычисляемые, автоинкрементные) |
|
||||
| `AllowNull` | boolean | Допускает `NULL`; в форме показывается как «Не заполнено» |
|
||||
|
||||
Типы поля: `Число`, `Строка`, `Дата`, `Булево`, `УникальныйИдентификатор`, `ДвоичныеДанные` и
|
||||
ссылка на таблицу внешнего источника:
|
||||
|
||||
```xml
|
||||
<Type><v8:Type>cfg:ExternalDataSourceTableRef.PG.products</v8:Type></Type>
|
||||
```
|
||||
|
||||
Префикс корневой `cfg:` (объявлен в шапке `MetaDataObject`), квалификаторов у ссылки нет.
|
||||
|
||||
**Составной тип платформой запрещён.** Документация «Руководства разработчика» (17.5.2.3.4)
|
||||
разрешает составной тип из Числа/Строки/Даты/Булева, но 8.3.24 отвергает загрузку:
|
||||
«Поле не может иметь составной тип».
|
||||
|
||||
### 26.6. Расхождения Конфигуратора и загрузки XML
|
||||
|
||||
Интерактивный редактор строже загрузчика. Измерено на 8.3.24.1691:
|
||||
|
||||
| Ситуация | Конфигуратор | Загрузка XML |
|
||||
|---|---|---|
|
||||
| Таблица без `KeyFields` | ошибка «Не указано ни одного поля ключа» | принимает молча, обновление БД проходит |
|
||||
| Составной тип поля | галочки «Составной тип» нет | явная ошибка при загрузке |
|
||||
|
||||
Первое означает, что валидатор не должен считать пустой ключ ошибкой: рабочие конфигурации с
|
||||
таблицами без ключа существуют.
|
||||
|
||||
### 26.7. Что платформа заполняет сама
|
||||
|
||||
Конструктор загрузки структуры из СУБД:
|
||||
|
||||
- `TableDataType` — `ObjectData` при первичном ключе из одного поля, иначе `NonobjectData`;
|
||||
- `KeyFields` — из первичного ключа СУБД (у представления его нет → пусто);
|
||||
- `ReadOnly` таблицы — `true` для представлений и таблиц вида `Expression`;
|
||||
- `AllowNull` поля — из `NOT NULL` колонки;
|
||||
- ссылочный тип поля — из внешнего ключа СУБД;
|
||||
- имя объекта — `<база>_<схема>_<объект>` (точки заменяются подчёркиваниями).
|
||||
|
||||
Кроме того, при задании `PresentationField` платформа добавляет это поле в `InputByString`.
|
||||
|
||||
---
|
||||
|
||||
## 27. Различия версий платформы
|
||||
|
||||
### 26.1. Лестница версий
|
||||
|
||||
@@ -1750,7 +1924,7 @@ XML-элемент: `<WebService>`. Трёхуровневая вложенно
|
||||
|
||||
---
|
||||
|
||||
## 27. Сводная таблица: свойства по типам объектов
|
||||
## 28. Сводная таблица: свойства по типам объектов
|
||||
|
||||
| Свойство | Cat | Doc | Enum | Const | InfoReg | AccReg | AcctReg | CalcReg | CoA | CoCT | CoCaT | BP | Task | EP | DJ | Rep | DP |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
@@ -1782,9 +1956,11 @@ XML-элемент: `<WebService>`. Трёхуровневая вложенно
|
||||
|
||||
Сокращения: Cat=Справочник, Doc=Документ, Const=Константа, InfoReg=РегСведений, AccReg=РегНакопления, AcctReg=РегБухгалтерии, CalcReg=РегРасчёта, CoA=ПланСчетов, CoCT=ПВХ, CoCaT=ПВР, BP=БизнесПроцесс, EP=ПланОбмена, DJ=ЖурналДокументов, Rep=Отчёт, DP=Обработка.
|
||||
|
||||
Внешний источник данных в матрицу не вошёл: набор его свойств не пересекается с колонками (ни кода с наименованием, ни реквизитов, ни табличных частей — вместо них таблицы и поля). См. § 26.
|
||||
|
||||
---
|
||||
|
||||
## 28. Формат ссылок на объекты метаданных
|
||||
## 29. Формат ссылок на объекты метаданных
|
||||
|
||||
В свойствах типа `DefaultObjectForm`, `InputByString`, `RegisterRecords`, `DataLockFields` и др. используется формат ссылок:
|
||||
|
||||
@@ -1800,13 +1976,18 @@ XML-элемент: `<WebService>`. Трёхуровневая вложенно
|
||||
{ВидОбъекта}.{ИмяОбъекта}.Resource.{ИмяРесурса} # На ресурс регистра
|
||||
{ВидОбъекта}.{ИмяОбъекта}.AddressingAttribute.{Имя} # На реквизит адресации
|
||||
{ВидОбъекта}.{ИмяОбъекта}.EnumValue.{ИмяЗначения} # На значение перечисления
|
||||
ExternalDataSource.{Источник}.Table.{Таблица} # На таблицу внешнего источника
|
||||
ExternalDataSource.{Источник}.Table.{Таблица}.Field.{Поле} # На поле таблицы
|
||||
ExternalDataSource.{Источник}.Table.{Таблица}.Form.{Форма} # На форму таблицы
|
||||
```
|
||||
|
||||
Виды объектов в ссылках: `Catalog`, `Document`, `Enum`, `InformationRegister`, `AccumulationRegister`, `AccountingRegister`, `CalculationRegister`, `ChartOfAccounts`, `ChartOfCharacteristicTypes`, `ChartOfCalculationTypes`, `BusinessProcess`, `Task`, `ExchangePlan`, `DocumentJournal`, `Report`, `DataProcessor`, `CommonForm`, `CommonPicture`, `SessionParameter`, `Constant`.
|
||||
Виды объектов в ссылках: `Catalog`, `Document`, `Enum`, `InformationRegister`, `AccumulationRegister`, `AccountingRegister`, `CalculationRegister`, `ChartOfAccounts`, `ChartOfCharacteristicTypes`, `ChartOfCalculationTypes`, `BusinessProcess`, `Task`, `ExchangePlan`, `DocumentJournal`, `Report`, `DataProcessor`, `CommonForm`, `CommonPicture`, `SessionParameter`, `Constant`, `ExternalDataSource`.
|
||||
|
||||
Ссылки на внешний источник — единственные **шестичастные**: между видом и подчинённой сущностью стоит имя источника. Разбор с конца (как в ролях) на них не работает — считать надо с начала.
|
||||
|
||||
---
|
||||
|
||||
## 29. GeneratedType категории
|
||||
## 30. GeneratedType категории
|
||||
|
||||
Каждый объект метаданных содержит блок `<InternalInfo>` с элементами `<GeneratedType>`, описывающими платформенные типы. Ниже — эталонная таблица категорий по типам объектов (источник: выгрузки ACC 8.3.24, ERP 8.3.24).
|
||||
|
||||
@@ -1830,14 +2011,18 @@ XML-элемент: `<WebService>`. Трёхуровневая вложенно
|
||||
| Report | Object, Manager |
|
||||
| DataProcessor | Object, Manager |
|
||||
| DefinedType | DefinedType |
|
||||
| ExternalDataSource | Manager, TablesManager, CubesManager |
|
||||
| ExternalDataSource → Table | Manager, Object, Ref, List, Record, RecordSet, RecordKey, RecordManager |
|
||||
|
||||
Формат `name` в XML: `{Prefix}.{ObjectName}`, где Prefix = `{MetaType}{Category}` (например `CatalogObject.Номенклатура`, `AccountingRegisterExtDimensions.Хозрасчетный`).
|
||||
|
||||
Исключение — таблица внешнего источника: имя **трёхчастное**, `{Prefix}.{Источник}.{Таблица}` (`ExternalDataSourceTableRef.PG.products`), а префикс строится от `ExternalDataSourceTable`, а не от имени вида. У `<Field>` и `<Function>` блока `InternalInfo` нет вовсе — только атрибут `uuid`.
|
||||
|
||||
Примечание: TabularSection/TabularSectionRow генерируются динамически для каждой табличной части. ChartOfAccounts может иметь условные ExtDimensionTypes/ExtDimensionTypesRow (зависит от наличия `extDimensionTypes`).
|
||||
|
||||
---
|
||||
|
||||
## 30. Кодировка
|
||||
## 31. Кодировка
|
||||
|
||||
Все XML-файлы используют кодировку UTF-8 с BOM (байты `EF BB BF`):
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ ClassId — фиксированные идентификаторы классо
|
||||
<!-- CalculationRegister (только ERP и аналогичные) -->
|
||||
<BusinessProcess>Задание</BusinessProcess>
|
||||
<Task>ЗадачаИсполнителя</Task>
|
||||
<ExternalDataSource>ВнешниеДанные</ExternalDataSource>
|
||||
<IntegrationService>ОбменСообщениями</IntegrationService>
|
||||
</ChildObjects>
|
||||
```
|
||||
@@ -245,41 +246,43 @@ ClassId — фиксированные идентификаторы классо
|
||||
| 8 | `CommonTemplate` | `CommonTemplates/` | Общие макеты |
|
||||
| 9 | `FilterCriterion` | `FilterCriteria/` | Критерии отбора |
|
||||
| 10 | `CommonModule` | `CommonModules/` | Общие модули |
|
||||
| 11 | `Bot` | `Bots/` | Боты (платформа 8.3.18+) |
|
||||
| 12 | `CommonAttribute` | `CommonAttributes/` | Общие реквизиты |
|
||||
| 13 | `ExchangePlan` | `ExchangePlans/` | Планы обмена |
|
||||
| 14 | `XDTOPackage` | `XDTOPackages/` | XDTO-пакеты |
|
||||
| 15 | `WebService` | `WebServices/` | Веб-сервисы |
|
||||
| 16 | `HTTPService` | `HTTPServices/` | HTTP-сервисы |
|
||||
| 17 | `WSReference` | `WSReferences/` | WS-ссылки |
|
||||
| 18 | `EventSubscription` | `EventSubscriptions/` | Подписки на события |
|
||||
| 19 | `ScheduledJob` | `ScheduledJobs/` | Регламентные задания |
|
||||
| 20 | `SettingsStorage` | `SettingsStorages/` | Хранилища настроек |
|
||||
| 21 | `FunctionalOption` | `FunctionalOptions/` | Функциональные опции |
|
||||
| 22 | `FunctionalOptionsParameter` | `FunctionalOptionsParameters/` | Параметры ФО |
|
||||
| 23 | `DefinedType` | `DefinedTypes/` | Определяемые типы |
|
||||
| 24 | `CommonCommand` | `CommonCommands/` | Общие команды |
|
||||
| 25 | `CommandGroup` | `CommandGroups/` | Группы команд |
|
||||
| 26 | `Constant` | `Constants/` | Константы |
|
||||
| 27 | `CommonForm` | `CommonForms/` | Общие формы |
|
||||
| 28 | `Catalog` | `Catalogs/` | Справочники |
|
||||
| 29 | `Document` | `Documents/` | Документы |
|
||||
| 30 | `DocumentNumerator` | `DocumentNumerators/` | Нумераторы документов |
|
||||
| 31 | `Sequence` | `Sequences/` | Последовательности |
|
||||
| 32 | `DocumentJournal` | `DocumentJournals/` | Журналы документов |
|
||||
| 33 | `Enum` | `Enums/` | Перечисления |
|
||||
| 34 | `Report` | `Reports/` | Отчёты |
|
||||
| 35 | `DataProcessor` | `DataProcessors/` | Обработки |
|
||||
| 36 | `InformationRegister` | `InformationRegisters/` | Регистры сведений |
|
||||
| 37 | `AccumulationRegister` | `AccumulationRegisters/` | Регистры накопления |
|
||||
| 38 | `ChartOfCharacteristicTypes` | `ChartsOfCharacteristicTypes/` | Планы видов характеристик |
|
||||
| 39 | `ChartOfAccounts` | `ChartsOfAccounts/` | Планы счетов |
|
||||
| 40 | `AccountingRegister` | `AccountingRegisters/` | Регистры бухгалтерии |
|
||||
| 41 | `ChartOfCalculationTypes` | `ChartsOfCalculationTypes/` | Планы видов расчёта |
|
||||
| 42 | `CalculationRegister` | `CalculationRegisters/` | Регистры расчёта |
|
||||
| 43 | `BusinessProcess` | `BusinessProcesses/` | Бизнес-процессы |
|
||||
| 44 | `Task` | `Tasks/` | Задачи |
|
||||
| 45 | `IntegrationService` | `IntegrationServices/` | Сервисы интеграции |
|
||||
| 11 | `CommonAttribute` | `CommonAttributes/` | Общие реквизиты |
|
||||
| 12 | `ExchangePlan` | `ExchangePlans/` | Планы обмена |
|
||||
| 13 | `XDTOPackage` | `XDTOPackages/` | XDTO-пакеты |
|
||||
| 14 | `WebService` | `WebServices/` | Веб-сервисы |
|
||||
| 15 | `HTTPService` | `HTTPServices/` | HTTP-сервисы |
|
||||
| 16 | `WSReference` | `WSReferences/` | WS-ссылки |
|
||||
| 17 | `EventSubscription` | `EventSubscriptions/` | Подписки на события |
|
||||
| 18 | `ScheduledJob` | `ScheduledJobs/` | Регламентные задания |
|
||||
| 19 | `SettingsStorage` | `SettingsStorages/` | Хранилища настроек |
|
||||
| 20 | `FunctionalOption` | `FunctionalOptions/` | Функциональные опции |
|
||||
| 21 | `FunctionalOptionsParameter` | `FunctionalOptionsParameters/` | Параметры ФО |
|
||||
| 22 | `DefinedType` | `DefinedTypes/` | Определяемые типы |
|
||||
| 23 | `Bot` | `Bots/` | Боты (платформа 8.3.18+) |
|
||||
| 24 | `PaletteColor` | `PaletteColors/` | Цвета палитры (платформа 8.5+) |
|
||||
| 25 | `CommonCommand` | `CommonCommands/` | Общие команды |
|
||||
| 26 | `CommandGroup` | `CommandGroups/` | Группы команд |
|
||||
| 27 | `Constant` | `Constants/` | Константы |
|
||||
| 28 | `CommonForm` | `CommonForms/` | Общие формы |
|
||||
| 29 | `Catalog` | `Catalogs/` | Справочники |
|
||||
| 30 | `Document` | `Documents/` | Документы |
|
||||
| 31 | `DocumentNumerator` | `DocumentNumerators/` | Нумераторы документов |
|
||||
| 32 | `Sequence` | `Sequences/` | Последовательности |
|
||||
| 33 | `DocumentJournal` | `DocumentJournals/` | Журналы документов |
|
||||
| 34 | `Enum` | `Enums/` | Перечисления |
|
||||
| 35 | `Report` | `Reports/` | Отчёты |
|
||||
| 36 | `DataProcessor` | `DataProcessors/` | Обработки |
|
||||
| 37 | `InformationRegister` | `InformationRegisters/` | Регистры сведений |
|
||||
| 38 | `AccumulationRegister` | `AccumulationRegisters/` | Регистры накопления |
|
||||
| 39 | `ChartOfCharacteristicTypes` | `ChartsOfCharacteristicTypes/` | Планы видов характеристик |
|
||||
| 40 | `ChartOfAccounts` | `ChartsOfAccounts/` | Планы счетов |
|
||||
| 41 | `AccountingRegister` | `AccountingRegisters/` | Регистры бухгалтерии |
|
||||
| 42 | `ChartOfCalculationTypes` | `ChartsOfCalculationTypes/` | Планы видов расчёта |
|
||||
| 43 | `CalculationRegister` | `CalculationRegisters/` | Регистры расчёта |
|
||||
| 44 | `BusinessProcess` | `BusinessProcesses/` | Бизнес-процессы |
|
||||
| 45 | `Task` | `Tasks/` | Задачи |
|
||||
| 46 | `ExternalDataSource` | `ExternalDataSources/` | Внешние источники данных |
|
||||
| 47 | `IntegrationService` | `IntegrationServices/` | Сервисы интеграции |
|
||||
|
||||
Внутри одного типа объекты отсортированы по имени (алфавитный порядок). Типы, для которых нет объектов, в ChildObjects не записываются.
|
||||
|
||||
@@ -314,6 +317,14 @@ ClassId — фиксированные идентификаторы классо
|
||||
| `IntegrationService` | `IntegrationServiceManager`/Manager |
|
||||
| `WSReference` | `WSReferenceManager`/Manager |
|
||||
| `DefinedType` | `DefinedType`/DefinedType |
|
||||
| `ExternalDataSource` | `ExternalDataSourceManager`/Manager, `ExternalDataSourceTablesManager`/TablesManager, `ExternalDataSourceCubesManager`/CubesManager |
|
||||
| `Table` | `ExternalDataSourceTableManager`/Manager, `ExternalDataSourceTableObject`/Object, `ExternalDataSourceTableRef`/Ref, `ExternalDataSourceTableList`/List, `ExternalDataSourceTableRecord`/Record, `ExternalDataSourceTableRecordSet`/RecordSet, `ExternalDataSourceTableRecordKey`/RecordKey, `ExternalDataSourceTableRecordManager`/RecordManager |
|
||||
|
||||
`Table` — корневой элемент файла таблицы внешнего источника (`ExternalDataSources/<Источник>/Tables/<Имя>.xml`),
|
||||
единственный подчинённый объект со своим `<InternalInfo>` в отдельном файле. Имя элемента у него
|
||||
**трёхчастное**: `<префикс>.<ИмяИсточника>.<ИмяТаблицы>` (например
|
||||
`ExternalDataSourceTableRef.PG.eds_public_products`), поэтому навыки-эмиттеры строят его инлайном,
|
||||
а не из карты «вид → набор». У `<Field>` и `<Function>` блока `InternalInfo` нет — только атрибут `uuid`.
|
||||
|
||||
Табличные части объектов несут собственную пару в своём `<InternalInfo>`: `<Тип>TabularSection.<Объект>.<ТЧ>`/TabularSection и `<Тип>TabularSectionRow.<Объект>.<ТЧ>`/TabularSectionRow.
|
||||
|
||||
@@ -1055,7 +1066,10 @@ ChildObjects содержат `IntegrationServiceChannel` (каналы) — inl
|
||||
|
||||
### 7.4. Configuration.xml — ChildObjects
|
||||
|
||||
Набор типов объектов в ChildObjects **идентичен** между версиями.
|
||||
Порядок типов между версиями не меняется, а вот набор — растёт: `Bot` доступен с платформы 8.3.18,
|
||||
`PaletteColor` — с 8.5. Версия появления указана в колонке «Описание» таблицы порядка типов.
|
||||
Конфигурация на более старой платформе просто не содержит таких групп; позиция типа, когда он есть,
|
||||
одна и та же (замерено: 8.3.27 и 8.5.1 ставят `Bot` сразу за `DefinedType`).
|
||||
|
||||
### 7.5. ConfigDumpInfo.xml и Ext/ файлы
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ CommonForms регистрируются в `Configuration.xml`:
|
||||
| BusinessProcess | DefaultObjectForm, DefaultListForm, DefaultChoiceForm |
|
||||
| Task | DefaultObjectForm, DefaultListForm, DefaultChoiceForm |
|
||||
| Constant | DefaultForm — но собственных форм у константы нет, свойство указывает на общую форму |
|
||||
| Table | DefaultObjectForm, DefaultRecordForm, DefaultListForm, DefaultChoiceForm |
|
||||
| CommonForm | — (регистрируется в Configuration.xml, нет DefaultForm) |
|
||||
|
||||
> Report.DefaultForm может указывать на общую форму: `CommonForm.ФормаОтчета`.
|
||||
@@ -171,13 +172,13 @@ CommonForms регистрируются в `Configuration.xml`:
|
||||
|
||||
| Назначение | Свойство объекта | Главный реквизит | Для каких типов |
|
||||
|------------|------------------|------------------|-----------------|
|
||||
| Форма объекта | DefaultObjectForm | `{Тип}Object.{Имя}` | Catalog, Document, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, ExchangePlan, BusinessProcess, Task |
|
||||
| Форма объекта | DefaultObjectForm | `{Тип}Object.{Имя}` | Catalog, Document, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, ExchangePlan, BusinessProcess, Task, Table |
|
||||
| Форма обработки/отчёта | DefaultForm | `{Тип}Object.{Имя}` | DataProcessor, Report, ExternalDataProcessor, ExternalReport |
|
||||
| Форма группы | DefaultFolderForm | `{Тип}Object.{Имя}` | Catalog, ChartOfCharacteristicTypes |
|
||||
| Форма списка | DefaultListForm (у DocumentJournal и FilterCriterion — DefaultForm) | `DynamicList` + `MainTable` | все типы со списком |
|
||||
| Форма выбора | DefaultChoiceForm | `DynamicList` + `MainTable` | Catalog, Document, ChartOf*, ExchangePlan, BusinessProcess, Task, Enum |
|
||||
| Форма выбора группы | DefaultFolderChoiceForm | `DynamicList` + `MainTable` | Catalog, ChartOfCharacteristicTypes |
|
||||
| Форма записи | DefaultRecordForm | `InformationRegisterRecordManager.{Имя}` | InformationRegister |
|
||||
| Форма записи | DefaultRecordForm | `InformationRegisterRecordManager.{Имя}` или `ExternalDataSourceTableRecordManager.{Источник}.{Имя}` | InformationRegister, Table |
|
||||
| Форма набора записей | — (свойства нет) | `{Тип}RecordSet.{Имя}` | все регистры |
|
||||
| Форма сохранения/загрузки настроек | DefaultSaveForm / DefaultLoadForm | нет | SettingsStorage |
|
||||
| Произвольная форма | — (свойства нет) | нет | любой тип с собственными формами |
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
| `WebService` | `WebServices/` | Веб-сервисы | [1c-config-objects-spec.md § 25](1c-config-objects-spec.md#25-веб-сервисы-webservices) |
|
||||
| `HTTPService` | `HTTPServices/` | HTTP-сервисы | [1c-config-objects-spec.md § 24](1c-config-objects-spec.md#24-http-сервисы-httpservices) |
|
||||
| `WSReference` | `WSReferences/` | WS-ссылки | [1c-configuration-spec.md § 6.15](1c-configuration-spec.md#615-wsreference--ws-ссылка) |
|
||||
| `ExternalDataSource` | `ExternalDataSources/` | Внешние источники данных | [1c-config-objects-spec.md § 26](1c-config-objects-spec.md#26-внешние-источники-данных-externaldatasources) |
|
||||
| `IntegrationService` | `IntegrationServices/` | Сервисы интеграции | [1c-configuration-spec.md § 6.13](1c-configuration-spec.md#613-integrationservice--сервис-интеграции) |
|
||||
|
||||
### Поведение и параметризация
|
||||
@@ -157,8 +158,8 @@
|
||||
| Общие свойства Properties | [1c-config-objects-spec.md § 4](1c-config-objects-spec.md#4-общие-элементы-properties) |
|
||||
| Стандартные реквизиты | [1c-config-objects-spec.md § 5](1c-config-objects-spec.md#5-стандартные-реквизиты-standardattributes) |
|
||||
| Дочерние объекты (Attribute, TabularSection, Form, Template, Command) | [1c-config-objects-spec.md § 6](1c-config-objects-spec.md#6-дочерние-объекты-childobjects) |
|
||||
| Формат ссылок на объекты | [1c-config-objects-spec.md § 28](1c-config-objects-spec.md#28-формат-ссылок-на-объекты-метаданных) |
|
||||
| Различия версий формата 2.17 → 2.18 → 2.19 → 2.20 | [1c-config-objects-spec.md § 26](1c-config-objects-spec.md#26-различия-версий-платформы) |
|
||||
| Формат ссылок на объекты | [1c-config-objects-spec.md § 29](1c-config-objects-spec.md#29-формат-ссылок-на-объекты-метаданных) |
|
||||
| Различия версий формата 2.17 → 2.18 → 2.19 → 2.20 | [1c-config-objects-spec.md § 27](1c-config-objects-spec.md#27-различия-версий-платформы) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
| `/db-run` | — | Запуск 1С:Предприятие |
|
||||
| `/db-load-git` | `.ps1` | Загрузка изменений из Git в базу |
|
||||
| `/db-repo` | `.ps1` | Хранилище конфигурации: захват, помещение, получение изменений |
|
||||
| `/db-cfe-admin` | `.ps1` | Расширения в базе: список и состояние, проверка применимости и модулей, свойства подключения, удаление |
|
||||
|
||||
## Рабочий цикл
|
||||
|
||||
|
||||
@@ -1958,3 +1958,200 @@ ChildObjects и модулей.
|
||||
| `direction` | `In` | Направление: `In` / `Out` / `InOut` |
|
||||
|
||||
Если значение — строка, интерпретируется как `type`.
|
||||
|
||||
---
|
||||
|
||||
## 18. Внешние источники данных (ExternalDataSource)
|
||||
|
||||
Источник описывается одним JSON целиком: сам источник, его таблицы с полями и функции. Компилятор
|
||||
пишет `ExternalDataSources/<Имя>.xml` плюс по файлу на таблицу в `<Имя>/Tables/` — единственный вид,
|
||||
у которого объект складывается более чем из одного XML.
|
||||
|
||||
Параметров соединения в конфигурации нет: строка соединения, пользователь, пароль и тип СУБД
|
||||
задаются в режиме «Предприятие» (стандартная функция «Управление внешними источниками данных»)
|
||||
и хранятся в информационной базе.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "ExternalDataSource",
|
||||
"name": "PG",
|
||||
"dataLockControlMode": "AutomaticAndManaged",
|
||||
"tables": {
|
||||
"prices": ["product_id: Number(10,0)", "period: Date", "price: Number(15,2)"],
|
||||
"products": {
|
||||
"nameInDataSource": "eds.public.products",
|
||||
"tableDataType": "ObjectData",
|
||||
"keyFields": ["id"],
|
||||
"presentationField": "name",
|
||||
"parentField": "parent_id",
|
||||
"fields": [
|
||||
"id: Number(10,0)",
|
||||
"name: String(150)",
|
||||
"article: String(50) | nullable",
|
||||
"parent_id: ExternalDataSourceTableRef.PG.products | nullable"
|
||||
]
|
||||
}
|
||||
},
|
||||
"functions": {
|
||||
"total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" },
|
||||
"nextKey": "NEXT VALUE FOR public.seq_key"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 18.1 Свойства источника
|
||||
|
||||
| Ключ | Умолчание | Значения |
|
||||
|------|-----------|----------|
|
||||
| `synonym` / `comment` | авто из имени / пусто | как у прочих объектов (§2, §4.4) |
|
||||
| `dataLockControlMode` | `Automatic` | `Automatic` / `Managed` / `AutomaticAndManaged` |
|
||||
| `tables` | `{}` | таблицы (§18.2) |
|
||||
| `functions` | `{}` | функции (§18.4) |
|
||||
|
||||
`AutomaticAndManaged` — значение, которого нет у прочих объектов: оно отдаёт решение о режиме
|
||||
блокировок каждой таблице. При конкретном значении (`Automatic`/`Managed`) одноимённое свойство
|
||||
таблицы платформа игнорирует.
|
||||
|
||||
### 18.2 Таблицы
|
||||
|
||||
Ключ — имя таблицы в конфигурации. Значение — **массив полей** либо **объект** со свойствами и
|
||||
ключом `fields`: та же двойственность, что у `tabularSections` (§5).
|
||||
|
||||
| Ключ | XML | Умолчание | Значения |
|
||||
|------|-----|-----------|----------|
|
||||
| `synonym` / `comment` | Synonym / Comment | авто из имени / пусто | ML (§4.4) |
|
||||
| `tableType` | TableType | `Table` | `Table` — реальная таблица или представление; `Expression` — табличная функция/выражение |
|
||||
| `nameInDataSource` | NameInDataSource | = имя таблицы | имя физической таблицы; платформа пишет три части `<база>.<схема>.<таблица>`. У `Expression` — пусто |
|
||||
| `expressionInDataSource` | ExpressionInDataSource | пусто | выражение для `Expression`, напр. `public.f_by_parent(&1)`; имя базы в нём НЕ указывается |
|
||||
| `tableDataType` | TableDataType | `NonobjectData` | `ObjectData` — запись определяется одним полем (аналог справочника); `NonobjectData` — составным ключом (аналог регистра сведений) |
|
||||
| `keyFields` | KeyFields | `[]` | имена ключевых полей |
|
||||
| `presentationField` | PresentationField | пусто | имя поля представления (осмысленно только у `ObjectData`) |
|
||||
| `parentField` | ParentField | пусто | имя поля родителя; его тип обязан быть ссылкой на эту же таблицу |
|
||||
| `inputByString` | InputByString | = `[presentationField]` | имена полей ввода по строке |
|
||||
| `dataVersionField` | DataVersionField | пусто | имя поля, растущего при каждой записи (ловит конкурентное изменение) |
|
||||
| `dataLockFields` | DataLockFields | `[]` | имена полей управляемой блокировки |
|
||||
| `readOnly` | ReadOnly | `false` | запрет записи |
|
||||
| `transactionsIsolationLevel` | TransactionsIsolationLevel | `Auto` | `Auto` / `ReadUncommitted` / `ReadCommitted` / `RepeatableRead` / `Serializable` |
|
||||
| `dataLockControlMode` | DataLockControlMode | `Automatic` | `Automatic` / `Managed` / `AutomaticAndManaged` |
|
||||
| `basedOn` | BasedOn | `[]` | ввод на основании, `MDObjectRef` verbatim (`Catalog.Контрагенты`) |
|
||||
| `useStandardCommands` | UseStandardCommands | `true` | bool |
|
||||
| `quickChoice` | QuickChoice | `false` | bool |
|
||||
| `editType` | EditType | `InDialog` | `InDialog` / `InList` |
|
||||
| `fields` | ChildObjects/Field | `[]` | поля (§18.3); синоним ключа — `columns` |
|
||||
|
||||
Прочие свойства — представления (`objectPresentation`, `extendedObjectPresentation`,
|
||||
`recordPresentation`, `extendedRecordPresentation`, `listPresentation`, `extendedListPresentation`),
|
||||
формы по умолчанию (`defaultObjectForm`, `defaultRecordForm`, `defaultListForm`, `defaultChoiceForm`),
|
||||
`characteristics`, `explanation`, `includeHelpInContents`, `createOnInput`,
|
||||
`searchStringModeOnInputByString`, `choiceDataGetModeOnInputByString`, `choiceHistoryOnInput` —
|
||||
общий слой, как у справочника.
|
||||
|
||||
**Ссылки на поля** (`keyFields`, `presentationField`, `parentField`, `dataVersionField`,
|
||||
`inputByString`, `dataLockFields`) задаются короткими именами полей этой же таблицы — конвенция §7.1.5.
|
||||
Компилятор разворачивает их в полный путь `ExternalDataSource.<Источник>.Table.<Таблица>.Field.<Поле>`,
|
||||
декомпилятор сворачивает обратно. Это единственные **шестичастные** ссылки в метаданных, поэтому
|
||||
разбор с конца (как в ролях) на них не работает.
|
||||
|
||||
**`inputByString` — presence-aware.** Ключа нет → компилятор выводит список из `presentationField`
|
||||
(так делает платформа при загрузке). Явный список, в том числе пустой `[]`, уважается как есть.
|
||||
|
||||
**Значение незаполненного родителя в DSL отсутствует намеренно.** Платформа сбрасывает его в пустое
|
||||
при любой загрузке XML — включая загрузку собственной выгрузки (измерено на 8.3.24.1691 тремя
|
||||
независимыми прогонами: значением `0` от Конфигуратора, `5` из XML и `1`, выставленным в Конфигураторе
|
||||
осознанно). Задать его можно только интерактивно, поэтому компилятор эмитит форму, которая получается
|
||||
после загрузки: `xsi:type="xs:string"` пусто при заданном `parentField`, иначе `xsi:nil="true"`.
|
||||
|
||||
### 18.3 Поля таблицы
|
||||
|
||||
Строковая и объектная форма — те же, что у реквизитов (§4). Своих ключа три:
|
||||
|
||||
| Ключ | XML | Умолчание | Значения |
|
||||
|------|-----|-----------|----------|
|
||||
| `nameInDataSource` | NameInDataSource | = имя поля | имя колонки. Значение в одинарных кавычках уходит в SQL как есть; без кавычек экранируется двойными при спецсимволах |
|
||||
| `readOnly` | ReadOnly | `false` | поле не записывается (вычисляемые, автоинкрементные, ключевые) |
|
||||
| `allowNull` | AllowNull | `false` | допускает `NULL`; в форме показывается как «Не заполнено» |
|
||||
|
||||
Флаги строковой формы: `readonly`, `nullable`.
|
||||
|
||||
```json
|
||||
"fields": [
|
||||
"id: Number(10,0) | readonly",
|
||||
{ "name": "article", "type": "String(50)", "nameInDataSource": "art_code", "allowNull": true }
|
||||
]
|
||||
```
|
||||
|
||||
Допустимые типы поля: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` и ссылка на таблицу
|
||||
внешнего источника — `ExternalDataSourceTableRef.<Источник>.<Таблица>`.
|
||||
|
||||
Формы `BinaryData`:
|
||||
|
||||
| Запись | Length | AllowedLength |
|
||||
|---|---|---|
|
||||
| `BinaryData` | 4294967292 | Fixed |
|
||||
| `BinaryData(N)` | N | Variable |
|
||||
| `BinaryData(N,fixed)` | N | Fixed |
|
||||
| `BinaryData(N,variable)` | N | Variable |
|
||||
|
||||
Голая форма — то, что пишет платформа при импорте поля из СУБД. Декомпилятор
|
||||
сворачивает в неё только точное совпадение 4294967292/Fixed; любая другая длина возвращается
|
||||
со скобками. Посторонний текст в скобках (`BinaryData(abc)`) — ошибка компиляции.
|
||||
|
||||
`BinaryData` (ДвоичныеДанные) — **не** `ValueStorage`: это разные типы и разные узлы XML —
|
||||
`xs:base64Binary` и `v8:ValueStorage` соответственно. Квалификаторы их НЕ различают: `xs:base64Binary`
|
||||
без `BinaryDataQualifiers` — тоже двоичные данные, просто безлимитные. Замерено на 8.3.24.1691:
|
||||
такой узел загружается и выгружается обратно как `Length 0` / `AllowedLength Variable`, то есть
|
||||
`BinaryData(0)` — именно так его возвращает декомпилятор. Рукописный XML — единственное место,
|
||||
где узел без квалификаторов встречается: сама платформа их пишет всегда.
|
||||
|
||||
**Составной тип у поля платформа запрещает** — при загрузке отвечает «Поле не может иметь составной
|
||||
тип», хотя «Руководство разработчика» (17.5.2.3.4) его разрешает. Документация опережает реализацию.
|
||||
|
||||
Набор свойств поля — канонический блок реквизита (§4) без `ChoiceFoldersAndItems`, `LinkByType`,
|
||||
`Indexing`, `Use`, `FullTextSearch`, `DataHistory`: индексами и полнотекстовым поиском чужой таблицы
|
||||
1С не владеет. `ChoiceForm` у поля стоит ПОСЛЕ `ChoiceHistoryOnInput`, а не перед — порядок снят
|
||||
с выгрузки платформы.
|
||||
|
||||
### 18.4 Функции
|
||||
|
||||
Ключ — имя функции. Значение — строка (интерпретируется как `expression`) либо объект: как
|
||||
`urlTemplates` (§16) и `operations` (§17).
|
||||
|
||||
| Ключ | XML | Умолчание | Значения |
|
||||
|------|-----|-----------|----------|
|
||||
| `expression` | ExpressionInDataSource | — | выражение в источнике, обязательный |
|
||||
| `returns` | Type | `String` | тип возвращаемого значения |
|
||||
| `returnValue` | ReturnValue | `true` | `false` — процедура, тип не пишется |
|
||||
| `synonym` / `comment` | Synonym / Comment | авто из имени / пусто | ML (§4.4) |
|
||||
|
||||
Функция лежит полным узлом внутри файла источника, отдельного файла у неё нет.
|
||||
|
||||
**Параметры функции не являются объектами метаданных.** Формальные параметры записываются прямо
|
||||
в выражении как `&1`, `&2`; необязательные — в фигурных скобках `f(&1{, &2})`; переменное число —
|
||||
`&n[]` (только последним в списке). Отдельного ключа для них нет и быть не может.
|
||||
|
||||
### 18.5 Границы
|
||||
|
||||
- **Кубы OLAP** (`Cube`, `DimensionTable`, `Dimension`, `Resource`) не поддерживаются: проверить их
|
||||
платформой нечем — SSAS в контейнер не ставится, а эмит вслепую по документации уже давал осечку.
|
||||
- **Формы, макеты и модули** таблиц компилятор не создаёт — как и у прочих объектов (`form-add`,
|
||||
`form-compile`, `template-add`). У форм таблицы ссылки трёхчастные
|
||||
(`ExternalDataSource.<И>.Table.<Т>.Form.<Ф>`, главный реквизит `ExternalDataSourceTableObject.<И>.<Т>`),
|
||||
а набор назначений зависит от `tableDataType` — замерено на 8.3.24.1691:
|
||||
|
||||
| tableDataType | Допустимые назначения |
|
||||
|---|---|
|
||||
| `ObjectData` (ключ из одного поля) | Object, List, Choice, Custom |
|
||||
| `NonobjectData` (составной ключ) | Record, List, Choice, Custom |
|
||||
|
||||
Неверная пара не отвергается схемой формы — она валит загрузку всей конфигурации
|
||||
«Исключением XDTO при чтении файла», не называя причины, поэтому `form-add` проверяет её сам.
|
||||
- Слоты `default*Form` таблицы декомпилятор снимает — как у прочих объектов. Собранная из такого
|
||||
DSL конфигурация сама по себе не загрузится («Неизвестный объект метаданных — …Form.ФормаСписка»): файлы форм
|
||||
создаёт навык `form-add`. Это та же договорённость, что для справочников и документов.
|
||||
- **Таблица без ключевых полей** собирается: загрузку XML платформа принимает молча, хотя
|
||||
Конфигуратор интерактивно ключ требует. `meta-validate` предупреждает, но не отвергает — рабочие
|
||||
конфигурации без ключей существуют. Без ключа недоступны форма записи и набор записей.
|
||||
- `meta-compile` описывает источник **целиком**: повторный запуск заменяет его файл, выдаёт новый uuid,
|
||||
а таблицы, которых нет в описании, остаются на диске сиротами. Дописать таблицу или функцию
|
||||
в существующий источник умеет `meta-edit` (`add.tables` / `add.functions`), удалить таблицу —
|
||||
`meta-remove ExternalDataSource.<Источник>.Table.<Таблица>`.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user