diff --git a/.claude/skills/meta-compile/reference/external-data-source.md b/.claude/skills/meta-compile/reference/external-data-source.md
index a05445626..ce0800ada 100644
--- a/.claude/skills/meta-compile/reference/external-data-source.md
+++ b/.claude/skills/meta-compile/reference/external-data-source.md
@@ -97,8 +97,9 @@
]
```
-Допустимые типы: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` и ссылка на таблицу
-внешнего источника — `ExternalDataSourceTableRef.<Источник>.<Таблица>`.
+Допустимые типы: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` (двоичные данные;
+`BinaryData(N)` — с ограничением длины) и ссылка на таблицу внешнего источника —
+`ExternalDataSourceTableRef.<Источник>.<Таблица>`.
**Составной тип у поля недопустим** — платформа такую конфигурацию не загружает.
diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1
index d82dbb021..a427b792f 100644
--- a/.claude/skills/meta-compile/scripts/meta-compile.ps1
+++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1
@@ -1,4 +1,4 @@
-# meta-compile v1.106 — Compile 1C metadata object from JSON
+# meta-compile v1.107 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -604,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"
@@ -807,6 +811,17 @@ function Emit-TypeContent {
}
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary, хоть 1С и принимает оба).
+ # ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников).
+ if ($typeStr -eq "BinaryData" -or $typeStr -match '^BinaryData\(') {
+ $blen = if ($typeStr -match '^BinaryData\((\d+)\)$') { $Matches[1] } else { "4294967292" }
+ $ballowed = if ($typeStr -match '^BinaryData\(\d+\)$') { "Variable" } else { "Fixed" }
+ X "$indentxs:base64Binary"
+ X "$indent"
+ X "$indent`t$blen"
+ X "$indent`t$ballowed"
+ X "$indent"
+ return
+ }
if ($typeStr -eq "ValueStorage") {
X "$indentv8:ValueStorage"
return
diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py
index 7adbc4ef6..96ef5e955 100644
--- a/.claude/skills/meta-compile/scripts/meta-compile.py
+++ b/.claude/skills/meta-compile/scripts/meta-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# meta-compile v1.106 — Compile 1C metadata object from JSON
+# meta-compile v1.107 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -735,6 +735,10 @@ type_synonyms = {
# ValueStorage / UUID — прощающий ввод (base64Binary / рус. форма → канон).
'valuestorage': 'ValueStorage',
'base64binary': 'ValueStorage',
+ # ДвоичныеДанные — ОТДЕЛЬНЫЙ тип, не ХранилищеЗначения: платформа пишет его как
+ # xs:base64Binary с квалификаторами. Встречается у полей внешних источников данных.
+ 'binarydata': 'BinaryData',
+ 'двоичныеданные': 'BinaryData',
'хранилищезначений': 'ValueStorage',
'хранилищезначения': 'ValueStorage',
'uuid': 'UUID',
@@ -927,6 +931,17 @@ 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}cfg:{type_str}')
return
+ # ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников).
+ m_bin = re.match(r'^BinaryData(?:\((\d+)\))?$', type_str)
+ if m_bin:
+ blen = m_bin.group(1) or '4294967292'
+ ballowed = 'Variable' if m_bin.group(1) else 'Fixed'
+ X(f'{indent}xs:base64Binary')
+ X(f'{indent}')
+ X(f'{indent}\t{blen}')
+ X(f'{indent}\t{ballowed}')
+ X(f'{indent}')
+ return
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary).
if type_str == 'ValueStorage':
X(f'{indent}v8:ValueStorage')
diff --git a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1
index 40699c2e7..920509beb 100644
--- a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1
+++ b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1
@@ -1,4 +1,4 @@
-# meta-decompile v0.65 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
+# meta-decompile v0.66 — 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,18 @@ 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, но принимает и эту форму.
+ $bq = $typeNode.SelectSingleNode('v8:BinaryDataQualifiers', $nsm)
+ if ($bq) {
+ $blen = $bq.SelectSingleNode('v8:Length', $nsm)
+ $bal = $bq.SelectSingleNode('v8:AllowedLength', $nsm)
+ if ($blen -and $bal -and $bal.InnerText -eq 'Variable') { $parts += "BinaryData($($blen.InnerText))" }
+ else { $parts += 'BinaryData' }
+ } else { $parts += 'ValueStorage' }
+ break
+ }
default { $parts += (Strip-NsPrefix $raw) } # cfg:CatalogRef.X → CatalogRef.X
}
} elseif ($ln -eq 'TypeSet') {
@@ -288,12 +299,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 +425,7 @@ function Attr-ToDsl {
# Пустой (реквизит без типа / произвольный) → $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 +1677,116 @@ 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 }
+ # Пустой ≠ авто-синоним из имени: без явного '' компилятор до-генерит его из имени.
+ 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 }
+ $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) {
diff --git a/.claude/skills/meta-decompile/scripts/meta-decompile.py b/.claude/skills/meta-decompile/scripts/meta-decompile.py
index faa2eba14..0087922a0 100644
--- a/.claude/skills/meta-decompile/scripts/meta-decompile.py
+++ b/.claude/skills/meta-decompile/scripts/meta-decompile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# meta-decompile v0.65 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
+# meta-decompile v0.66 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
@@ -311,7 +311,18 @@ 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, но принимает и эту форму.
+ bq = type_node.find('v8:BinaryDataQualifiers', NS)
+ if bq is not None:
+ blen = bq.find('v8:Length', NS)
+ bal = bq.find('v8:AllowedLength', NS)
+ if blen is not None and bal is not None and _text(bal) == 'Variable':
+ parts.append(f'BinaryData({_text(blen)})')
+ else:
+ parts.append('BinaryData')
+ else:
+ parts.append('ValueStorage')
else:
parts.append(strip_ns_prefix(raw)) # cfg:CatalogRef.X → CatalogRef.X
elif ln == 'TypeSet':
@@ -420,6 +431,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 +451,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 +638,8 @@ def attr_to_dsl(attr_node):
# Пустой (реквизит без типа) → 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 +2134,7 @@ SUPPORTED_TYPES = (
'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob',
'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter',
'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService',
+ 'ExternalDataSource',
)
@@ -2154,6 +2177,150 @@ 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:
+ # Пустой != авто-синоним из имени: без явного '' компилятор до-генерит его.
+ 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
+ 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:
diff --git a/tests/skills/cases/meta-decompile/external-data-source.json b/tests/skills/cases/meta-decompile/external-data-source.json
new file mode 100644
index 000000000..c0361b1e6
--- /dev/null
+++ b/tests/skills/cases/meta-decompile/external-data-source.json
@@ -0,0 +1,58 @@
+{
+ "name": "Декомпиляция внешнего источника: таблицы из отдельных файлов, функции, поля",
+ "preRun": [
+ {
+ "script": "meta-compile/scripts/meta-compile",
+ "input": {
+ "type": "ExternalDataSource",
+ "name": "PG",
+ "dataLockControlMode": "AutomaticAndManaged",
+ "tables": {
+ "prices": ["product_id: Number(10,0)", "price: Number(15,2)"],
+ "products": {
+ "nameInDataSource": "eds.public.products",
+ "tableDataType": "ObjectData",
+ "keyFields": ["id"],
+ "presentationField": "name",
+ "transactionsIsolationLevel": "ReadUncommitted",
+ "fields": [
+ "id: Number(10,0)",
+ "name: String(150)",
+ "photo: BinaryData | nullable",
+ { "name": "cost", "type": "Number(15,2)", "nameInDataSource": "cost_net", "readOnly": true }
+ ]
+ }
+ },
+ "functions": {
+ "total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" },
+ "nextKey": "NEXT VALUE FOR public.seq_key"
+ }
+ },
+ "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" }
+ }
+ ],
+ "params": {
+ "objectPath": "ExternalDataSources/PG.xml",
+ "outputPath": "draft.json"
+ },
+ "expect": {
+ "files": ["draft.json"],
+ "fileContains": [
+ {
+ "file": "draft.json",
+ "text": [
+ "\"dataLockControlMode\": \"AutomaticAndManaged\"",
+ "\"keyFields\": [\"id\"]",
+ "\"nameInDataSource\": \"eds.public.products\"",
+ "\"transactionsIsolationLevel\": \"ReadUncommitted\"",
+ "photo: BinaryData | nullable",
+ "\"nameInDataSource\": \"cost_net\"",
+ "\"nextKey\": \"NEXT VALUE FOR public.seq_key\""
+ ]
+ }
+ ],
+ "fileNotContains": [
+ { "file": "draft.json", "text": "ValueStorage" }
+ ]
+ }
+}
diff --git a/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG.xml b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG.xml
new file mode 100644
index 000000000..ce6f9c996
--- /dev/null
+++ b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+ UUID-002
+ UUID-003
+
+
+ UUID-004
+ UUID-005
+
+
+ UUID-006
+ UUID-007
+
+
+
+ PG
+
+
+ ru
+ PG
+
+
+
+ AutomaticAndManaged
+
+
+
+
+
+
+ total
+
+
+ true
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+ public.f_total(&1, &2)
+
+
+
+
+ nextKey
+
+
+ true
+
+ xs:string
+
+ 10
+ Variable
+
+
+ NEXT VALUE FOR public.seq_key
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml
new file mode 100644
index 000000000..f3d281122
--- /dev/null
+++ b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+ UUID-002
+ UUID-003
+
+
+ UUID-004
+ UUID-005
+
+
+ UUID-006
+ UUID-007
+
+
+ UUID-008
+ UUID-009
+
+
+ UUID-010
+ UUID-011
+
+
+ UUID-012
+ UUID-013
+
+
+ UUID-014
+ UUID-015
+
+
+ UUID-016
+ UUID-017
+
+
+
+ prices
+
+
+ ru
+ prices
+
+
+
+ Table
+ prices
+
+ NonobjectData
+
+
+
+
+
+ true
+ false
+
+ Auto
+ Begin
+ Directly
+ Auto
+
+
+
+
+
+
+
+
+
+
+
+ false
+ false
+ Auto
+
+ InDialog
+
+
+ Automatic
+
+
+
+
+ product_id
+
+
+ ru
+ product_id
+
+
+
+
+ xs:decimal
+
+ 10
+ 0
+ Any
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+ 0
+ DontCheck
+
+
+ Auto
+ Auto
+ Auto
+
+ product_id
+ false
+ false
+
+
+
+
+ price
+
+
+ ru
+ price
+
+
+
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+ 0
+ DontCheck
+
+
+ Auto
+ Auto
+ Auto
+
+ price
+ false
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml
new file mode 100644
index 000000000..f68a791a1
--- /dev/null
+++ b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml
@@ -0,0 +1,256 @@
+
+
+
+
+
+ UUID-002
+ UUID-003
+
+
+ UUID-004
+ UUID-005
+
+
+ UUID-006
+ UUID-007
+
+
+ UUID-008
+ UUID-009
+
+
+ UUID-010
+ UUID-011
+
+
+ UUID-012
+ UUID-013
+
+
+ UUID-014
+ UUID-015
+
+
+ UUID-016
+ UUID-017
+
+
+
+ products
+
+
+ ru
+ products
+
+
+
+ Table
+ eds.public.products
+
+ ObjectData
+
+ ExternalDataSource.PG.Table.products.Field.id
+
+ ExternalDataSource.PG.Table.products.Field.name
+
+
+
+ true
+ false
+
+ ExternalDataSource.PG.Table.products.Field.name
+
+ Auto
+ Begin
+ Directly
+ Auto
+
+
+
+
+
+
+
+
+
+
+
+ false
+ false
+ ReadUncommitted
+
+ InDialog
+
+
+ Automatic
+
+
+
+
+ id
+
+
+ ru
+ id
+
+
+
+
+ xs:decimal
+
+ 10
+ 0
+ Any
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+ 0
+ DontCheck
+
+
+ Auto
+ Auto
+ Auto
+
+ id
+ false
+ false
+
+
+
+
+ name
+
+
+ ru
+ name
+
+
+
+
+ xs:string
+
+ 150
+ Variable
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+
+ DontCheck
+
+
+ Auto
+ Auto
+ Auto
+
+ name
+ false
+ false
+
+
+
+
+ photo
+
+
+ ru
+ photo
+
+
+
+
+ xs:base64Binary
+
+ 4294967292
+ Fixed
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+
+ DontCheck
+
+
+ Auto
+ Auto
+ Auto
+
+ photo
+ false
+ true
+
+
+
+
+ cost
+
+
+ ru
+ cost
+
+
+
+
+ xs:decimal
+
+ 15
+ 2
+ Any
+
+
+ false
+
+
+
+ false
+
+ false
+ false
+
+
+ false
+ 0
+ DontCheck
+
+
+ Auto
+ Auto
+ Auto
+
+ cost_net
+ true
+ false
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/meta-decompile/snapshots/external-data-source/draft.json b/tests/skills/cases/meta-decompile/snapshots/external-data-source/draft.json
new file mode 100644
index 000000000..5bf91d417
--- /dev/null
+++ b/tests/skills/cases/meta-decompile/snapshots/external-data-source/draft.json
@@ -0,0 +1,33 @@
+{
+ "type": "ExternalDataSource",
+ "name": "PG",
+ "dataLockControlMode": "AutomaticAndManaged",
+ "tables": {
+ "prices": ["product_id: Number(10,0)", "price: Number(15,2)"],
+ "products": {
+ "nameInDataSource": "eds.public.products",
+ "tableDataType": "ObjectData",
+ "keyFields": ["id"],
+ "presentationField": "name",
+ "transactionsIsolationLevel": "ReadUncommitted",
+ "fields": [
+ "id: Number(10,0)",
+ "name: String(150)",
+ "photo: BinaryData | nullable",
+ {
+ "name": "cost",
+ "type": "Number(15,2)",
+ "nameInDataSource": "cost_net",
+ "flags": ["readonly"]
+ }
+ ]
+ }
+ },
+ "functions": {
+ "total": {
+ "expression": "public.f_total(&1, &2)",
+ "returns": "Number(15,2)"
+ },
+ "nextKey": "NEXT VALUE FOR public.seq_key"
+ }
+}
\ No newline at end of file