From c859ca1c59c1280a608210b3611f88b4804f91b6 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 28 Oct 2026 22:15:23 +0400 Subject: [PATCH] =?UTF-8?q?fix(realtime):=20getDataRowId=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B0=D0=B5=D1=82=20null=20?= =?UTF-8?q?=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20undefined,=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D1=8B=20M-8/M-9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit После jsonb_strip_nulls у групп/позиций нет ключа line_id, getDataRowId возвращал undefined, и row_id в сообщении удаления сериализовался в {} (KeyError на сервере). Возвращаем null, как до фазы B. Добавлены тесты updateCells (глубокое отсутствие блока, два обновления в батче) и сверка jsonb_strip_nulls с _strip_nulls. Co-Authored-By: Claude Opus 5.5 --- api/tests/integration/test_heavy_get_json.py | 13 +++++++++++++ .../RealtimeTable/utils/cellUtils.test.js | 16 ++++++++++++++++ .../components/RealtimeTable/utils/rowUtils.js | 2 +- .../RealtimeTable/utils/rowUtils.test.js | 9 ++++++++- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/api/tests/integration/test_heavy_get_json.py b/api/tests/integration/test_heavy_get_json.py index 612acaf..aa63a28 100644 --- a/api/tests/integration/test_heavy_get_json.py +++ b/api/tests/integration/test_heavy_get_json.py @@ -61,6 +61,19 @@ def _strip_nulls(value): return value +def test_pg_jsonb_strip_nulls_matches_strip_nulls(client, db_session): + literal = '{"a":{"b":null},"c":[null,{"d":null,"e":1},[{"f":null}]],"g":null,"h":{}}' + + async def _go(): + res = await db_session.execute(text("SELECT jsonb_strip_nulls(CAST(:v AS jsonb))"), {"v": literal}) + return res.scalar_one() + pg = client.portal.call(_go) + if isinstance(pg, str): + pg = json.loads(pg) + assert pg == _strip_nulls(json.loads(literal)) + assert pg == {"a": {}, "c": [None, {"e": 1}, [{}]], "h": {}} + + def _has_null_field(value) -> bool: if isinstance(value, dict): return any(v is None or _has_null_field(v) for v in value.values()) diff --git a/web/src/components/RealtimeTable/utils/cellUtils.test.js b/web/src/components/RealtimeTable/utils/cellUtils.test.js index a7f849e..92bc357 100644 --- a/web/src/components/RealtimeTable/utils/cellUtils.test.js +++ b/web/src/components/RealtimeTable/utils/cellUtils.test.js @@ -229,3 +229,19 @@ test('AURORA-1848: отсутствующий скалярный ключ вер const out = updateCells(data, [['INPUT', 3, 1, { line_id: 9 }]]); assert.equal(out[0].data.line_id, 9); }); + +test('AURORA-1848: блок отсутствует на двух уровнях вглубь — создаётся цепочка, вход не меняется', () => { + const data = deepFreeze([row({ line_id: 7 })]); + const out = updateCells(data, [['INPUT', 3, 1, { plan: { support: { q1: 5 } } }]]); + assert.equal(out[0].data.plan.support.q1, 5); + assert.deepEqual(data[0].data, { line_id: 7 }); +}); + +test('AURORA-1848: два обновления одной строки в одном батче складываются', () => { + const data = deepFreeze([row({ line_id: 7 })]); + const out = updateCells(data, [ + ['INPUT', 3, 1, { q1: { m1: 5 } }], + ['INPUT', 3, 1, { q1: { m2: 3 } }], + ]); + assert.deepEqual(out[0].data.q1, { m1: 5, m2: 3 }); +}); diff --git a/web/src/components/RealtimeTable/utils/rowUtils.js b/web/src/components/RealtimeTable/utils/rowUtils.js index af5efcb..cc3ce5d 100644 --- a/web/src/components/RealtimeTable/utils/rowUtils.js +++ b/web/src/components/RealtimeTable/utils/rowUtils.js @@ -30,7 +30,7 @@ export function buildRowHierarchy(items, mapItem = (item) => item) { } export function getDataRowId(data) { - return data?.id || data?.line_id; + return data?.id || data?.line_id || null; } export function getRowId(row) { diff --git a/web/src/components/RealtimeTable/utils/rowUtils.test.js b/web/src/components/RealtimeTable/utils/rowUtils.test.js index 0c40434..7d45956 100644 --- a/web/src/components/RealtimeTable/utils/rowUtils.test.js +++ b/web/src/components/RealtimeTable/utils/rowUtils.test.js @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { buildRowHierarchy } from './rowUtils.js'; +import { buildRowHierarchy, getDataRowId } from './rowUtils.js'; test('row hierarchy uses row types when depth skips a level and keeps INPUT rows as siblings', () => { const rows = [ @@ -19,3 +19,10 @@ test('row hierarchy uses row types when depth skips a level and keeps INPUT rows assert.deepEqual(tree[0].subRows[0].subRows.map(({ id }) => id), ['input-1', 'input-2']); assert.deepEqual(tree[0].subRows[0].subRows[0].subRows, []); }); + +test('AURORA-1848: getDataRowId без id и line_id возвращает null и сериализуется в row_id:null', () => { + assert.equal(getDataRowId({ header: {} }), null); + assert.equal(getDataRowId({ id: 5 }), 5); + assert.equal(getDataRowId({ line_id: 7 }), 7); + assert.ok(JSON.stringify({ row_id: getDataRowId({ header: {} }) }).includes('"row_id":null')); +});