Repository: mathesar-foundation/mathesar

Issue:
Improve the structure of the record summary data, as returned from the records API
Here is a sample response from the records API

```json5
{
  "count": 16,
  "grouping": { /* ... */ },
  "preview_data": [
    {
      "template": "{23__7__col__8}",
      "column": 23,

      // CURRENT VALUE RETURNED
      "data": [
        { "23__7__col__8": "Rocha PLC" },
        { "23__7__col__8": "Rocha PLC" },
        { "23__7__col__8": "Rocha PLC" },
        { "23__7__col__8": "Rocha PLC" },
        { "23__7__col__8": "Stokes, Campos and Rich" },
        { "23__7__col__8": "Stokes, Campos and Rich" },
        { "23__7__col__8": "Stokes, Campos and Rich" },
        { "23__7__col__8": "Wright LLC" },
        { "23__7__col__8": "Wright LLC" },
      ],

      // PROPOSAL FROM SEAN
      "data": {
      // 👇  These are primary key values of linked records 
        "77": { "23__7__col__8": "Rocha PLC" },
        "88": { "23__7__col__8": "Stokes, Campos and Rich" },
        "99": { "23__7__col__8": "Wright LLC" }
      //        👆 This is the data necessary to render the record summary
      },

      // PROPOSAL FROM MUKESH
      "data": [
        { "primary_key": 77, "data": { "23__7__col__8": "Rocha PLC" }},
        { "primary_key": 88, "data": { "23__7__col__8": "Stokes, Campos and Rich" }},
        { "primary_key": 99, "data": { "23__7__col__8": "Wright LLC" }}
      ]
    }
  ],
  "results": [ /* ... */ ]
}
```

I would like the `data` property within the `preview_data` blob to be changed to the structure as indicated above under the "PROPOSAL FROM SEAN". With this structure, I can use the primary key value of the linked record (e.g. "77") to look up the data (e.g. `{ "23__7__col__8": "Rocha PLC" }`) necessary to render the record summary for that linked record.

I've included the "PROPOSAL FROM MUKESH" as a side note because this is an alternate structure we considered briefly but decided against.

@mathemancer and I had a call today where we decided to move forward with modifying the API to fit the "PROPOSAL FROM SEAN".



Discussion:

Resolving patch:
diff --git a/mathesar/api/db/viewsets/records.py b/mathesar/api/db/viewsets/records.py
--- a/mathesar/api/db/viewsets/records.py
+++ b/mathesar/api/db/viewsets/records.py
@@ -112,12 +112,11 @@ def retrieve(self, request, pk=None, table_pk=None):
             ]
         }
         column_names_to_ids = table.get_column_name_id_bidirectional_map()
-        column_ids_to_names = column_names_to_ids.inverse
         records = paginator.paginate_queryset(
             table,
             request,
             table,
-            column_ids_to_names,
+            column_names_to_ids,
             filters=record_filters
         )
         if not records:
@@ -146,12 +145,11 @@ def create(self, request, table_pk=None):
             ]
         }
         column_names_to_ids = table.get_column_name_id_bidirectional_map()
-        column_ids_to_names = column_names_to_ids.inverse
         records = paginator.paginate_queryset(
             table,
             request,
             table,
-            column_ids_to_names,
+            column_names_to_ids,
             filters=record_filters
         )
         serializer = RecordSerializer(
@@ -182,12 +180,11 @@ def partial_update(self, request, pk=None, table_pk=None):
             ]
         }
         column_names_to_ids = table.get_column_name_id_bidirectional_map()
-        column_ids_to_names = column_names_to_ids.inverse
         records = paginator.paginate_queryset(
             table,
             request,
             table,
-            column_ids_to_names,
+            column_names_to_ids,
             filters=record_filters
         )
         serializer = RecordSerializer(
diff --git a/mathesar/api/utils.py b/mathesar/api/utils.py
--- a/mathesar/api/utils.py
+++ b/mathesar/api/utils.py
@@ -71,7 +71,9 @@ def _use_correct_column_identifier(group_metadata_item):
             return _replace_column_names_with_ids(group_metadata_item)
         else:
             return group_metadata_item
-    if preview_metadata:
+
+    if preview_metadata and column_name_id_map is not None:
+        column_id_name_map = {i: n for n, i in column_name_id_map.items()}
         # Extract preview data from the records
         # TODO Replace modifying the parameter directly
         for preview_colum_id, preview_info in preview_metadata.items():
@@ -81,13 +83,13 @@ def _use_correct_column_identifier(group_metadata_item):
             # Move column id into the object so that dict can be flattened into a list
             preview_metadata[preview_colum_id]['column'] = preview_colum_id
             preview_data_column_aliases = column_alias_from_preview_template(preview_template)
-            preview_records = []
-            for record_index, record in enumerate(processed_records):
+            preview_records = {}
+            for record in processed_records:
                 column_preview_data = {}
                 for preview_data_column_alias in preview_data_column_aliases:
-                    preview_value = processed_records[record_index].pop(preview_data_column_alias)
+                    preview_value = record.pop(preview_data_column_alias)
                     column_preview_data.update({preview_data_column_alias: preview_value})
-                preview_records.append(column_preview_data)
+                preview_records[str(record[column_id_name_map[preview_colum_id]])] = column_preview_data
             preview_metadata[preview_colum_id]['data'] = preview_records
         # Flatten the preview objects
         preview_metadata = preview_metadata.values()


Test patch:
diff --git a/mathesar/tests/api/test_record_api.py b/mathesar/tests/api/test_record_api.py
--- a/mathesar/tests/api/test_record_api.py
+++ b/mathesar/tests/api/test_record_api.py
@@ -807,7 +807,7 @@ def test_foreign_key_record_api_all_column_previews(publication_tables, client):
     preview_column_alias = f'{{{publication_title_alias}}} Published By: {{{ publisher_name_alias}}} and Authored by Full Name: {{{author_first_name_alias}}} {{{author_last_name_alias}}} along with Full Name: {{{co_author_first_name_alias}}} {{{co_author_last_name_alias}}}'
 
     assert preview_column['template'] == preview_column_alias
-    preview_data = preview_column['data'][0]
+    preview_data = preview_column['data']['1']
     assert all([key in preview_data for key in [publication_title_alias, publisher_name_alias, author_first_name_alias, author_last_name_alias, co_author_first_name_alias, co_author_last_name_alias]])
 
     expected_preview_data = {publication_title_alias: 'Pressure Should Old', publisher_name_alias: 'Ruiz', author_first_name_alias: 'Matthew', author_last_name_alias: 'Brown', co_author_first_name_alias: 'Mark', co_author_last_name_alias: 'Smith'}
diff --git a/mathesar_ui/src/stores/table-data/record-summaries/__tests__/recordSummaryUtils.test.ts b/mathesar_ui/src/stores/table-data/record-summaries/__tests__/recordSummaryUtils.test.ts
new file mode 100644
--- /dev/null
+++ b/mathesar_ui/src/stores/table-data/record-summaries/__tests__/recordSummaryUtils.test.ts
@@ -0,0 +1,73 @@
+import { ImmutableMap } from '@mathesar-component-library';
+import {
+  prepareFieldsAsRecordSummaryInputData,
+  renderTransitiveRecordSummary,
+  type RecordSummariesForSheet,
+} from '../recordSummaryUtils';
+
+describe('Record Summary', () => {
+  // prettier-ignore
+  const fields = new ImmutableMap<number, string | number | null>([ /*
+      | Column ids | cell values | */
+      [  11        , 'one'       ],
+      [  12        , 'two'       ],
+      [  13        , null        ],
+      [  14        , 1401        ], // an FK column with transitive data
+      [  15        , 1501        ], // an FK column with incomplete transitive data
+      [  16        , 1601        ], // an FK column with no transitive data
+  ]);
+  const inputData = prepareFieldsAsRecordSummaryInputData(fields);
+  const transitiveData: RecordSummariesForSheet = new ImmutableMap([
+    [
+      '14', // <- The id of the FK column
+      new ImmutableMap([
+        [
+          '1401', // <- The id of the record in the FK column
+          'Apple-Banana', // <- The record summary for that record
+        ],
+      ]),
+    ],
+    [
+      '15', // <- The id of the FK column
+      // We have some input data here, but we don't have an entry with a
+      // record id of 1501. We don't expect this to happen in practice, but we
+      // need to handle it anyway.
+      new ImmutableMap([
+        [
+          '9999', // <- The id of the record in the FK column
+          'NOPE',
+        ],
+      ]),
+      // Note that, for the sake of testing different scenarios, we _don't_ have
+      // an entry here for FK column 16.
+    ],
+  ]);
+  // prettier-ignore
+  const testCases =
+  [
+  //  template         | result
+    [ ''               , '' ],
+    [ '{11} {12}'      , 'one two' ],
+    [ '{{11} {12}}'    , '{one two}' ],
+    [ '{{11}}'         , '{one}' ],
+    [ '{ 11}'          , '{ 11}' ],
+    [ '{11 }'          , '{11 }' ],
+    [ '{99}'           , '{99}' ], // column id 99 doesn't exist
+    [ '{13}'           , '(null)' ],
+    [ '{14}'           , 'Apple-Banana' ],
+    [ '{11} {14}'      , 'one Apple-Banana' ],
+    [ '{15}'           , '1501' ],
+    [ '{16}'           , '1601' ],
+  ];
+  test.each(testCases)(
+    'renderTransitiveRecordSummary %#',
+    (template, result) => {
+      const recordSummary = renderTransitiveRecordSummary({
+        template,
+        inputData,
+        transitiveData,
+      });
+      expect(recordSummary).toBe(result);
+    },
+  );
+});
diff --git a/mathesar_ui/src/utils/__tests__/recordSummary.test.ts b/mathesar_ui/src/utils/__tests__/recordSummary.test.ts
deleted file mode 100644
--- a/mathesar_ui/src/utils/__tests__/recordSummary.test.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { ImmutableMap } from '@mathesar-component-library';
-import {
-  renderSummaryFromFieldsAndFkData,
-  renderSummaryFromValuesObject,
-} from '../recordSummary';
-import type { DataForRecordSummaryInFkCell } from '../recordSummaryTypes';
-
-test.each(
-  // prettier-ignore
-  [
-  [ '{2__9___10__21__col__22}'   , 'one' ],
-  [ '({2__9___10__21__col__22})' , '(one)' ],
-  [ '2__9___10__21__col__22'     , '2__9___10__21__col__22' ],
-  [ '{9__9___99__99__col__99}'   , '{9__9___99__99__col__99}' ],
-  [ ''                           , '' ],
-],
-)('renderRecordSummaryFromValuesObject %#', (template, result) => {
-  const values: Record<string, string> = {
-    '2__9___10__21__col__22': 'one',
-  };
-  expect(renderSummaryFromValuesObject(template, values)).toBe(result);
-});
-
-test.each(
-  // prettier-ignore
-  [
-  [ ''                      , '' ],
-  [ '{1} {2}'               , 'one two' ],
-  [ '{4} {5}'               , 'null {5}' ],
-  [ '{{1}} {2}'             , '{one} two' ],
-  [ '{1 } { 1} {2{8}} {9}'  , '{1 } { 1} {2{8}} {9}' ],
-  [ '{3}'                   , 'Hi!' ],
-],
-)('renderRecordSummaryFromFieldsAndFkData %#', (template, result) => {
-  const fields = new ImmutableMap<number, string | number | null>([
-    // Column ids
-    //  cell values
-    [1, 'one'],
-    [2, 'two'],
-    [3, 99], // an FK column
-    [4, null],
-  ]);
-  const fkSummaryData = new ImmutableMap<number, DataForRecordSummaryInFkCell>([
-    [
-      3,
-      {
-        column: 3,
-        template: '{7__8__col__9}',
-        data: { '7__8__col__9': 'Hi!' },
-      },
-    ],
-  ]);
-  expect(
-    renderSummaryFromFieldsAndFkData(template, fields, fkSummaryData),
-  ).toBe(result);
-});