feat(meta-edit,meta-remove): точечная правка и удаление внешних источников

meta-edit добавляет поля в таблицу внешнего источника: тот же парсер реквизита, свой тег <Field>,
свой контекст (без индексов и полнотекстового поиска — чужой таблицей 1С не владеет) и три своих
свойства в хвосте. Сам источник точечно не правится: и таблица (отдельный файл), и функция (узел
с полным набором свойств) требуют эмиттера, который живёт в meta-compile, — дублировать его здесь
значило бы завести вторую реализацию одного и того же.

Заодно закрыт тихий отказ, существовавший независимо от внешних источников: проверка допустимых
детей смотрела на истинность списка, а не на наличие ключа, поэтому для вида с пустым списком
трактовалась как «ограничений нет» и чужой ребёнок молча записывался в объект. Теперь add-attribute
на таблице внешнего источника отвергается с предупреждением, а не пишет <Attribute> вместо <Field>.

meta-remove понимает две формы: ExternalDataSource.PG — источник целиком, и четырёхчастную
ExternalDataSource.PG.Table.products — одну таблицу. Таблица числится в ChildObjects файла
источника, а не конфигурации, поэтому дерегистрация разведена переменной реестра; поиск ссылок
дополнен путями вида ExternalDataSource.И.Table.Т и ExternalDataSourceTableRef.И.Т.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
This commit is contained in:
Nick Shirokov
2026-09-05 21:44:23 +03:00
co-authored by Claude Opus 5
parent bb56a7e898
commit ed766154fe
18 changed files with 1349 additions and 73 deletions
@@ -1,4 +1,4 @@
# meta-remove v1.11 — Remove metadata object from 1C configuration dump
# meta-remove v1.12 — 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)) {
@@ -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.12 — 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,7 +449,7 @@ 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")
registered_in_cfg = False
@@ -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()