mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-20 09:30:59 +03:00
feat(skd): support @file references for query text in skd-compile and skd-edit
Allows using "@path/to/file.sql" instead of inline query text. Path resolved relative to definition file, then CWD; absolute paths supported. Closes #9 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ffa3189442
commit
05fc7eba27
@@ -56,6 +56,8 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
|
|||||||
{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...] }
|
{ "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...] }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Запрос поддерживает `@file` — ссылку на внешний .sql файл вместо inline-текста: `"query": "@queries/sales.sql"`. Путь разрешается относительно JSON-файла, затем CWD.
|
||||||
|
|
||||||
### Поля — shorthand
|
### Поля — shorthand
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -199,6 +201,17 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### С запросом из внешнего файла (@file)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dataSets": [{
|
||||||
|
"query": "@queries/sales.sql",
|
||||||
|
"fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### С ресурсами, параметрами и @autoDates
|
### С ресурсами, параметрами и @autoDates
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# skd-compile v1.0 — Compile 1C DCS from JSON
|
# skd-compile v1.1 — Compile 1C DCS from JSON
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[string]$DefinitionFile,
|
[string]$DefinitionFile,
|
||||||
@@ -41,6 +41,9 @@ if (-not $def.dataSets -or $def.dataSets.Count -eq 0) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Base directory for resolving @file references in query
|
||||||
|
$script:queryBaseDir = if ($DefinitionFile) { [System.IO.Path]::GetDirectoryName($DefinitionFile) } else { (Get-Location).Path }
|
||||||
|
|
||||||
# --- 2. XML helpers ---
|
# --- 2. XML helpers ---
|
||||||
|
|
||||||
$script:xml = New-Object System.Text.StringBuilder 16384
|
$script:xml = New-Object System.Text.StringBuilder 16384
|
||||||
@@ -55,6 +58,27 @@ function Esc-Xml {
|
|||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Resolve-QueryValue {
|
||||||
|
param([string]$val, [string]$baseDir)
|
||||||
|
if (-not $val.StartsWith("@")) { return $val }
|
||||||
|
$filePath = $val.Substring(1)
|
||||||
|
if ([System.IO.Path]::IsPathRooted($filePath)) {
|
||||||
|
$candidates = @($filePath)
|
||||||
|
} else {
|
||||||
|
$candidates = @(
|
||||||
|
(Join-Path $baseDir $filePath),
|
||||||
|
(Join-Path (Get-Location).Path $filePath)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
foreach ($c in $candidates) {
|
||||||
|
if (Test-Path $c) {
|
||||||
|
return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
function Emit-MLText {
|
function Emit-MLText {
|
||||||
param([string]$tag, [string]$text, [string]$indent)
|
param([string]$tag, [string]$text, [string]$indent)
|
||||||
X "$indent<$tag xsi:type=`"v8:LocalStringType`">"
|
X "$indent<$tag xsi:type=`"v8:LocalStringType`">"
|
||||||
@@ -672,7 +696,8 @@ function Emit-DataSet {
|
|||||||
|
|
||||||
# Type-specific content
|
# Type-specific content
|
||||||
if ($dsType -eq "DataSetQuery") {
|
if ($dsType -eq "DataSetQuery") {
|
||||||
X "$indent`t<query>$(Esc-Xml "$($ds.query)")</query>"
|
$queryText = Resolve-QueryValue "$($ds.query)" $script:queryBaseDir
|
||||||
|
X "$indent`t<query>$(Esc-Xml $queryText)</query>"
|
||||||
if ($ds.autoFillFields -eq $false) {
|
if ($ds.autoFillFields -eq $false) {
|
||||||
X "$indent`t<autoFillFields>false</autoFillFields>"
|
X "$indent`t<autoFillFields>false</autoFillFields>"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# skd-compile v1.0 — Compile 1C DCS from JSON
|
# skd-compile v1.1 — Compile 1C DCS from JSON
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
@@ -13,6 +13,25 @@ def esc_xml(s):
|
|||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_query_value(val, base_dir):
|
||||||
|
if not val.startswith("@"):
|
||||||
|
return val
|
||||||
|
file_path = val[1:]
|
||||||
|
if os.path.isabs(file_path):
|
||||||
|
candidates = [file_path]
|
||||||
|
else:
|
||||||
|
candidates = [
|
||||||
|
os.path.join(base_dir, file_path),
|
||||||
|
os.path.join(os.getcwd(), file_path),
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if os.path.exists(c):
|
||||||
|
with open(c, 'r', encoding='utf-8-sig') as f:
|
||||||
|
return f.read().rstrip()
|
||||||
|
print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def emit_mltext(lines, indent, tag, text):
|
def emit_mltext(lines, indent, tag, text):
|
||||||
if not text:
|
if not text:
|
||||||
lines.append(f"{indent}<{tag}/>")
|
lines.append(f"{indent}<{tag}/>")
|
||||||
@@ -530,7 +549,8 @@ def emit_data_set(lines, ds, indent, default_source):
|
|||||||
|
|
||||||
# Type-specific content
|
# Type-specific content
|
||||||
if ds_type == 'DataSetQuery':
|
if ds_type == 'DataSetQuery':
|
||||||
lines.append(f'{indent}\t<query>{esc_xml(str(ds.get("query", "")))}</query>')
|
query_text = resolve_query_value(str(ds.get("query", "")), query_base_dir)
|
||||||
|
lines.append(f'{indent}\t<query>{esc_xml(query_text)}</query>')
|
||||||
if ds.get('autoFillFields') is False:
|
if ds.get('autoFillFields') is False:
|
||||||
lines.append(f'{indent}\t<autoFillFields>false</autoFillFields>')
|
lines.append(f'{indent}\t<autoFillFields>false</autoFillFields>')
|
||||||
elif ds_type == 'DataSetObject':
|
elif ds_type == 'DataSetObject':
|
||||||
@@ -1351,6 +1371,10 @@ def main():
|
|||||||
print("JSON must have at least one entry in 'dataSets'", file=sys.stderr)
|
print("JSON must have at least one entry in 'dataSets'", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Base directory for resolving @file references in query
|
||||||
|
global query_base_dir
|
||||||
|
query_base_dir = os.path.dirname(def_file) if args.DefinitionFile else os.getcwd()
|
||||||
|
|
||||||
# --- 2. Resolve defaults ---
|
# --- 2. Resolve defaults ---
|
||||||
|
|
||||||
# DataSources
|
# DataSources
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ Shorthand: `"Имя: ТЕКСТ_ЗАПРОСА"` или `"ТЕКСТ_ЗАПРО
|
|||||||
```
|
```
|
||||||
"Доп: ВЫБРАТЬ 1 КАК Тест"
|
"Доп: ВЫБРАТЬ 1 КАК Тест"
|
||||||
"ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура"
|
"ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура"
|
||||||
|
"Продажи: @queries/sales.sql"
|
||||||
```
|
```
|
||||||
|
|
||||||
`dataSource` берётся из первого существующего. Дубликат имени — предупреждение, пропуск. Не поддерживает пакетный режим (запрос может содержать `;;`).
|
`dataSource` берётся из первого существующего. Дубликат имени — предупреждение, пропуск. Не поддерживает пакетный режим (запрос может содержать `;;`).
|
||||||
@@ -160,7 +161,7 @@ Shorthand: `"Параметр = значение [when условие] [for По
|
|||||||
|
|
||||||
### set-query — заменить текст запроса
|
### set-query — заменить текст запроса
|
||||||
|
|
||||||
Не поддерживает пакетный режим. Value — полный текст запроса.
|
Не поддерживает пакетный режим. Value — полный текст запроса или `@path/to/file.sql` (ссылка на внешний файл). Путь разрешается относительно Template.xml, затем CWD.
|
||||||
|
|
||||||
### set-outputParameter — установить параметр вывода
|
### set-outputParameter — установить параметр вывода
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# skd-edit v1.1 — Atomic 1C DCS editor
|
# skd-edit v1.2 — Atomic 1C DCS editor
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -47,6 +47,29 @@ function Esc-Xml {
|
|||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Resolve-QueryValue {
|
||||||
|
param([string]$val, [string]$baseDir)
|
||||||
|
if (-not $val.StartsWith("@")) { return $val }
|
||||||
|
$filePath = $val.Substring(1)
|
||||||
|
if ([System.IO.Path]::IsPathRooted($filePath)) {
|
||||||
|
$candidates = @($filePath)
|
||||||
|
} else {
|
||||||
|
$candidates = @(
|
||||||
|
(Join-Path $baseDir $filePath),
|
||||||
|
(Join-Path (Get-Location).Path $filePath)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
foreach ($c in $candidates) {
|
||||||
|
if (Test-Path $c) {
|
||||||
|
return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:queryBaseDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||||
|
|
||||||
# --- 2. Type system (copied from skd-compile) ---
|
# --- 2. Type system (copied from skd-compile) ---
|
||||||
|
|
||||||
$script:typeSynonyms = New-Object System.Collections.Hashtable
|
$script:typeSynonyms = New-Object System.Collections.Hashtable
|
||||||
@@ -1712,7 +1735,7 @@ switch ($Operation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# InnerText setter handles XML escaping automatically
|
# InnerText setter handles XML escaping automatically
|
||||||
$queryEl.InnerText = $Value
|
$queryEl.InnerText = Resolve-QueryValue $Value $script:queryBaseDir
|
||||||
|
|
||||||
Write-Host "[OK] Query replaced in dataset `"$dsName`""
|
Write-Host "[OK] Query replaced in dataset `"$dsName`""
|
||||||
}
|
}
|
||||||
@@ -1813,6 +1836,7 @@ switch ($Operation) {
|
|||||||
$childIndent = Get-ChildIndent $root
|
$childIndent = Get-ChildIndent $root
|
||||||
|
|
||||||
$parsed = Parse-DataSetShorthand $Value
|
$parsed = Parse-DataSetShorthand $Value
|
||||||
|
$parsed.query = Resolve-QueryValue $parsed.query $script:queryBaseDir
|
||||||
|
|
||||||
# Auto-name if empty
|
# Auto-name if empty
|
||||||
if (-not $parsed.name) {
|
if (-not $parsed.name) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# skd-edit v1.1 — Atomic 1C DCS editor (Python port)
|
# skd-edit v1.2 — Atomic 1C DCS editor (Python port)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
@@ -78,6 +78,25 @@ def esc_xml(s):
|
|||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_query_value(val, base_dir):
|
||||||
|
if not val.startswith("@"):
|
||||||
|
return val
|
||||||
|
file_path = val[1:]
|
||||||
|
if os.path.isabs(file_path):
|
||||||
|
candidates = [file_path]
|
||||||
|
else:
|
||||||
|
candidates = [
|
||||||
|
os.path.join(base_dir, file_path),
|
||||||
|
os.path.join(os.getcwd(), file_path),
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if os.path.exists(c):
|
||||||
|
with open(c, 'r', encoding='utf-8-sig') as f:
|
||||||
|
return f.read().rstrip()
|
||||||
|
print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -94,6 +113,7 @@ if not os.path.exists(template_path):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
resolved_path = os.path.abspath(template_path)
|
resolved_path = os.path.abspath(template_path)
|
||||||
|
query_base_dir = os.path.dirname(resolved_path)
|
||||||
|
|
||||||
# ── 2. Type system ──────────────────────────────────────────
|
# ── 2. Type system ──────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1465,7 +1485,7 @@ elif operation == "set-query":
|
|||||||
if query_el is None:
|
if query_el is None:
|
||||||
print(f"No <query> element found in dataset '{ds_name}'", file=sys.stderr)
|
print(f"No <query> element found in dataset '{ds_name}'", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
query_el.text = value_arg
|
query_el.text = resolve_query_value(value_arg, query_base_dir)
|
||||||
print(f'[OK] Query replaced in dataset "{ds_name}"')
|
print(f'[OK] Query replaced in dataset "{ds_name}"')
|
||||||
|
|
||||||
elif operation == "set-outputParameter":
|
elif operation == "set-outputParameter":
|
||||||
@@ -1541,6 +1561,7 @@ elif operation == "add-dataSetLink":
|
|||||||
elif operation == "add-dataSet":
|
elif operation == "add-dataSet":
|
||||||
child_indent = get_child_indent(xml_doc)
|
child_indent = get_child_indent(xml_doc)
|
||||||
parsed = parse_data_set_shorthand(value_arg)
|
parsed = parse_data_set_shorthand(value_arg)
|
||||||
|
parsed["query"] = resolve_query_value(parsed["query"], query_base_dir)
|
||||||
|
|
||||||
if not parsed["name"]:
|
if not parsed["name"]:
|
||||||
count = sum(1 for ch in xml_doc if isinstance(ch.tag, str) and local_name(ch) == "dataSet" and etree.QName(ch.tag).namespace == SCH_NS)
|
count = sum(1 for ch in xml_doc if isinstance(ch.tag, str) and local_name(ch) == "dataSet" and etree.QName(ch.tag).namespace == SCH_NS)
|
||||||
|
|||||||
+15
-1
@@ -85,12 +85,26 @@
|
|||||||
|------|---------|----------|
|
|------|---------|----------|
|
||||||
| `name` | нет | Авто: "НаборДанных1"... |
|
| `name` | нет | Авто: "НаборДанных1"... |
|
||||||
| `source` | нет | Имя dataSource (авто: первый) |
|
| `source` | нет | Имя dataSource (авто: первый) |
|
||||||
| `query` | да* | Текст запроса (DataSetQuery) |
|
| `query` | да* | Текст запроса (DataSetQuery). Поддерживает `@file` — см. ниже |
|
||||||
| `objectName` | да* | Имя объекта (DataSetObject) |
|
| `objectName` | да* | Имя объекта (DataSetObject) |
|
||||||
| `items` | да* | Вложенные наборы (DataSetUnion) |
|
| `items` | да* | Вложенные наборы (DataSetUnion) |
|
||||||
| `fields` | нет | Массив полей |
|
| `fields` | нет | Массив полей |
|
||||||
| `autoFillFields` | нет | `false` — отключить автозаполнение (по умолчанию не выводится = true) |
|
| `autoFillFields` | нет | `false` — отключить автозаполнение (по умолчанию не выводится = true) |
|
||||||
|
|
||||||
|
### Ссылка на внешний файл запроса (@file)
|
||||||
|
|
||||||
|
Вместо inline-текста запроса можно указать путь к внешнему файлу с префиксом `@`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "query": "@queries/sales.sql" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Порядок разрешения пути:
|
||||||
|
1. Абсолютный путь — используется как есть
|
||||||
|
2. Относительно директории JSON-файла определения
|
||||||
|
3. Относительно текущей рабочей директории (CWD)
|
||||||
|
4. Если файл не найден — ошибка компиляции
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Поля — shorthand и объектная форма
|
## 4. Поля — shorthand и объектная форма
|
||||||
|
|||||||
+1
-1
@@ -125,7 +125,7 @@ Claude вызовет `/skd-info` (overview → trace → query → variant) и
|
|||||||
> поля Номенклатура, Количество, Сумма. Период — параметр.
|
> поля Номенклатура, Количество, Сумма. Период — параметр.
|
||||||
```
|
```
|
||||||
|
|
||||||
Claude сформирует JSON:
|
Claude сформирует JSON (запрос можно вынести в файл: `"query": "@queries/sales.sql"`):
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"dataSets": [{
|
"dataSets": [{
|
||||||
|
|||||||
Reference in New Issue
Block a user