From e300d4ea8be3ec842d0842929dc01a204e360931 Mon Sep 17 00:00:00 2001 From: "Zimin A.N." Date: Wed, 6 May 2026 17:29:42 +0300 Subject: [PATCH] fix(schema-editor): preserve nested objects via 'opaque' kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: parseSchemaJson fallback на line 196 возвращал {kind: 'string'} для unknown types (object с nested properties, array of objects, etc.). При save back to schema это давало lossy round-trip: spacecraft.orbit (object с 7 nested fields) → parseSchemaJson → kind: 'string' → propertyToSchema → {type: 'string'} → diffSchemas → BREAKING CHANGE 'object → string' → save blocked Симптом видел user: edit modal spacecraft показывает 'НЕСОВМЕСТИМОЕ ИЗМЕНЕНИЕ СХЕМЫ' для orbit, save заблокирован, хотя admin не трогал orbit. Fix: - Новый PropertyKind 'opaque' для unknown types - PropertyDef.rawSchema (optional JsonSchema) хранит оригинал - parseSchemaJson fallback теперь {kind:'opaque', rawSchema:raw} - propertyToSchema для opaque возвращает rawSchema verbatim (с description override если admin менял в Поля-табе) - PropertyEditor: 'opaque' исключён из Type dropdown (admin не может вручную выбрать), для existing opaque поля показывается warning card с raw JSON read-only + hint что редактируется через JSON-таб Test coverage: 73 tests passing (5 + 10 SchemaBuilder + 7 + ...). Эффект: edit любого справочника с nested objects (orbit) больше не триггерит false-positive breaking change. i18n: schema.kind.opaque + schema.opaque.{title,hint} × ru-RU/en-US. --- .../src/components/schema/PropertyEditor.tsx | 27 ++++++++++++++++--- .../src/components/schema/types.ts | 17 +++++++++++- ordinis-admin-ui/src/i18n.ts | 6 +++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/ordinis-admin-ui/src/components/schema/PropertyEditor.tsx b/ordinis-admin-ui/src/components/schema/PropertyEditor.tsx index 8592986..93cde57 100644 --- a/ordinis-admin-ui/src/components/schema/PropertyEditor.tsx +++ b/ordinis-admin-ui/src/components/schema/PropertyEditor.tsx @@ -32,10 +32,15 @@ export const PropertyEditor = ({ }: Props) => { const { t } = useTranslation() - const kindOptions: SelectOption[] = PROPERTY_KINDS.map((k) => ({ - id: k, - label: t(`schema.kind.${k}`), - })) + // 'opaque' исключён из dropdown — admin не может вручную выбрать сложный + // тип. Поле получает opaque только через parseSchemaJson (когда уже было + // в исходной schema). Для NEW поля admin использует обычные kinds. + const kindOptions: SelectOption[] = PROPERTY_KINDS + .filter((k) => k !== 'opaque') + .map((k) => ({ + id: k, + label: t(`schema.kind.${k}`), + })) const update = (patch: Partial) => onChange({ ...prop, ...patch }) @@ -108,6 +113,20 @@ export const PropertyEditor = ({ onChange={(e) => update({ description: e.target.value })} /> + {prop.kind === 'opaque' && ( +
+
+ {t('schema.opaque.title')} +
+
+ {t('schema.opaque.hint')} +
+
+            {JSON.stringify(prop.rawSchema ?? {}, null, 2)}
+          
+
+ )} + {(prop.kind === 'string') && (
{ ...(p.uniqueItems ? { uniqueItems: true } : {}), ...uniqueExt, } as JsonSchema + case 'opaque': + // Round-trip raw schema без изменений. Description можно overwrite + // если admin отредактировал в metadata-табе, остальное as-is. + return p.rawSchema + ? (desc !== undefined && desc !== p.rawSchema.description + ? { ...p.rawSchema, description: desc } + : p.rawSchema) + : { type: 'string', description: desc } } } @@ -193,7 +205,10 @@ const inferKind = ( uniqueItems: Boolean((raw as { uniqueItems?: boolean }).uniqueItems), } } - return { ...base, kind: 'string' } + // Fallback: nested objects, complex arrays, unknown types — preserve raw + // schema полностью. Lossy round-trip (object → string fallback) ломал + // breaking-change detector + терял nested properties (см. spacecraft.orbit). + return { ...base, kind: 'opaque', rawSchema: raw } } // === schema diff (breaking detection) === diff --git a/ordinis-admin-ui/src/i18n.ts b/ordinis-admin-ui/src/i18n.ts index 4b9d35e..10247e0 100644 --- a/ordinis-admin-ui/src/i18n.ts +++ b/ordinis-admin-ui/src/i18n.ts @@ -210,6 +210,9 @@ i18n 'schema.kind.date': 'Дата', 'schema.kind.datetime': 'Дата+время', 'schema.kind.array_string': 'Массив строк', + 'schema.kind.opaque': 'Сложный (JSON)', + 'schema.opaque.title': 'Сложное поле — редактируется только через JSON-таб', + 'schema.opaque.hint': 'Nested object / array of objects. Поля-табе нет редактора, raw schema показан ниже. Меняй через вкладку "JSON".', 'field.name': 'Наименование', 'field.designator': 'Designator (COSPAR)', 'field.norad_id': 'NORAD ID', @@ -432,6 +435,9 @@ i18n 'schema.kind.date': 'Date', 'schema.kind.datetime': 'Date+time', 'schema.kind.array_string': 'Array of strings', + 'schema.kind.opaque': 'Complex (JSON)', + 'schema.opaque.title': 'Complex field — edit only via JSON tab', + 'schema.opaque.hint': 'Nested object / array of objects. No editor in Fields tab, raw schema shown below. Edit via JSON tab.', 'field.name': 'Name', 'field.designator': 'Designator (COSPAR)', 'field.norad_id': 'NORAD ID',