From 6dde36a690134be93f0ad3144e1d5b6d44ae0e5d Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 5 Sep 2026 20:53:37 +0300 Subject: [PATCH] =?UTF-8?q?feat(meta-compile):=20=D1=81=D0=BE=D0=B7=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=BD=D0=B5=D1=88=D0=BD=D0=B8?= =?UTF-8?q?=D1=85=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вид ExternalDataSource навыки meta-* не поддерживали вовсе: в корпусе выгрузок его нет ни одного, формат был неизвестен. Разведан на стенде (PostgreSQL в Docker + база 8.3.24) и по реальной выгрузке рабочего проекта. meta-compile собирает источник целиком одним JSON: сам источник, его таблицы с полями и функции. Единственный вид, у которого объект складывается более чем из одного XML — файл источника плюс по файлу на таблицу в <Источник>/Tables/. DSL без новых конвенций: tables — dict имя → массив полей ЛИБО объект (как tabularSections), поле — обычный реквизит плюс три своих ключа (nameInDataSource/readOnly/allowNull), ссылки на поля короткими именами (как inputByString у справочника), functions — dict имя → строка ИЛИ объект (как urlTemplates). Параметры функций не объекты метаданных: они живут в самом выражении как &1, &2. Инструкция под каскадом: в SKILL.md строка индекса, вся специфика — в reference/external-data-source.md. meta-validate принимает ExternalDataSource и Table, знает их наборы GeneratedType, состав ChildObjects и шестичастную ссылку на форму таблицы. Проверено на выгрузке платформы и на чужой рабочей выгрузке из другого проекта — обе проходят чисто. Раундтрип на платформе 8.3.24.1691: наш эмит → загрузка → выгрузка даёт файлы, идентичные поданным (с точностью до uuid). Два поведения платформы пришлось воспроизвести явно, оба измерены: ввод по строке выводится из поля представления, а заданное значение незаполненного родителя платформа при загрузке XML сбрасывает в пустую строку — задать его можно только интерактивно. Спека: раздел «Внешние источники данных» в 1c-config-objects-spec.md, формат ссылок, наборы GeneratedType, строка в индексе спек. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi --- .claude/skills/meta-compile/SKILL.md | 1 + .../reference/external-data-source.md | 137 ++++++++ .../meta-compile/scripts/meta-compile.ps1 | 301 ++++++++++++++++- .../meta-compile/scripts/meta-compile.py | 304 +++++++++++++++++- .../skills/meta-edit/scripts/meta-edit.ps1 | 5 +- .claude/skills/meta-edit/scripts/meta-edit.py | 5 +- .../meta-validate/scripts/meta-validate.ps1 | 22 +- .../meta-validate/scripts/meta-validate.py | 20 +- docs/1c-config-objects-spec.md | 197 +++++++++++- docs/1c-configuration-spec.md | 11 +- docs/1c-specs-index.md | 5 +- .../meta-compile/external-data-source.json | 71 ++++ .../external-data-source/Configuration.xml | 252 +++++++++++++++ .../Ext/ClientApplicationInterface.xml | 18 ++ .../ExternalDataSources/PG.xml | 68 ++++ .../PG/Tables/byParent.xml | 169 ++++++++++ .../ExternalDataSources/PG/Tables/prices.xml | 210 ++++++++++++ .../PG/Tables/products.xml | 295 +++++++++++++++++ .../Languages/Русский.xml | 16 + tests/skills/check-type-maps.mjs | 3 + 20 files changed, 2071 insertions(+), 39 deletions(-) create mode 100644 .claude/skills/meta-compile/reference/external-data-source.md create mode 100644 tests/skills/cases/meta-compile/external-data-source.json create mode 100644 tests/skills/cases/meta-compile/snapshots/external-data-source/Configuration.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/external-data-source/Ext/ClientApplicationInterface.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/byParent.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/external-data-source/Languages/Русский.xml diff --git a/.claude/skills/meta-compile/SKILL.md b/.claude/skills/meta-compile/SKILL.md index d277eb512..faf6dc98b 100644 --- a/.claude/skills/meta-compile/SKILL.md +++ b/.claude/skills/meta-compile/SKILL.md @@ -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` | Кросс-типовые детали: diff --git a/.claude/skills/meta-compile/reference/external-data-source.md b/.claude/skills/meta-compile/reference/external-data-source.md new file mode 100644 index 000000000..c900aa942 --- /dev/null +++ b/.claude/skills/meta-compile/reference/external-data-source.md @@ -0,0 +1,137 @@ +# ExternalDataSource (внешний источник данных) + +Источник описывается **одним JSON целиком**: сам источник, его таблицы с полями и функции. +Компилятор пишет несколько файлов — `ExternalDataSources/<Имя>.xml` и по файлу на таблицу +в `ExternalDataSources/<Имя>/Tables/`. + +Параметров соединения в конфигурации нет: строка соединения, пользователь, пароль и тип СУБД +задаются в режиме «Предприятие» и хранятся в базе. В DSL их указывать негде и не нужно. + +```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` — табличная функция | +| `expressionInDataSource` | пусто | выражение для `Expression`, напр. `public.f_by_parent(&1)`; имя базы не указывается | +| `tableDataType` | `NonobjectData` | `ObjectData` (запись определяется одним полем) / `NonobjectData` | +| `keyFields` | `[]` | имена ключевых полей | +| `presentationField` | пусто | имя поля представления (только `ObjectData`) | +| `parentField` | пусто | имя поля родителя; его тип должен быть ссылкой на эту же таблицу | +| `unfilledParentValue` | `null` | значение «нет родителя»; `null` → NULL, число/строка → «Заданное значение» | +| `inputByString` | `[]` | имена полей ввода по строке | +| `dataVersionField` | пусто | имя поля версии данных | +| `dataLockFields` | `[]` | имена полей блокировки | +| `readOnly` | `false` | запрет записи | +| `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`) задаются **короткими именами** — компилятор разворачивает их +в полный путь `ExternalDataSource.<Источник>.Table.<Таблица>.Field.<Поле>`. + +Прочие свойства — представления (`objectPresentation`, `listPresentation`, …), формы по умолчанию +(`defaultObjectForm`, `defaultListForm`, …), `characteristics`, `explanation`, +`includeHelpInContents` — как у справочника. + +## Поля + +Строковая и объектная форма — те же, что у реквизитов (см. `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.<Источник>.<Таблица>`. + +**Составной тип запрещён платформой** — она отвергает загрузку с ошибкой «Поле не может иметь +составной тип», поэтому компилятор отказывает сразу. + +## Функции + +Ключ — имя функции. Значение — строка (интерпретируется как `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)" } +} +``` + +## Чего компилятор не делает + +- **Кубы OLAP** (`Cube`, `DimensionTable`, `Dimension`, `Resource`) не поддерживаются. +- **Формы и модули** таблиц не создаются — как и у прочих объектов, для форм есть `form-add` + и `form-compile`. + +## Пустой ключ таблицы — предупреждение, не ошибка + +Конфигуратор интерактивно требует `keyFields`, а загрузка XML принимает таблицу без ключа молча. +Рабочие конфигурации без ключей существуют, поэтому компилятор такую таблицу собирает, а +`meta-validate` предупреждает. Без ключа недоступны форма записи и набор записей. diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 52eaa45c0..6e51a7c2b 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.104 — Compile 1C metadata object from JSON +# meta-compile v1.105 — 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 как есть — выгрузка получалась с , которую платформа не принимает. # Прощаем регистр, но приводим к канону списка. @@ -638,6 +641,7 @@ $script:typeSynonyms["планвидовхарактеристикссылка"] $script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef" $script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef" $script:typeSynonyms["планобменассылка"] = "ExchangePlanRef" +$script:typeSynonyms["внешнийисточникданныхтаблицассылка"] = "ExternalDataSourceTableRef" $script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef" $script:typeSynonyms["задачассылка"] = "TaskRef" $script:typeSynonyms["определяемыйтип"] = "DefinedType" @@ -856,7 +860,9 @@ function Emit-TypeContent { # $script:cfgPrefix = $null означает «пишем файл, корень которого cfg НЕ объявляет» # (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит # на локальную форму: в корпусе `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$($script:cfgPrefix):$typeStr" } else { @@ -1186,6 +1192,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 } } } @@ -2056,7 +2066,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." } @@ -2120,18 +2130,34 @@ function Emit-Attribute { if ($parsed.fillChecking) { $fillChecking = $parsed.fillChecking } X "$indent`t`t$fillChecking" - X "$indent`t`t$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })" + # Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm + # у него стоит ПОСЛЕ ChoiceHistoryOnInput, а не перед — порядок снят с выгрузки платформы. + if ($context -ne "eds-field") { + X "$indent`t`t$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })" + } 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$qc" $coi = if ($parsed.createOnInput) { $parsed.createOnInput } else { "Auto" } X "$indent`t`t$coi" - if ($parsed.choiceForm) { X "$indent`t`t$(Esc-XmlText "$($parsed.choiceForm)")" } else { X "$indent`t`t" } - Emit-LinkByType "$indent`t`t" $parsed.linkByType + if ($context -ne "eds-field") { + if ($parsed.choiceForm) { X "$indent`t`t$(Esc-XmlText "$($parsed.choiceForm)")" } else { X "$indent`t`t" } + Emit-LinkByType "$indent`t`t" $parsed.linkByType + } $chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" } X "$indent`t`t$chi" + if ($context -eq "eds-field") { + if ($parsed.choiceForm) { X "$indent`t`t$(Esc-XmlText "$($parsed.choiceForm)")" } else { X "$indent`t`t" } + $nids = if ($parsed.nameInDataSource) { "$($parsed.nameInDataSource)" } else { $parsed.name } + X "$indent`t`t$(Esc-XmlText $nids)" + $ro = if ($parsed.readOnly -eq $true -or $parsed.flags -contains "readonly") { "true" } else { "false" } + X "$indent`t`t$ro" + $an = if ($parsed.allowNull -eq $true -or $parsed.flags -contains "nullable") { "true" } else { "false" } + X "$indent`t`t$an" + } + # Измерение регистра сведений: 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" } @@ -2185,7 +2211,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) не имеют /, но имеют . if ($context -ne "account-flag") { # Ресурс регистра накопления/расчёта/бухгалтерии НЕ имеет (только ); измерение/реквизит — имеют. @@ -4321,6 +4348,223 @@ 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$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$n"))" + } + X "$indent" +} + +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"))" +} + +function Emit-ExternalDataSourceProperties { + param([string]$indent) + $i = $indent + X "$i$(Esc-XmlText $objName)" + Emit-MLText $i "Synonym" $synonym + if ($def.comment) { X "$i$(Esc-XmlText $def.comment)" } else { X "$i" } + $dlcm = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic" + X "$i$dlcm" +} + +# Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо +# в выражении как &1, &2 (см. reference/external-data-source.md). +function Emit-EdsFunction { + param([string]$indent, [string]$fnName, $val) + $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" + X "$indent`t" + X "$indent`t`t$(Esc-XmlText $fnName)" + Emit-MLText "$indent`t`t" "Synonym" $fnSynonym + if ($fnComment) { X "$indent`t`t$(Esc-XmlText $fnComment)" } else { X "$indent`t`t" } + X "$indent`t`t$(if ($returnValue) { 'true' } else { 'false' })" + if ($returnValue) { + Emit-ValueType "$indent`t`t" $(if ($returns) { $returns } else { "String" }) + } else { + X "$indent`t`t" + } + X "$indent`t`t$(Esc-XmlText $expr)" + X "$indent`t" + X "$indent" +} + +# Свойства таблицы: 38 узлов в порядке выгрузки платформы. +function Emit-EdsTableProperties { + param([string]$indent, [string]$srcName, [string]$tableName, $t) + $i = $indent + $tblSynonym = if ($t -and $null -ne $t.synonym) { $t.synonym } else { Split-CamelCase $tableName } + X "$i$(Esc-XmlText $tableName)" + Emit-MLText $i "Synonym" $tblSynonym + if ($t -and $t.comment) { X "$i$(Esc-XmlText "$($t.comment)")" } else { X "$i" } + + $tableType = if ($t -and $t.tableType) { "$($t.tableType)" } else { "Table" } + X "$i$tableType" + # Имя в источнике по умолчанию равно имени объекта — так поступает и платформа. + $nids = if ($t -and $t.nameInDataSource) { "$($t.nameInDataSource)" } elseif ($tableType -eq "Expression") { "" } else { $tableName } + if ($nids) { X "$i$(Esc-XmlText $nids)" } else { X "$i" } + $expr = if ($t -and $t.expressionInDataSource) { "$($t.expressionInDataSource)" } elseif ($t -and $t.expression) { "$($t.expression)" } else { "" } + if ($expr) { X "$i$(Esc-XmlText $expr)" } else { X "$i" } + $dataType = if ($t -and $t.tableDataType) { "$($t.tableDataType)" } else { "NonobjectData" } + X "$i$dataType" + + 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. + $upv = if ($t) { $t.unfilledParentValue } else { $null } + $hasParent = ($t -and $t.parentField) + if ($null -ne $upv) { Emit-MinMaxValue $i "UnfilledParentValue" $upv } + elseif ($hasParent) { X "$i" } + else { X "$i" } + Emit-Characteristics $i $(if ($t) { $t.characteristics } else { $null }) + + X "$i$(if ($t -and $t.useStandardCommands -eq $false) { 'false' } else { 'true' })" + X "$i$(if ($t -and $t.quickChoice -eq $true) { 'true' } else { 'false' })" + # Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке). + # Явный список, в том числе пустой, уважаем как есть — отсюда 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$(if ($t -and $t.createOnInput) { "$($t.createOnInput)" } else { 'Auto' })" + X "$i$(if ($t -and $t.searchStringModeOnInputByString) { "$($t.searchStringModeOnInputByString)" } else { 'Begin' })" + X "$i$(if ($t -and $t.choiceDataGetModeOnInputByString) { "$($t.choiceDataGetModeOnInputByString)" } else { 'Directly' })" + X "$i$(if ($t -and $t.choiceHistoryOnInput) { "$($t.choiceHistoryOnInput)" } else { 'Auto' })" + + foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { + $key = $formTag.Substring(0,1).ToLower() + $formTag.Substring(1) + Emit-FormRef $i $formTag $(if ($t) { $t.$key } else { $null }) + } + 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$(if ($t -and $t.includeHelpInContents -eq $true) { 'true' } else { 'false' })" + X "$i$(if ($t -and $t.readOnly -eq $true) { 'true' } else { 'false' })" + X "$i$(if ($t -and $t.transactionsIsolationLevel) { "$($t.transactionsIsolationLevel)" } else { 'Auto' })" + Emit-EdsFieldRefScalar $i "DataVersionField" $(if ($t) { $t.dataVersionField } else { $null }) $srcName $tableName + X "$i$(if ($t -and $t.editType) { "$($t.editType)" } else { 'InDialog' })" + Emit-MDRefList $i "BasedOn" $(if ($t) { $t.basedOn } else { $null }) + Emit-EdsFieldRefList $i "DataLockFields" $(if ($t) { $t.dataLockFields } else { $null }) $srcName $tableName + X "$i$(if ($t -and $t.dataLockControlMode) { "$($t.dataLockControlMode)" } else { 'Automatic' })" +} + +# Отдельный XML-документ таблицы. Возвращает строку: X пишет в общий StringBuilder, +# поэтому «перехват» — запомнить длину, отдать эмиттерам, вырезать добавленное +# (тот же приём, что у составного типа). +function Build-EdsTableXml { + param([string]$srcName, [string]$tableName, $entry) + $before = $script:xml.Length + + $tableUuid = New-Guid-String + X '' + X "" + X "`t" + # InternalInfo у таблицы эмитится здесь, а не через $script:generatedTypes: имя элемента + # трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает. + X "`t`t" + foreach ($pair in @( + @("ExternalDataSourceTableManager", "Manager"), + @("ExternalDataSourceTableObject", "Object"), + @("ExternalDataSourceTableRef", "Ref"), + @("ExternalDataSourceTableList", "List"), + @("ExternalDataSourceTableRecord", "Record"), + @("ExternalDataSourceTableRecordSet", "RecordSet"), + @("ExternalDataSourceTableRecordKey", "RecordKey"), + @("ExternalDataSourceTableRecordManager", "RecordManager"))) { + X "`t`t`t" + X "`t`t`t`t$(New-Guid-String)" + X "`t`t`t`t$(New-Guid-String)" + X "`t`t`t" + } + X "`t`t" + + X "`t`t" + Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props + X "`t`t" + + $fields = @($entry.fields) + if ($fields.Count -gt 0) { + X "`t`t" + foreach ($f in $fields) { + $parsed = Parse-AttributeShorthand $f + Emit-Attribute "`t`t`t" $parsed "eds-field" "Field" + } + X "`t`t" + } else { + X "`t`t" + } + X "`t
" + X "
" + + $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"' @@ -4464,6 +4708,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" @@ -4761,6 +5006,29 @@ 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" + foreach ($tblName in $script:edsTables.Keys) { + X "`t`t`t$(Esc-XmlText $tblName)
" + } + foreach ($fnName in $functions.Keys) { + Emit-EdsFunction "`t`t`t" $fnName $functions[$fnName] + } + X "`t`t
" + } else { + X "`t`t" + } +} + # --- CommonModule: no ChildObjects --- X "`t" @@ -4809,6 +5077,7 @@ $script:typePluralMap = @{ "WSReference" = "WSReferences" "CommonPicture" = "CommonPictures" "CommonTemplate" = "CommonTemplates" + "ExternalDataSource" = "ExternalDataSources" } $typePlural = $script:typePluralMap[$objType] @@ -5043,6 +5312,20 @@ function Write-XmlFileKeepEol([string]$path, [string]$text, $encoding) { 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) { + $tableXml = Build-EdsTableXml $objName $tblName $script:edsTables[$tblName] + $tablePath = Join-Path $tablesDir "$tblName.xml" + Write-XmlFileKeepEol $tablePath $tableXml $enc + $edsTablesCreated += $tablePath + } +} + # Module files $modulesCreated = @() diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index 7533a7f18..5887eed92 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.104 — Compile 1C metadata object from JSON +# meta-compile v1.105 — 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) @@ -745,6 +748,7 @@ type_synonyms = { 'планвидоврасчётассылка': 'ChartOfCalculationTypesRef', 'планвидоврасчетассылка': 'ChartOfCalculationTypesRef', 'планобменассылка': 'ExchangePlanRef', + 'внешнийисточникданныхтаблицассылка': 'ExternalDataSourceTableRef', 'бизнеспроцессссылка': 'BusinessProcessRef', 'задачассылка': 'TaskRef', 'определяемыйтип': 'DefinedType', @@ -969,7 +973,9 @@ def emit_type_content(indent, type_str): # cfg_prefix = None означает «пишем файл, корень которого cfg НЕ объявляет» # (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит # на локальную форму: в корпусе `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}{cfg_prefix}:{type_str}') @@ -1293,6 +1299,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): @@ -2285,15 +2295,28 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'): if parsed.get('fillChecking'): fill_checking = parsed['fillChecking'] X(f'{indent}\t\t{fill_checking}') - X(f'{indent}\t\t{parsed.get("choiceFoldersAndItems") or "Items"}') + # Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm + # у него стоит ПОСЛЕ ChoiceHistoryOnInput, а не перед — порядок снят с выгрузки платформы. + if context != 'eds-field': + X(f'{indent}\t\t{parsed.get("choiceFoldersAndItems") or "Items"}') 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{parsed.get("quickChoice") or "Auto"}') X(f'{indent}\t\t{parsed.get("createOnInput") or "Auto"}') - X(f'{indent}\t\t{esc_xml_text(str(parsed["choiceForm"]))}' if parsed.get('choiceForm') else f'{indent}\t\t') - emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType')) + if context != 'eds-field': + X(f'{indent}\t\t{esc_xml_text(str(parsed["choiceForm"]))}' if parsed.get('choiceForm') else f'{indent}\t\t') + emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType')) chi = parsed.get('choiceHistoryOnInput') or 'Auto' X(f'{indent}\t\t{chi}') + + if context == 'eds-field': + X(f'{indent}\t\t{esc_xml_text(str(parsed["choiceForm"]))}' if parsed.get('choiceForm') else f'{indent}\t\t') + nids = parsed.get('nameInDataSource') or parsed['name'] + X(f'{indent}\t\t{esc_xml_text(str(nids))}') + ro = 'true' if (parsed.get('readOnly') is True or 'readonly' in parsed.get('flags', [])) else 'false' + X(f'{indent}\t\t{ro}') + an = 'true' if (parsed.get('allowNull') is True or 'nullable' in parsed.get('flags', [])) else 'false' + X(f'{indent}\t\t{an}') # Измерение регистра сведений: 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' @@ -2341,7 +2364,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_value}') - if context not in ('processor', 'processor-tabular'): + # и не для полей внешнего источника: индексами и полнотекстовым поиском чужой таблицы 1С не владеет. + if context not in ('processor', 'processor-tabular', 'eds-field'): # Признаки учёта ПС (account-flag) не имеют /, но имеют . if context != 'account-flag': # Ресурс регистра накопления НЕ имеет (только ); измерение/реквизит — имеют. @@ -4313,6 +4337,237 @@ 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{esc_xml_text(get_eds_field_ref(src_name, table_name, n))}') + X(f'{indent}') + + +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))}') + + +def emit_external_data_source_properties(indent): + i = indent + X(f'{i}{esc_xml_text(obj_name)}') + emit_mltext(i, 'Synonym', synonym) + if defn.get('comment'): + X(f'{i}{esc_xml_text(str(defn["comment"]))}') + else: + X(f'{i}') + dlcm = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic') + X(f'{i}{dlcm}') + + +def emit_eds_function(indent, fn_name, val): + """Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо + в выражении как &1, &2 (см. reference/external-data-source.md).""" + 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}') + X(f'{indent}\t') + X(f'{indent}\t\t{esc_xml_text(fn_name)}') + emit_mltext(f'{indent}\t\t', 'Synonym', fn_synonym) + if fn_comment: + X(f'{indent}\t\t{esc_xml_text(fn_comment)}') + else: + X(f'{indent}\t\t') + X(f'{indent}\t\t{"true" if return_value else "false"}') + if return_value: + emit_value_type(f'{indent}\t\t', returns or 'String') + else: + X(f'{indent}\t\t') + X(f'{indent}\t\t{esc_xml_text(expr)}') + X(f'{indent}\t') + X(f'{indent}') + + +def emit_eds_table_properties(indent, src_name, table_name, t): + """Свойства таблицы: 38 узлов в порядке выгрузки платформы.""" + 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}{esc_xml_text(table_name)}') + emit_mltext(i, 'Synonym', tbl_synonym) + if t.get('comment'): + X(f'{i}{esc_xml_text(str(t["comment"]))}') + else: + X(f'{i}') + + table_type = str(t.get('tableType') or 'Table') + X(f'{i}{table_type}') + # Имя в источнике по умолчанию равно имени объекта — так поступает и платформа. + if t.get('nameInDataSource'): + nids = str(t['nameInDataSource']) + elif table_type == 'Expression': + nids = '' + else: + nids = table_name + X(f'{i}{esc_xml_text(nids)}' if nids else f'{i}') + expr = str(t.get('expressionInDataSource') or t.get('expression') or '') + X(f'{i}{esc_xml_text(expr)}' if expr else f'{i}') + X(f'{i}{t.get("tableDataType") or "NonobjectData"}') + + 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. + upv = t.get('unfilledParentValue') + if upv is not None: + emit_min_max_value(i, 'UnfilledParentValue', upv) + elif t.get('parentField'): + X(f'{i}') + else: + X(f'{i}') + emit_characteristics(i, t.get('characteristics')) + + X(f'{i}{"false" if t.get("useStandardCommands") is False else "true"}') + X(f'{i}{"true" if t.get("quickChoice") is True else "false"}') + # Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке). + # Явный список, в том числе пустой, уважаем как есть — отсюда 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}{t.get("createOnInput") or "Auto"}') + X(f'{i}{t.get("searchStringModeOnInputByString") or "Begin"}') + X(f'{i}{t.get("choiceDataGetModeOnInputByString") or "Directly"}') + X(f'{i}{t.get("choiceHistoryOnInput") or "Auto"}') + + for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'): + key = form_tag[0].lower() + form_tag[1:] + emit_form_ref(i, form_tag, t.get(key)) + 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}{"true" if t.get("includeHelpInContents") is True else "false"}') + X(f'{i}{"true" if t.get("readOnly") is True else "false"}') + X(f'{i}{t.get("transactionsIsolationLevel") or "Auto"}') + emit_eds_field_ref_scalar(i, 'DataVersionField', t.get('dataVersionField'), src_name, table_name) + X(f'{i}{t.get("editType") or "InDialog"}') + 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}{t.get("dataLockControlMode") or "Automatic"}') + + +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): + """Отдельный XML-документ таблицы. Возвращает строку: X пишет в общий список строк, + поэтому «перехват» — запомнить длину, отдать эмиттерам, срезать добавленное + (в ps1-порте тот же приём выражен через StringBuilder — различие рантаймов, не логики).""" + before = len(lines) + + X('') + X(f'') + X(f'\t') + # InternalInfo у таблицы эмитится здесь, а не через generated_types: имя элемента + # трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает. + X('\t\t') + for prefix, category in EDS_TABLE_GENERATED_TYPES: + X(f'\t\t\t') + X(f'\t\t\t\t{new_uuid()}') + X(f'\t\t\t\t{new_uuid()}') + X('\t\t\t') + X('\t\t') + + X('\t\t') + emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props']) + X('\t\t') + + fields = entry['fields'] + if fields: + X('\t\t') + for f in fields: + emit_attribute('\t\t\t', parse_attribute_shorthand(f), 'eds-field', 'Field') + X('\t\t') + else: + X('\t\t') + X('\t
') + X('
') + + chunk = '\r\n'.join(lines[before:]) + del lines[before:] + return chunk + + # --------------------------------------------------------------------------- # 14. Namespaces # --------------------------------------------------------------------------- @@ -4453,6 +4708,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') @@ -4726,6 +4982,25 @@ if obj_type == 'WebService': else: X('\t\t') +# --- 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') + for tbl_name in eds_tables: + X(f'\t\t\t{esc_xml_text(tbl_name)}
') + for fn_name, fn_val in functions.items(): + emit_eds_function('\t\t\t', fn_name, fn_val) + X('\t\t
') + else: + X('\t\t') + # --- CommonModule: no ChildObjects --- X(f'\t') @@ -4775,6 +5050,7 @@ type_plural_map = { 'WSReference': 'WSReferences', 'CommonPicture': 'CommonPictures', 'CommonTemplate': 'CommonTemplates', + 'ExternalDataSource': 'ExternalDataSources', } type_plural = type_plural_map[obj_type] @@ -4795,6 +5071,18 @@ if obj_type not in types_no_sub_dir: 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: + table_xml = build_eds_table_xml(obj_name, tbl_name, eds_tables[tbl_name]) + 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 = [] diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index b1317cd58..9d0e0b166 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.ps1 +++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1 @@ -1,4 +1,4 @@ -# meta-edit v1.42 — Edit existing 1C metadata object XML +# meta-edit v1.43 — 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") diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py index 695b9ac56..0ad834873 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.py +++ b/.claude/skills/meta-edit/scripts/meta-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-edit v1.42 — Edit existing 1C metadata object XML +# meta-edit v1.43 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -427,7 +427,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'], diff --git a/.claude/skills/meta-validate/scripts/meta-validate.ps1 b/.claude/skills/meta-validate/scripts/meta-validate.ps1 index 114bcd470..0a9adff47 100644 --- a/.claude/skills/meta-validate/scripts/meta-validate.ps1 +++ b/.claude/skills/meta-validate/scripts/meta-validate.ps1 @@ -1,4 +1,4 @@ -# meta-validate v1.25 — Validate 1C metadata object structure +# meta-validate v1.26 — 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", + # Внешний источник данных и его таблица (корень файла таблицы — ). + "ExternalDataSource","Table" ) # Валидные типы метаданных без глубоких правил валидации — раньше падали как "Unrecognized" @@ -213,6 +215,9 @@ $generatedTypeCategories = @{ "DataProcessor" = @("Object","Manager") "DefinedType" = @("DefinedType") "ExternalDataSource" = @("Manager","TablesManager","CubesManager") + # Таблица внешнего источника: имя элемента трёхчастное (Префикс.Источник.Таблица), + # но проверка «имя оканчивается на .ИмяОбъекта» на нём работает как есть. + "Table" = @("Manager","Object","Ref","List","Record","RecordSet","RecordKey","RecordManager") } # Types that have NO InternalInfo / GeneratedType @@ -274,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" = @() @@ -296,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") @@ -1762,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 diff --git a/.claude/skills/meta-validate/scripts/meta-validate.py b/.claude/skills/meta-validate/scripts/meta-validate.py index 53852b51d..f012c82d9 100644 --- a/.claude/skills/meta-validate/scripts/meta-validate.py +++ b/.claude/skills/meta-validate/scripts/meta-validate.py @@ -1,4 +1,4 @@ -# meta-validate v1.25 — Validate 1C metadata object structure (Python port) +# meta-validate v1.26 — 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", + # Внешний источник данных и его таблица (корень файла таблицы —
). + "ExternalDataSource", "Table", ) # Валидные типы метаданных без глубоких правил валидации — раньше падали как "Unrecognized" @@ -259,6 +261,9 @@ generated_type_categories = { "DataProcessor": ["Object", "Manager"], "DefinedType": ["DefinedType"], "ExternalDataSource": ["Manager", "TablesManager", "CubesManager"], + # Таблица внешнего источника: имя элемента трёхчастное (Префикс.Источник.Таблица), + # но проверка «имя оканчивается на .ИмяОбъекта» на нём работает как есть. + "Table": ["Manager", "Object", "Ref", "List", "Record", "RecordSet", "RecordKey", "RecordManager"], } # Types that have NO InternalInfo / GeneratedType @@ -320,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": [], @@ -344,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"], @@ -1707,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.<Форма>')") diff --git a/docs/1c-config-objects-spec.md b/docs/1c-config-objects-spec.md index e33e69ac6..94614c3f8 100644 --- a/docs/1c-config-objects-spec.md +++ b/docs/1c-config-objects-spec.md @@ -1696,7 +1696,181 @@ XML-элемент: ``. Трёхуровневая вложенно --- -## 26. Различия версий платформы +## 26. Внешние источники данных (ExternalDataSources) + +XML-элемент: ``. Двухуровневая вложенность: источник → таблица → поле, +но **уровней каталогов только два**: + +``` +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 + + PG + ... + + AutomaticAndManaged + +``` + +`DataLockControlMode`: `Automatic` | `Managed` | `AutomaticAndManaged`. При конкретном значении +одноимённое свойство таблицы игнорируется; `AutomaticAndManaged` отдаёт решение таблицам. + +### 26.2. ChildObjects источника: Table (именем) + Function (целиком) + +Смешанный список: таблицы перечислены **именами** (их содержимое — в отдельных файлах), функции +лежат **полными узлами** прямо здесь. + +```xml + +
products
+ prices
+ + + total + + + true + + xs:decimal + ... + + public.f_total(&1, &2) + + +
+``` + +Свойств у функции шесть. **Параметров как объектов метаданных не существует**: формальные параметры +записываются прямо в выражении как `&1`, `&2`; необязательные — в фигурных скобках (`f(&1{, &2})`), +переменное число — `&n[]` (последним в списке). `ReturnValue=false` описывает процедуру, тогда +`` пуст. + +Куб (``) — четвёртый вид дочернего объекта источника, здесь не описан. + +### 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` | список | Ключевые поля, `` с полным путём | +| `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 + +0 +``` + +Тип значения — тип **ключевого** поля таблицы, а не ссылочный тип поля родителя. + +### 26.4. Скаляры против списков + +Путь внутри одинаковый, оборачивание разное: + +```xml +ExternalDataSource.PG.Table.products.Field.name + + + ExternalDataSource.PG.Table.products.Field.id + +``` + +Скаляры: `PresentationField`, `ParentField`, `DataVersionField`. +Списки ``: `KeyFields`, `InputByString`, `DataLockFields`. + +### 26.5. Поле таблицы + +26 свойств: канонический блок реквизита (см. §6.1) без `ChoiceFoldersAndItems`, `LinkByType`, +`Indexing`, `Use`, `FullTextSearch`, `DataHistory`, плюс три своих в конце: + +| Свойство | Тип | Описание | +|---|---|---| +| `NameInDataSource` | string | Имя колонки. В одинарных кавычках попадает в SQL как есть, без них — экранируется двойными при спецсимволах | +| `ReadOnly` | boolean | Поле не записывается (вычисляемые, автоинкрементные) | +| `AllowNull` | boolean | Допускает `NULL`; в форме показывается как «Не заполнено» | + +Типы поля: `Число`, `Строка`, `Дата`, `Булево`, `УникальныйИдентификатор`, `ДвоичныеДанные` и +ссылка на таблицу внешнего источника: + +```xml +cfg:ExternalDataSourceTableRef.PG.products +``` + +Префикс корневой `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-элемент: ``. Трёхуровневая вложенно --- -## 27. Сводная таблица: свойства по типам объектов +## 28. Сводная таблица: свойства по типам объектов | Свойство | Cat | Doc | Enum | Const | InfoReg | AccReg | AcctReg | CalcReg | CoA | CoCT | CoCaT | BP | Task | EP | DJ | Rep | DP | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| @@ -1782,9 +1956,11 @@ XML-элемент: ``. Трёхуровневая вложенно Сокращения: 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-элемент: ``. Трёхуровневая вложенно {ВидОбъекта}.{ИмяОбъекта}.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 категории Каждый объект метаданных содержит блок `` с элементами ``, описывающими платформенные типы. Ниже — эталонная таблица категорий по типам объектов (источник: выгрузки ACC 8.3.24, ERP 8.3.24). @@ -1830,14 +2011,18 @@ XML-элемент: ``. Трёхуровневая вложенно | 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`, а не от имени вида. У `` и `` блока `InternalInfo` нет вовсе — только атрибут `uuid`. + Примечание: TabularSection/TabularSectionRow генерируются динамически для каждой табличной части. ChartOfAccounts может иметь условные ExtDimensionTypes/ExtDimensionTypesRow (зависит от наличия `extDimensionTypes`). --- -## 30. Кодировка +## 31. Кодировка Все XML-файлы используют кодировку UTF-8 с BOM (байты `EF BB BF`): diff --git a/docs/1c-configuration-spec.md b/docs/1c-configuration-spec.md index dcd22f000..4b827d7f6 100644 --- a/docs/1c-configuration-spec.md +++ b/docs/1c-configuration-spec.md @@ -318,12 +318,13 @@ ClassId — фиксированные идентификаторы классо | `WSReference` | `WSReferenceManager`/Manager | | `DefinedType` | `DefinedType`/DefinedType | | `ExternalDataSource` | `ExternalDataSourceManager`/Manager, `ExternalDataSourceTablesManager`/TablesManager, `ExternalDataSourceCubesManager`/CubesManager | -| `ExternalDataSource` → `Table` | `ExternalDataSourceTableManager`/Manager, `ExternalDataSourceTableObject`/Object, `ExternalDataSourceTableRef`/Ref, `ExternalDataSourceTableList`/List, `ExternalDataSourceTableRecord`/Record, `ExternalDataSourceTableRecordSet`/RecordSet, `ExternalDataSourceTableRecordKey`/RecordKey, `ExternalDataSourceTableRecordManager`/RecordManager | +| `Table` | `ExternalDataSourceTableManager`/Manager, `ExternalDataSourceTableObject`/Object, `ExternalDataSourceTableRef`/Ref, `ExternalDataSourceTableList`/List, `ExternalDataSourceTableRecord`/Record, `ExternalDataSourceTableRecordSet`/RecordSet, `ExternalDataSourceTableRecordKey`/RecordKey, `ExternalDataSourceTableRecordManager`/RecordManager | -Таблица внешнего источника — единственный подчинённый объект со своим `` в отдельном файле; -имя элемента у неё **трёхчастное**: `<префикс>.<ИмяИсточника>.<ИмяТаблицы>` (например -`ExternalDataSourceTableRef.PG.eds_public_products`). У `` и `` блока `InternalInfo` нет — -только атрибут `uuid`. +`Table` — корневой элемент файла таблицы внешнего источника (`ExternalDataSources/<Источник>/Tables/<Имя>.xml`), +единственный подчинённый объект со своим `` в отдельном файле. Имя элемента у него +**трёхчастное**: `<префикс>.<ИмяИсточника>.<ИмяТаблицы>` (например +`ExternalDataSourceTableRef.PG.eds_public_products`), поэтому навыки-эмиттеры строят его инлайном, +а не из карты «вид → набор». У `` и `` блока `InternalInfo` нет — только атрибут `uuid`. Табличные части объектов несут собственную пару в своём ``: `<Тип>TabularSection.<Объект>.<ТЧ>`/TabularSection и `<Тип>TabularSectionRow.<Объект>.<ТЧ>`/TabularSectionRow. diff --git a/docs/1c-specs-index.md b/docs/1c-specs-index.md index 0077625e9..c4590e4e0 100644 --- a/docs/1c-specs-index.md +++ b/docs/1c-specs-index.md @@ -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-различия-версий-платформы) | --- diff --git a/tests/skills/cases/meta-compile/external-data-source.json b/tests/skills/cases/meta-compile/external-data-source.json new file mode 100644 index 000000000..ffa3a67c0 --- /dev/null +++ b/tests/skills/cases/meta-compile/external-data-source.json @@ -0,0 +1,71 @@ +{ + "name": "Внешний источник данных: таблицы, поля, функция", + "input": { + "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", + "dataVersionField": "row_ver", + "transactionsIsolationLevel": "ReadUncommitted", + "dataLockFields": ["id"], + "fields": [ + "id: Number(10,0)", + "name: String(150)", + "article: String(50) | nullable", + "parent_id: ExternalDataSourceTableRef.PG.products | nullable", + "row_ver: Number(19,0) | readonly" + ] + }, + "byParent": { + "tableType": "Expression", + "expressionInDataSource": "public.f_products_by_parent(&1)", + "fields": ["id: Number(10,0)", "name: String(150)"] + } + }, + "functions": { + "total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" }, + "nextKey": "NEXT VALUE FOR public.seq_key" + } + }, + "validatePath": "ExternalDataSources/PG", + "expect": { + "files": [ + "ExternalDataSources/PG.xml", + "ExternalDataSources/PG/Tables/products.xml", + "ExternalDataSources/PG/Tables/prices.xml", + "ExternalDataSources/PG/Tables/byParent.xml" + ], + "fileContains": [ + { + "file": "ExternalDataSources/PG/Tables/products.xml", + "text": [ + "cfg:ExternalDataSourceTableRef.PG.products", + "ExternalDataSource.PG.Table.products.Field.id", + "ExternalDataSource.PG.Table.products.Field.name", + "", + "ReadUncommitted", + "eds.public.products" + ] + }, + { + "file": "ExternalDataSources/PG/Tables/byParent.xml", + "text": ["Expression", "", "public.f_products_by_parent(&1)"] + }, + { + "file": "ExternalDataSources/PG.xml", + "text": [ + "AutomaticAndManaged", + "products
", + "public.f_total(&1, &2)" + ] + } + ] + } +} diff --git a/tests/skills/cases/meta-compile/snapshots/external-data-source/Configuration.xml b/tests/skills/cases/meta-compile/snapshots/external-data-source/Configuration.xml new file mode 100644 index 000000000..da0e784ee --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/external-data-source/Configuration.xml @@ -0,0 +1,252 @@ + + + + + + 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 + + + + TestConfig + + + ru + TestConfig + + + + + Version8_3_24 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + NFC + false + + + DocumentScanning + false + + + SpeechToText + false + + + Geofences + false + + + IncomingShareRequests + false + + + AllIncomingShareRequestsTypesProcessing + false + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + Version8_3_24 + + + + Русский + PG + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/external-data-source/Ext/ClientApplicationInterface.xml b/tests/skills/cases/meta-compile/snapshots/external-data-source/Ext/ClientApplicationInterface.xml new file mode 100644 index 000000000..3c1161b2d --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/external-data-source/Ext/ClientApplicationInterface.xml @@ -0,0 +1,18 @@ + + + + + UUID-002 + + + + + UUID-004 + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG.xml b/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG.xml new file mode 100644 index 000000000..4c3307c35 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG.xml @@ -0,0 +1,68 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + + PG + + + ru + PG + + + + AutomaticAndManaged + + + prices
+ products
+ byParent
+ + + 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-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/byParent.xml b/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/byParent.xml new file mode 100644 index 000000000..bfe3060b3 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/byParent.xml @@ -0,0 +1,169 @@ + + + + + + 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 + + + + byParent + + + ru + by parent + + + + Expression + + public.f_products_by_parent(&1) + NonobjectData + + + + + + true + false + + Auto + Begin + Directly + Auto + + + + + + + + + + + + false + false + Auto + + 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 + + + +
+
\ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml b/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml new file mode 100644 index 000000000..316128161 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml @@ -0,0 +1,210 @@ + + + + + + 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 + + + + + period + + + ru + period + + + + + xs:dateTime + + Date + + + false + + + + false + + false + false + + + false + + DontCheck + + + Auto + Auto + Auto + + period + 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-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml b/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml new file mode 100644 index 000000000..008933184 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml @@ -0,0 +1,295 @@ + + + + + + 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 + ExternalDataSource.PG.Table.products.Field.parent_id + + + true + false + + ExternalDataSource.PG.Table.products.Field.name + + Auto + Begin + Directly + Auto + + + + + + + + + + + + false + false + ReadUncommitted + ExternalDataSource.PG.Table.products.Field.row_ver + InDialog + + + ExternalDataSource.PG.Table.products.Field.id + + 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 + + + + + article + + + ru + article + + + + + xs:string + + 50 + Variable + + + false + + + + false + + false + false + + + false + + DontCheck + + + Auto + Auto + Auto + + article + false + true + + + + + parent_id + + + ru + parent_id + + + + + cfg:ExternalDataSourceTableRef.PG.products + + false + + + + false + + false + false + + + false + + DontCheck + + + Auto + Auto + Auto + + parent_id + false + true + + + + + row_ver + + + ru + row_ver + + + + + xs:decimal + + 19 + 0 + Any + + + false + + + + false + + false + false + + + false + 0 + DontCheck + + + Auto + Auto + Auto + + row_ver + true + false + + + +
+
\ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/external-data-source/Languages/Русский.xml b/tests/skills/cases/meta-compile/snapshots/external-data-source/Languages/Русский.xml new file mode 100644 index 000000000..37c60d786 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/external-data-source/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/tests/skills/check-type-maps.mjs b/tests/skills/check-type-maps.mjs index c72fe9b46..f4d3d88ca 100644 --- a/tests/skills/check-type-maps.mjs +++ b/tests/skills/check-type-maps.mjs @@ -83,17 +83,20 @@ const MAPS = [ { skill: 'cfe-borrow', file: 'cfe-borrow', kind: 'gentypes', py: 'GENERATED_TYPES', ps1: '$script:generatedTypes', + exclude: { Table: 'навык не работает с таблицами внешних источников' }, }, { skill: 'meta-compile', file: 'meta-compile', kind: 'gentypes', py: 'generated_types', ps1: '$script:generatedTypes', exclude: { IntegrationService: 'навык не создаёт объекты этого вида — генерируемых типов для него нет', + Table: 'InternalInfo таблицы строится инлайном: имя элемента трёхчастное (префикс.Источник.Таблица), карта «вид → набор» такой формы не выражает', }, }, { skill: 'cfe-validate', file: 'cfe-validate', kind: 'gencats', py: 'GENERATED_TYPE_CATEGORIES', ps1: '$generatedTypeCategories', + exclude: { Table: 'навык не работает с таблицами внешних источников' }, }, { skill: 'meta-validate', file: 'meta-validate', kind: 'gencats',