Skip to content

foreign key fix#1725

Merged
lyubov-voloshko merged 3 commits into
mainfrom
feature/guard-null-primary-key
Apr 23, 2026
Merged

foreign key fix#1725
lyubov-voloshko merged 3 commits into
mainfrom
feature/guard-null-primary-key

Conversation

@gugu
Copy link
Copy Markdown
Contributor

@gugu gugu commented Apr 21, 2026

Prevents edit/duplicate/delete buttons and the preview sidebar's "Open the record" link from producing broken queries (?pk=null) when a row's primary key is null, empty, or a null foreign-key reference. Also applies pnpm audit --fix overrides.

Summary by CodeRabbit

  • New Features

    • Add explicit “null/empty” option for foreign-key editors so users can clear a relation.
    • Autocomplete and edit UI now show a distinct styled null option.
  • Bug Fixes

    • Improved null-safety across table and foreign-key displays: null/undefined values render as an em dash (—) and avoid runtime errors.
    • Clickable links/buttons only render when a value is present.
  • Tests

    • Added extensive tests covering null, undefined and falsy foreign-key cases.

Copilot AI review requested due to automatic review settings April 21, 2026 11:24
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

Added null-safety guards and conditional rendering for foreign-key handling across table and record view/edit components, plus corresponding unit tests to cover null, undefined, and falsy values.

Changes

Cohort / File(s) Summary
Dashboard row & table logic
frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts, frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts
Added optional chaining and early-return guards to getForeignKeyValue, getForeignKeyQueryParams, getCellValue, and isForeignKeySelected to safely handle null/undefined cells and records.
Record-view foreign key template & component
frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html, frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.ts
Template renders a dash when displayValue() is null; link only rendered for non-null values. Click handler now checks value() != null before emitting. Minor formatting/type annotation edits.
Table-display foreign key template & tests
frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.html, frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.ts, frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.spec.ts
Template reworked to show em dash for value() == null, render button only when relations exist; tests updated/expanded to cover null/undefined and falsy values (0, empty string).
Record-edit foreign key component & UI
frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts, frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html, frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.css, frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts
Added null-option support: allowsNull flag, nullSuggestion, and withNullOption(...) wrapper to append a nullable suggestion. Selecting null clears value/query params and emits null. Added styling for null option and tests covering null-option behavior.
Dynamic form structure wiring
frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html
Passes structure: tableRowStructure[value] into dynamic inputs so form widgets receive field structure.
Test coverage: dashboard/table row view
frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts, frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts
Expanded unit tests for null-safety and object-vs-primitive foreign-key cases; ensure methods return null, {} or appropriate values without throwing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • gugu

Poem

🐇 I nibble code and tidy seams,

When nulls appear I shake no greens.
A dash, a guard, a gentle hop—
Now foreign keys behave, don't drop. 🥕

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "foreign key fix" is vague and non-descriptive; it does not clearly summarize what specific aspect of foreign keys is being fixed or the main change. Revise the title to be more specific and descriptive, e.g., "Handle null foreign key values in UI components" or "Add null-safety checks to prevent broken queries with null primary keys".
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed PR implements proper defensive null-checking in foreign key field handling with guard clauses, optional chaining, and safe defaults that prevent malformed query parameters and application errors.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/guard-null-primary-key

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Guards dashboard table row actions and preview “Open the record” affordances against rows whose primary key (including FK-backed PKs) is null/empty, and applies pnpm audit --fix-style dependency overrides.

Changes:

  • Add primary-key validity checks in the table data source and row preview component.
  • Block/skip edit/duplicate/delete actions (single + bulk) and hide action links when PK is invalid.
  • Update pnpm overrides/lockfile to address audit findings (ajv, picomatch, path-to-regexp, lodash, @nestjs/core).

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds/updates pnpm overrides and updates resolved versions from audit fixes.
package.json Adds pnpm override rules for several audited transitive deps.
frontend/src/app/components/dashboard/db-tables-data-source.ts Makes FK-backed PK param extraction null-safe; adds hasValidPrimaryKey.
frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts Adds guards for single and bulk actions to prevent targeting rows with empty PKs.
frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html Hides edit/duplicate/delete controls when PK is invalid.
frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts Adds PK validity check for preview sidebar actions.
frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html Hides “copy link/open record” when PK is invalid; adds type="button" for close button.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 345 to 372
@@ -355,7 +355,7 @@ <h2 class="mat-h2 table-name">{{ displayName }}</h2>
(click)="stashUrlParams(); posthog.capture('Dashboard: edit row is clicked')">
<mat-icon fontSet="material-symbols-outlined">create</mat-icon>
</a>
<a mat-icon-button *ngIf="canAddRow()"
<a mat-icon-button *ngIf="canAddRow() && tableData.hasValidPrimaryKey(element)"
routerLink="/dashboard/{{connectionID}}/{{name}}/entry"
[queryParams]="tableData.getQueryParams(element, 'dub')"
class="db-table-cell-action-button"
@@ -368,7 +368,7 @@ <h2 class="mat-h2 table-name">{{ displayName }}</h2>
(click)="stashUrlParams(); posthog.capture('Dashboard: duplicate row is clicked')">
<mat-icon fontSet="material-symbols-outlined">difference</mat-icon>
</a>
<button type="button" mat-icon-button *ngIf="canDeleteRow() && tableData.canDelete"
<button type="button" mat-icon-button *ngIf="canDeleteRow() && tableData.canDelete && tableData.hasValidPrimaryKey(element)"
class="db-table-cell-action-button"
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tableData.hasValidPrimaryKey(element) is invoked multiple times per row in this template (edit/duplicate/delete). Because template method calls run on every change-detection pass, this can become a hotspot for large tables. Consider computing a per-row flag once (e.g., assign to a local template variable via an outer ng-container, or precompute/attach the value when formatting rows) and reuse it for all action buttons.

Copilot uses AI. Check for mistakes.
Comment thread package.json Outdated
"path-to-regexp@>=8.0.0 <8.4.0": ">=8.4.0",
"lodash@>=4.0.0 <=4.17.23": ">=4.18.0",
"lodash@<=4.17.23": ">=4.18.0",
"@nestjs/core@<=11.1.17": ">=11.1.18"
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new pnpm overrides use open-ended ranges (e.g. ">=8.18.0", ">=4.18.0", ">=11.1.18"). Even with a lockfile, this makes future lock regenerations potentially pick up unreviewed major/minor updates (and may produce large, noisy lock diffs). Consider pinning these overrides to specific patched versions (or at least bounding them with an upper limit) to keep installs reproducible and upgrades intentional.

Suggested change
"@nestjs/core@<=11.1.17": ">=11.1.18"
"@nestjs/core@<=11.1.17": "11.1.18"

Copilot uses AI. Check for mistakes.
Comment thread pnpm-lock.yaml
path-to-regexp@>=8.0.0 <8.4.0: '>=8.4.0'
lodash@>=4.0.0 <=4.17.23: '>=4.18.0'
lodash@<=4.17.23: '>=4.18.0'
'@nestjs/core@<=11.1.17': '>=11.1.18'
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lockfile overrides include multiple broad range remaps (ajv/picomatch/path-to-regexp/lodash/@nestjs/core). If these are intended as security fixes, consider pinning them to concrete versions (or adding upper bounds) to reduce the chance of unintended upgrades when the lockfile is regenerated.

Suggested change
'@nestjs/core@<=11.1.17': '>=11.1.18'
'@nestjs/core@<=11.1.17': '11.1.18'

Copilot uses AI. Check for mistakes.
Comment on lines 351 to 357
getQueryParams(row, action) {
const params = Object.fromEntries(
this.keyAttributes.map((column) => {
if (this.foreignKeysList.includes(column.column_name)) {
const referencedColumnNameOfForeignKey = this.foreignKeys[column.column_name].referenced_column_name;
return [column.column_name, row[column.column_name][referencedColumnNameOfForeignKey]];
return [column.column_name, row[column.column_name]?.[referencedColumnNameOfForeignKey] ?? null];
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getQueryParams now safely handles nullable foreign-key objects, but there is still an unguarded foreign-key dereference earlier in this file when building the primary key object for fetchTableRow (it indexes firstRow[column][referencedColumn] without optional chaining). If a primary key column is also a nullable FK, fetchRows() can still throw before the UI guards ever run. Consider applying the same optional chaining / null handling there as well.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
package.json (1)

4-4: ⚠️ Potential issue | 🔴 Critical

Fix the pnpm version source-of-truth mismatch.

CI is already failing because packageManager pins pnpm@10.33.0 while the GitHub Action also sets pnpm version: "10". Prefer keeping this packageManager pin and removing the workflow-level version key, or make both exactly match.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 4, The package.json "packageManager" field is pinned to
"pnpm@10.33.0" while the GitHub Action workflow also sets pnpm via a "version"
key (mismatched source-of-truth), causing CI failures; fix by making them
identical: either keep package.json's packageManager "pnpm@10.33.0" and remove
or update the workflow's "version" key to match that exact version, or change
package.json.packageManager to match the workflow; locate references to
packageManager in package.json and the workflow's pnpm "version" key and make
both exact matches.
frontend/src/app/components/dashboard/db-tables-data-source.ts (1)

162-172: ⚠️ Potential issue | 🟠 Major

Guard the first-row related-record lookup too.

A table whose first row has a null FK primary key still hits Line 166 before the new action guards run, causing a null dereference; a non-FK null primary key still calls fetchTableRow with null query params.

🐛 Proposed fix
-						this._tableRow
-							.fetchTableRow(
-								connectionID,
-								tableName,
-								res.primaryColumns.reduce((keys, column) => {
-									if (this.foreignKeysList.includes(column.column_name)) {
-										const referencedColumnNameOfForeignKey =
-											this.foreignKeys[column.column_name].referenced_column_name;
-										keys[column.column_name] = firstRow[column.column_name][referencedColumnNameOfForeignKey];
-									} else {
-										keys[column.column_name] = firstRow[column.column_name];
-									}
-									return keys;
-								}, {}),
-							)
-							.subscribe((res) => (this.relatedRecords = res.referenced_table_names_and_columns[0]));
+						const firstRowPrimaryKeys = res.primaryColumns.reduce((keys, column) => {
+							if (this.foreignKeysList.includes(column.column_name)) {
+								const referencedColumnNameOfForeignKey =
+									this.foreignKeys[column.column_name].referenced_column_name;
+								keys[column.column_name] =
+									firstRow[column.column_name]?.[referencedColumnNameOfForeignKey] ?? null;
+							} else {
+								keys[column.column_name] = firstRow[column.column_name];
+							}
+							return keys;
+						}, {});
+
+						const hasValidFirstRowPrimaryKey = Object.values(firstRowPrimaryKeys).every(
+							(value) => value !== null && value !== undefined && value !== '',
+						);
+
+						if (hasValidFirstRowPrimaryKey) {
+							this._tableRow
+								.fetchTableRow(connectionID, tableName, firstRowPrimaryKeys)
+								.subscribe((res) => (this.relatedRecords = res.referenced_table_names_and_columns[0]));
+						}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/dashboard/db-tables-data-source.ts` around lines
162 - 172, The code that builds the key object from res.primaryColumns can
dereference nulls in firstRow and produce null query params; before accessing
firstRow[column.column_name][referencedColumnNameOfForeignKey] or passing values
to fetchTableRow, add guards: ensure firstRow[column.column_name] is non-null,
and for FKs also ensure referencedColumnNameOfForeignKey exists and
firstRow[column.column_name][referencedColumnNameOfForeignKey] is non-null; when
a value is missing, omit that key or set it to undefined so fetchTableRow is not
called with a null param. Update the block that reduces res.primaryColumns (and
any call sites to fetchTableRow) to perform these null checks using the symbols
res.primaryColumns, this.foreignKeysList, this.foreignKeys, firstRow, and
fetchTableRow.
frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html (1)

345-371: 🛠️ Refactor suggestion | 🟠 Major

Replace *ngIf with @if for the three control guards.

The changes at lines 345, 358, and 371 add necessary primary-key validation checks, but must use Angular 19's built-in control flow instead of structural directives per coding guidelines.

Refactor to `@if`
-                    <a mat-icon-button *ngIf="canEditRow() && tableData.hasValidPrimaryKey(element)"
+                    `@if` (canEditRow() && tableData.hasValidPrimaryKey(element)) {
+                    <a mat-icon-button
                         routerLink="/dashboard/{{connectionID}}/{{name}}/entry"
                         [queryParams]="tableData.getQueryParams(element)"
                         class="db-table-cell-action-button"
@@
                         (click)="stashUrlParams(); posthog.capture('Dashboard: edit row is clicked')">
                         <mat-icon fontSet="material-symbols-outlined">create</mat-icon>
                     </a>
-                    <a mat-icon-button *ngIf="canAddRow() && tableData.hasValidPrimaryKey(element)"
+                    }
+                    `@if` (canAddRow() && tableData.hasValidPrimaryKey(element)) {
+                    <a mat-icon-button
                         routerLink="/dashboard/{{connectionID}}/{{name}}/entry"
                         [queryParams]="tableData.getQueryParams(element, 'dub')"
                         class="db-table-cell-action-button"
@@
                         (click)="stashUrlParams(); posthog.capture('Dashboard: duplicate row is clicked')">
                         <mat-icon fontSet="material-symbols-outlined">difference</mat-icon>
                     </a>
-                    <button type="button" mat-icon-button *ngIf="canDeleteRow() && tableData.canDelete && tableData.hasValidPrimaryKey(element)"
+                    }
+                    `@if` (canDeleteRow() && tableData.canDelete && tableData.hasValidPrimaryKey(element)) {
+                    <button type="button" mat-icon-button
                         class="db-table-cell-action-button"
                         attr.data-testid="table-delete-record-{{i}}-button"
                         angulartics2On="click"
@@
                         (click)="handleDeleteRow($event, element); posthog.capture('Dashboard: delete row is clicked')">
                         <mat-icon fontSet="material-symbols-outlined">delete</mat-icon>
                     </button>
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html`
around lines 345 - 371, Replace the three structural directives that use *ngIf
with Angular 19 control-flow `@if` constructs: for the edit anchor that currently
uses "*ngIf=\"canEditRow() && tableData.hasValidPrimaryKey(element)\"", replace
that *ngIf with an `@if` equivalent while keeping the same condition and all
attributes (routerLink, [queryParams], stashUrlParams, etc.); do the same for
the duplicate anchor that uses "*ngIf=\"canAddRow() &&
tableData.hasValidPrimaryKey(element)\"" and for the delete button using
"*ngIf=\"canDeleteRow() && tableData.canDelete &&
tableData.hasValidPrimaryKey(element)\"". Locate these by the surrounding
symbols canEditRow(), canAddRow(), canDeleteRow(),
tableData.hasValidPrimaryKey(element), and the routerLink/queryParams usages and
update the template syntax to use `@if` without changing the conditions or other
attributes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html`:
- Around line 7-13: Replace the deprecated structural *ngIf usage with Angular
19 control-flow `@if` in the db-table-row-view template: for both the button and
the anchor that currently use *ngIf="selectedRow.link && hasValidPrimaryKey()",
wrap the elements (or use the `@if` inline syntax) so the visibility is driven by
`@if` (selectedRow.link && hasValidPrimaryKey()) instead of *ngIf; keep the
existing bindings and handlers (getDedicatedPageLink(), showCopyNotification())
and ensure the rendered element still provides the matTooltip,
[cdkCopyToClipboard], and (cdkCopyToClipboardCopied) behavior unchanged.

In `@frontend/src/app/components/dashboard/db-tables-data-source.ts`:
- Around line 369-377: The hasValidPrimaryKey function's parameter `row` is
untyped; update the method signature for hasValidPrimaryKey to declare `row:
Record<string, unknown>` so dynamic bracket access works with proper typing, and
ensure references to this.keyAttributes, this.foreignKeysList and
this.foreignKeys remain unchanged (they're the unique symbols to locate the
function) — no other logic changes required.

In `@package.json`:
- Around line 26-31: The pnpm overrides use open-ended lower-bound-only
replacements (e.g., the override keys/values for ajv "ajv@>=7.0.0-alpha.0
<8.18.0": ">=8.18.0" and `@nestjs/core` "@nestjs/core@<=11.1.17": ">=11.1.18",
plus picomatch, path-to-regexp and lodash entries) which permit future-major
upgrades; change each replacement value to a bounded semver range that prevents
major bumps (for example, use >=8.18.0 <9.0.0 for ajv, >=11.1.18 <12.0.0 for
`@nestjs/core`, >=4.0.4 <5.0.0 for picomatch, >=8.4.0 <9.0.0 for path-to-regexp,
and consolidate the two lodash selectors into a single override with a bounded
range like >=4.18.0 <5.0.0) so transitive dependencies cannot resolve to
incompatible major versions.

---

Outside diff comments:
In
`@frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html`:
- Around line 345-371: Replace the three structural directives that use *ngIf
with Angular 19 control-flow `@if` constructs: for the edit anchor that currently
uses "*ngIf=\"canEditRow() && tableData.hasValidPrimaryKey(element)\"", replace
that *ngIf with an `@if` equivalent while keeping the same condition and all
attributes (routerLink, [queryParams], stashUrlParams, etc.); do the same for
the duplicate anchor that uses "*ngIf=\"canAddRow() &&
tableData.hasValidPrimaryKey(element)\"" and for the delete button using
"*ngIf=\"canDeleteRow() && tableData.canDelete &&
tableData.hasValidPrimaryKey(element)\"". Locate these by the surrounding
symbols canEditRow(), canAddRow(), canDeleteRow(),
tableData.hasValidPrimaryKey(element), and the routerLink/queryParams usages and
update the template syntax to use `@if` without changing the conditions or other
attributes.

In `@frontend/src/app/components/dashboard/db-tables-data-source.ts`:
- Around line 162-172: The code that builds the key object from
res.primaryColumns can dereference nulls in firstRow and produce null query
params; before accessing
firstRow[column.column_name][referencedColumnNameOfForeignKey] or passing values
to fetchTableRow, add guards: ensure firstRow[column.column_name] is non-null,
and for FKs also ensure referencedColumnNameOfForeignKey exists and
firstRow[column.column_name][referencedColumnNameOfForeignKey] is non-null; when
a value is missing, omit that key or set it to undefined so fetchTableRow is not
called with a null param. Update the block that reduces res.primaryColumns (and
any call sites to fetchTableRow) to perform these null checks using the symbols
res.primaryColumns, this.foreignKeysList, this.foreignKeys, firstRow, and
fetchTableRow.

In `@package.json`:
- Line 4: The package.json "packageManager" field is pinned to "pnpm@10.33.0"
while the GitHub Action workflow also sets pnpm via a "version" key (mismatched
source-of-truth), causing CI failures; fix by making them identical: either keep
package.json's packageManager "pnpm@10.33.0" and remove or update the workflow's
"version" key to match that exact version, or change package.json.packageManager
to match the workflow; locate references to packageManager in package.json and
the workflow's pnpm "version" key and make both exact matches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 35185774-5303-49cd-b178-c843fdf5a28e

📥 Commits

Reviewing files that changed from the base of the PR and between b2df2ac and 442cc10.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html
  • frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts
  • frontend/src/app/components/dashboard/db-table-view/db-table-view.component.html
  • frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts
  • frontend/src/app/components/dashboard/db-tables-data-source.ts
  • package.json

Comment on lines +7 to +13
<button type="button" mat-icon-button *ngIf="selectedRow.link && hasValidPrimaryKey()"
matTooltip="Copy link to this record"
[cdkCopyToClipboard]="getDedicatedPageLink()"
(cdkCopyToClipboardCopied)="showCopyNotification('Link to this record was copied to clipboard.')">
<mat-icon>link</mat-icon>
</button>
<a mat-icon-button *ngIf="selectedRow.link"
<a mat-icon-button *ngIf="selectedRow.link && hasValidPrimaryKey()"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the newly guarded primary-key controls no longer use structural directives.
# Expected: no matches after refactoring touched guards to `@if`.
rg -nP '\*ngIf="[^"]*hasValidPrimaryKey' --iglob '*db-table*.component.html'

Repository: rocket-admin/rocketadmin

Length of output: 1058


Use @if instead of *ngIf for the primary-key guards.

Angular 19's built-in control flow must be used in all new code. Lines 7 and 13 use the deprecated *ngIf structural directive and must be refactored to use @if.

♻️ Refactoring required
-            <button type="button" mat-icon-button *ngIf="selectedRow.link && hasValidPrimaryKey()"
+            `@if` (selectedRow.link && hasValidPrimaryKey()) {
+            <button type="button" mat-icon-button
                 matTooltip="Copy link to this record"
                 [cdkCopyToClipboard]="getDedicatedPageLink()"
                 (cdkCopyToClipboardCopied)="showCopyNotification('Link to this record was copied to clipboard.')">
                 <mat-icon>link</mat-icon>
             </button>
-            <a mat-icon-button *ngIf="selectedRow.link && hasValidPrimaryKey()"
+            <a mat-icon-button
                 [routerLink]="selectedRow.link"
                 [queryParams]="getDedicatedPageLinkParams()"
                 matTooltip="Open the record"
                 (click)="stashUrlParams()">
                 <mat-icon fontSet="material-symbols-outlined">create</mat-icon>
             </a>
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button type="button" mat-icon-button *ngIf="selectedRow.link && hasValidPrimaryKey()"
matTooltip="Copy link to this record"
[cdkCopyToClipboard]="getDedicatedPageLink()"
(cdkCopyToClipboardCopied)="showCopyNotification('Link to this record was copied to clipboard.')">
<mat-icon>link</mat-icon>
</button>
<a mat-icon-button *ngIf="selectedRow.link"
<a mat-icon-button *ngIf="selectedRow.link && hasValidPrimaryKey()"
`@if` (selectedRow.link && hasValidPrimaryKey()) {
<button type="button" mat-icon-button
matTooltip="Copy link to this record"
[cdkCopyToClipboard]="getDedicatedPageLink()"
(cdkCopyToClipboardCopied)="showCopyNotification('Link to this record was copied to clipboard.')">
<mat-icon>link</mat-icon>
</button>
<a mat-icon-button
[routerLink]="selectedRow.link"
[queryParams]="getDedicatedPageLinkParams()"
matTooltip="Open the record"
(click)="stashUrlParams()">
<mat-icon fontSet="material-symbols-outlined">create</mat-icon>
</a>
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.html`
around lines 7 - 13, Replace the deprecated structural *ngIf usage with Angular
19 control-flow `@if` in the db-table-row-view template: for both the button and
the anchor that currently use *ngIf="selectedRow.link && hasValidPrimaryKey()",
wrap the elements (or use the `@if` inline syntax) so the visibility is driven by
`@if` (selectedRow.link && hasValidPrimaryKey()) instead of *ngIf; keep the
existing bindings and handlers (getDedicatedPageLink(), showCopyNotification())
and ensure the rendered element still provides the matTooltip,
[cdkCopyToClipboard], and (cdkCopyToClipboardCopied) behavior unchanged.

Comment on lines +369 to +377
hasValidPrimaryKey(row): boolean {
if (!this.keyAttributes?.length) return false;
return this.keyAttributes.every((column) => {
const value = this.foreignKeysList.includes(column.column_name)
? row[column.column_name]?.[this.foreignKeys[column.column_name].referenced_column_name]
: row[column.column_name];
return value !== null && value !== undefined && value !== '';
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the newly added method parameter is no longer untyped.
# Expected: no matches after adding an explicit row parameter type.
rg -nP 'hasValidPrimaryKey\s*\(\s*row\s*\)' --iglob 'db-tables-data-source.ts'

Repository: rocket-admin/rocketadmin

Length of output: 170


🏁 Script executed:

sed -n '360,390p' frontend/src/app/components/dashboard/db-tables-data-source.ts

Repository: rocket-admin/rocketadmin

Length of output: 576


🏁 Script executed:

head -50 frontend/src/app/components/dashboard/db-tables-data-source.ts | tail -40

Repository: rocket-admin/rocketadmin

Length of output: 1339


🏁 Script executed:

grep -n "row" frontend/src/app/components/dashboard/db-tables-data-source.ts | head -20

Repository: rocket-admin/rocketadmin

Length of output: 1143


🏁 Script executed:

sed -n '98,110p' frontend/src/app/components/dashboard/db-tables-data-source.ts

Repository: rocket-admin/rocketadmin

Length of output: 492


🏁 Script executed:

sed -n '351,365p' frontend/src/app/components/dashboard/db-tables-data-source.ts

Repository: rocket-admin/rocketadmin

Length of output: 618


🏁 Script executed:

rg -n "formatRow|getQueryParams|hasValidPrimaryKey" frontend/src/app/components/dashboard/db-tables-data-source.ts | head -10

Repository: rocket-admin/rocketadmin

Length of output: 257


Add type annotation to the row parameter.

The row parameter lacks a type annotation. Use Record<string, unknown> to match the dynamic key access pattern used throughout the method.

♻️ Proposed refactor
-	hasValidPrimaryKey(row): boolean {
+	hasValidPrimaryKey(row: Record<string, unknown>): boolean {
 		if (!this.keyAttributes?.length) return false;
 		return this.keyAttributes.every((column) => {
-			const value = this.foreignKeysList.includes(column.column_name)
-				? row[column.column_name]?.[this.foreignKeys[column.column_name].referenced_column_name]
-				: row[column.column_name];
+			const primaryKeyValue = row[column.column_name];
+			const value =
+				this.foreignKeysList.includes(column.column_name) &&
+				primaryKeyValue !== null &&
+				typeof primaryKeyValue === 'object'
+					? (primaryKeyValue as Record<string, unknown>)[
+							this.foreignKeys[column.column_name].referenced_column_name
+						]
+					: primaryKeyValue;
 			return value !== null && value !== undefined && value !== '';
 		});
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
hasValidPrimaryKey(row): boolean {
if (!this.keyAttributes?.length) return false;
return this.keyAttributes.every((column) => {
const value = this.foreignKeysList.includes(column.column_name)
? row[column.column_name]?.[this.foreignKeys[column.column_name].referenced_column_name]
: row[column.column_name];
return value !== null && value !== undefined && value !== '';
});
}
hasValidPrimaryKey(row: Record<string, unknown>): boolean {
if (!this.keyAttributes?.length) return false;
return this.keyAttributes.every((column) => {
const primaryKeyValue = row[column.column_name];
const value =
this.foreignKeysList.includes(column.column_name) &&
primaryKeyValue !== null &&
typeof primaryKeyValue === 'object'
? (primaryKeyValue as Record<string, unknown>)[
this.foreignKeys[column.column_name].referenced_column_name
]
: primaryKeyValue;
return value !== null && value !== undefined && value !== '';
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/components/dashboard/db-tables-data-source.ts` around lines
369 - 377, The hasValidPrimaryKey function's parameter `row` is untyped; update
the method signature for hasValidPrimaryKey to declare `row: Record<string,
unknown>` so dynamic bracket access works with proper typing, and ensure
references to this.keyAttributes, this.foreignKeysList and this.foreignKeys
remain unchanged (they're the unique symbols to locate the function) — no other
logic changes required.

Comment thread package.json Outdated
Guards the helpers that extract FK cell values against null/undefined and
updates the two FK display components (table cell, row preview) to render
a dash only when the value is genuinely absent. Using `== null` keeps `0`,
empty string, and `false` as real FK values so rows whose FK id is 0 still
render the link, not a dash.

Previously `Object.keys(null)` threw on any row with a null FK column and
`{{ value() || '—' }}` substituted a dash for legitimately-zero ids.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gugu gugu force-pushed the feature/guard-null-primary-key branch from 442cc10 to fc516b4 Compare April 22, 2026 09:23
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts (1)

245-270: Add explicit return types to the new spec callbacks.

The added it callbacks should declare : void to keep new TypeScript test code aligned with the project rule.

Proposed cleanup
-it('should return null (not throw) when foreign key cell is null', () => {
+it('should return null (not throw) when foreign key cell is null', (): void => {
 	const foreignKey = {
 		autocomplete_columns: ['FirstName'],
 		column_name: 'CustomerId',
@@
-it('should not throw in isForeignKeySelected when record is null', () => {
+it('should not throw in isForeignKeySelected when record is null', (): void => {
 	const foreignKey = {
 		autocomplete_columns: ['FirstName'],
 		column_name: 'CustomerId',

As per coding guidelines, "**/*.{ts,tsx}: Always add type annotations to function parameters and return types in TypeScript`."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts`
around lines 245 - 270, The two new test callbacks are missing explicit return
types; update the `it` callback signatures in db-table-view.component.spec.ts to
declare a return type of `: void` for both tests (the one exercising
`component.getCellValue` and the one exercising
`component.isForeignKeySelected`) so they comply with the project's TypeScript
rule requiring function return type annotations.
frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts (1)

26-96: Keep the new spec fixtures typed.

The added callbacks need explicit : void return types, and the repeated as any casts can hide drift in the selectedRow shape. A small typed fixture/helper would keep these tests strict without much extra setup.

Example direction
-describe('getForeignKeyValue', () => {
+describe('getForeignKeyValue', (): void => {
 	const baseRow = {
 		foreignKeys: {
@@
-	it('returns null when record field is null', () => {
-		component.selectedRow = { ...baseRow, record: { user_id: null } } as any;
+	it('returns null when record field is null', (): void => {
+		component.selectedRow = { ...baseRow, record: { user_id: null } } as NonNullable<
+			DbTableRowViewComponent['selectedRow']
+		>;
 		expect(component.getForeignKeyValue('user_id')).toBeNull();
 	});

As per coding guidelines, "**/*.{ts,tsx}: Always add type annotations to function parameters and return types in TypeScript" and "**/*.{ts,tsx}: Avoid any types - use specific types or generics instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts`
around lines 26 - 96, The tests for getForeignKeyValue and
getForeignKeyQueryParams use untyped callbacks and many "as any" casts on
selectedRow which can hide shape drift; add explicit ": void" return types to
each it(...) callback and replace the repeated casts by introducing a small
typed fixture/interface (e.g., a LocalSelectedRow or TestRow type) that models
foreignKeys and record, then use that fixture to set component.selectedRow
without "as any" to keep tests strictly typed while preserving the same test
values; update tests referencing component.selectedRow and both methods
(getForeignKeyValue, getForeignKeyQueryParams) to use the typed fixture.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts`:
- Around line 84-89: The current getForeignKeyQueryParams implementation returns
an object like { id: null } for object FKs whose referenced id is null; change
getForeignKeyQueryParams to guard against null/undefined referenced values
(e.g., if selectedRow.record[field] is an object but its referenced key value is
null/undefined then return null/undefined instead of an object) so no broken
query is generated, and add a unit test in db-table-row-view.component.spec.ts
mirroring the existing positive case but with record: { user_id: { id: null,
name: 'alice' } } and assert that getForeignKeyQueryParams('user_id') returns
null/undefined (matching your chosen sentinel).

In
`@frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.ts`:
- Around line 20-27: Replace the untyped payload and event in
onForeignKeyClick/handleForeignKeyClick by defining a specific interface (e.g.,
ForeignKeyClickPayload with foreignKey: ForeignKeyValue and value: string |
number | ... as appropriate) and change the handler signature to
handleForeignKeyClick(event: MouseEvent): void; inside handleForeignKeyClick
cache const relations = this.relations(); const value = this.value(); before the
null/guard so those narrowed types are used in the emit call
this.onForeignKeyClick.emit({ foreignKey: relations, value }); also update the
ForeignKeyValue type definition to include any additional allowed value types
beyond string|number and remove all uses of any.

---

Nitpick comments:
In
`@frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts`:
- Around line 26-96: The tests for getForeignKeyValue and
getForeignKeyQueryParams use untyped callbacks and many "as any" casts on
selectedRow which can hide shape drift; add explicit ": void" return types to
each it(...) callback and replace the repeated casts by introducing a small
typed fixture/interface (e.g., a LocalSelectedRow or TestRow type) that models
foreignKeys and record, then use that fixture to set component.selectedRow
without "as any" to keep tests strictly typed while preserving the same test
values; update tests referencing component.selectedRow and both methods
(getForeignKeyValue, getForeignKeyQueryParams) to use the typed fixture.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts`:
- Around line 245-270: The two new test callbacks are missing explicit return
types; update the `it` callback signatures in db-table-view.component.spec.ts to
declare a return type of `: void` for both tests (the one exercising
`component.getCellValue` and the one exercising
`component.isForeignKeySelected`) so they comply with the project's TypeScript
rule requiring function return type annotations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7cfc22dc-edec-4505-8cc7-c358c99bea68

📥 Commits

Reviewing files that changed from the base of the PR and between 442cc10 and fc516b4.

📒 Files selected for processing (9)
  • frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts
  • frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts
  • frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts
  • frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts
  • frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html
  • frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts
  • frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.html
  • frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.spec.ts
  • frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts
  • frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts

Comment on lines +84 to +89
it('returns referenced column param when FK is an object', () => {
component.selectedRow = {
...baseRow,
record: { user_id: { id: 42, name: 'alice' } },
} as any;
expect(component.getForeignKeyQueryParams('user_id')).toEqual({ id: 42 });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cover object FKs whose referenced value is null.

This positive object case still misses the null-reference branch. With the current implementation in frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts:272-277, { user_id: { id: null, name: 'alice' } } returns { id: null }, which can still generate the broken query this PR is meant to prevent.

Suggested regression and guard
 it('returns referenced column param when FK is an object', () => {
 	component.selectedRow = {
 		...baseRow,
 		record: { user_id: { id: 42, name: 'alice' } },
 	} as any;
 	expect(component.getForeignKeyQueryParams('user_id')).toEqual({ id: 42 });
 });
+
+it('returns {} when FK object referenced column is null', () => {
+	component.selectedRow = {
+		...baseRow,
+		record: { user_id: { id: null, name: 'alice' } },
+	} as any;
+	expect(component.getForeignKeyQueryParams('user_id')).toEqual({});
+});

Implementation-side guard:

 const cell = this.selectedRow?.record?.[field];
 const referencedColumnName = this.selectedRow?.foreignKeys?.[field]?.referenced_column_name;
 if (cell == null || !referencedColumnName) return {};
-return { [referencedColumnName]: typeof cell === 'object' ? cell[referencedColumnName] : cell };
+const value = typeof cell === 'object' ? cell[referencedColumnName] : cell;
+if (value == null) return {};
+return { [referencedColumnName]: value };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('returns referenced column param when FK is an object', () => {
component.selectedRow = {
...baseRow,
record: { user_id: { id: 42, name: 'alice' } },
} as any;
expect(component.getForeignKeyQueryParams('user_id')).toEqual({ id: 42 });
it('returns referenced column param when FK is an object', () => {
component.selectedRow = {
...baseRow,
record: { user_id: { id: 42, name: 'alice' } },
} as any;
expect(component.getForeignKeyQueryParams('user_id')).toEqual({ id: 42 });
});
it('returns {} when FK object referenced column is null', () => {
component.selectedRow = {
...baseRow,
record: { user_id: { id: null, name: 'alice' } },
} as any;
expect(component.getForeignKeyQueryParams('user_id')).toEqual({});
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts`
around lines 84 - 89, The current getForeignKeyQueryParams implementation
returns an object like { id: null } for object FKs whose referenced id is null;
change getForeignKeyQueryParams to guard against null/undefined referenced
values (e.g., if selectedRow.record[field] is an object but its referenced key
value is null/undefined then return null/undefined instead of an object) so no
broken query is generated, and add a unit test in
db-table-row-view.component.spec.ts mirroring the existing positive case but
with record: { user_id: { id: null, name: 'alice' } } and assert that
getForeignKeyQueryParams('user_id') returns null/undefined (matching your chosen
sentinel).

Comment on lines +20 to +27
readonly onForeignKeyClick = output<{ foreignKey: any; value: string }>();

handleForeignKeyClick($event): void {
$event.stopPropagation();
if (this.relations() && this.value()) {
this.onForeignKeyClick.emit({
foreignKey: this.relations(),
value: this.value()
});
}
}
handleForeignKeyClick($event): void {
$event.stopPropagation();
if (this.relations() && this.value() != null) {
this.onForeignKeyClick.emit({
foreignKey: this.relations(),
value: this.value(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the actual base field value input type and FK click consumers before finalizing ForeignKeyValue.
rg -nP -C4 '\b(value|readonly value)\s*=\s*input\b|\bvalue\s*\(\)' --iglob '*.ts'
rg -nP -C3 '\bonForeignKeyClick\b|handleForeignKeyClick' --iglob '*.ts' --iglob '*.html'

Repository: rocket-admin/rocketadmin

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get the full foreign-key.component.ts file
cat frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.ts

# Get the base class definition
fd -t f 'base-table-display-field.component.ts' --type f

Repository: rocket-admin/rocketadmin

Length of output: 1491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get base class
cat frontend/src/app/components/ui-components/table-display-fields/base-table-display-field/base-table-display-field.component.ts

# Search for onForeignKeyClick usage in templates
fd -t f -name '*.html' | xargs grep -l 'onForeignKeyClick' 2>/dev/null || true

Repository: rocket-admin/rocketadmin

Length of output: 996


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for onForeignKeyClick consumers in HTML and TS
rg -i 'onForeignKeyClick' --type ts --type html -C2

# Also check the template for this component
cat frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.html

Repository: rocket-admin/rocketadmin

Length of output: 5356


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what types are actually passed as element[column] in consumers
rg 'handleForeignKeyView|getCellValue' frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts -A3 -B1

Repository: rocket-admin/rocketadmin

Length of output: 402


Type the FK click payload and event parameter explicitly.

Line 20 uses any for the foreignKey property, and line 22 leaves $event untyped. Create a typed event interface and declare the event parameter as MouseEvent. Also cache relations() and value() before the guard so the narrowed types flow to the emit call.

Suggested fix
+type ForeignKeyValue = string | number;
+
+interface ForeignKeyClickEvent {
+	foreignKey: TableForeignKey;
+	value: ForeignKeyValue;
+}
+
 `@Component`({
@@
-	readonly onForeignKeyClick = output<{ foreignKey: any; value: string }>();
+	readonly onForeignKeyClick = output<ForeignKeyClickEvent>();
 
-	handleForeignKeyClick($event): void {
-		$event.stopPropagation();
-		if (this.relations() && this.value() != null) {
+	handleForeignKeyClick(event: MouseEvent): void {
+		event.stopPropagation();
+		const foreignKey = this.relations();
+		const value = this.value();
+		if (foreignKey && value != null) {
 			this.onForeignKeyClick.emit({
-				foreignKey: this.relations(),
-				value: this.value(),
+				foreignKey,
+				value,
 			});
 		}
 	}

Adjust ForeignKeyValue if the base field's value type supports additional types beyond string | number. Per coding guidelines, **/*.{ts,tsx} must avoid any, use specific types, and annotate function parameters and return types.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
readonly onForeignKeyClick = output<{ foreignKey: any; value: string }>();
handleForeignKeyClick($event): void {
$event.stopPropagation();
if (this.relations() && this.value()) {
this.onForeignKeyClick.emit({
foreignKey: this.relations(),
value: this.value()
});
}
}
handleForeignKeyClick($event): void {
$event.stopPropagation();
if (this.relations() && this.value() != null) {
this.onForeignKeyClick.emit({
foreignKey: this.relations(),
value: this.value(),
type ForeignKeyValue = string | number;
interface ForeignKeyClickEvent {
foreignKey: TableForeignKey;
value: ForeignKeyValue;
}
readonly onForeignKeyClick = output<ForeignKeyClickEvent>();
handleForeignKeyClick(event: MouseEvent): void {
event.stopPropagation();
const foreignKey = this.relations();
const value = this.value();
if (foreignKey && value != null) {
this.onForeignKeyClick.emit({
foreignKey,
value,
});
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.ts`
around lines 20 - 27, Replace the untyped payload and event in
onForeignKeyClick/handleForeignKeyClick by defining a specific interface (e.g.,
ForeignKeyClickPayload with foreignKey: ForeignKeyValue and value: string |
number | ... as appropriate) and change the handler signature to
handleForeignKeyClick(event: MouseEvent): void; inside handleForeignKeyClick
cache const relations = this.relations(); const value = this.value(); before the
null/guard so those narrowed types are used in the emit call
this.onForeignKeyClick.emit({ foreignKey: relations, value }); also update the
ForeignKeyValue type definition to include any additional allowed value types
beyond string|number and remove all uses of any.

Appends a "— empty" option to the autocomplete suggestions whenever the
column is nullable, sourced from either structure.allow_null or
widget_params.allow_null. Selecting it emits null, clears the related
link, and hides the "open related record" icon. The option sits at the
end of the list so mat-autocomplete's autoActiveFirstOption highlights
the real current selection, not the empty placeholder.

Also threads structure through the widget-branch ndc-dynamic in the
row-edit template so the flag reaches the widget when an explicit
Foreign_key widget is configured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gugu gugu changed the title db table view: guard row actions against null primary keys foreign key fix Apr 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts (3)

176-183: Clear currentFieldQueryParams when the typed string matches the null option.

When currentDisplayedString === '— empty', this branch sets currentFieldValue = null and emits null, but leaves any previously set currentFieldQueryParams intact. It is harmless in the UI today because the template hides the link when currentFieldValue == null, but it diverges from updateRelatedLink, which explicitly clears the query params on null selection. Aligning them prevents a future template change from leaking a stale href.

♻️ Proposed fix
 		if (currentRow !== undefined) {
 			this.currentFieldValue = currentRow.fieldValue;
+			if (currentRow.isNullOption) {
+				this.currentFieldQueryParams = undefined;
+			} else {
+				this.currentFieldQueryParams = currentRow.primaryKeys;
+			}
 			this.onFieldChange.emit(this.currentFieldValue);
 		} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts`
around lines 176 - 183, In fetchSuggestions(), when the typed string matches the
null option (currentDisplayedString === '— empty') and you set currentFieldValue
= null and emit via onFieldChange, also clear currentFieldQueryParams (e.g., set
it to undefined or {}) to mirror updateRelatedLink behavior and avoid leaving a
stale query param; update the branch in fetchSuggestions() that handles the
null-selection path to reset currentFieldQueryParams whenever currentFieldValue
is set to null.

272-274: Private method should be underscore-prefixed.

Other private members in this class (_tables, _connections, _debounceTimer) follow the convention; withNullOption should too.

-	private withNullOption(suggestions: Suggestion[]): Suggestion[] {
-		return this.allowsNull() ? [...suggestions, nullSuggestion] : suggestions;
-	}
+	private _withNullOption(suggestions: Suggestion[]): Suggestion[] {
+		return this.allowsNull() ? [...suggestions, nullSuggestion] : suggestions;
+	}

Update the three call sites (this.withNullOption(...) at Lines 138, 201, 209) accordingly.

As per coding guidelines: "Prefix private members with underscore (e.g., _privateMethod, _dataService)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts`
around lines 272 - 274, Rename the private method withNullOption to
_withNullOption and update all internal call sites that invoke
this.withNullOption(...) to use this._withNullOption(...); keep the method
marked private and preserve its behavior (returning allowsNull() ?
[...suggestions, nullSuggestion] : suggestions) so other class members like
_tables, _connections, and _debounceTimer follow the underscore-prefixed private
naming convention.

261-270: Consider null instead of undefined for currentFieldQueryParams.

[queryParams] on routerLink expects Params | null | undefined, so both work, but the field is typed Record<string, unknown> (non-optional) and is read in the template as a truthy-ish binding. For internal type consistency, assigning null (and typing the field Record<string, unknown> | null) is cleaner. Minor; the template already gates visibility on currentFieldValue != null, so there is no runtime user-visible issue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts`
around lines 261 - 270, The updateRelatedLink handler and the field
currentFieldQueryParams should use null instead of undefined for clearer typing
and template checks: in updateRelatedLink (and any other places setting
currentFieldQueryParams) assign null when clearing the selection (e.g., when
selected?.isNullOption) and set the field to selected.primaryKeys otherwise;
change the currentFieldQueryParams declaration/type from Record<string, unknown>
to Record<string, unknown> | null and initialize it to null so the template and
other code read a consistent nullable value; keep currentFieldValue and
onFieldChange logic as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts`:
- Around line 176-183: In fetchSuggestions(), when the typed string matches the
null option (currentDisplayedString === '— empty') and you set currentFieldValue
= null and emit via onFieldChange, also clear currentFieldQueryParams (e.g., set
it to undefined or {}) to mirror updateRelatedLink behavior and avoid leaving a
stale query param; update the branch in fetchSuggestions() that handles the
null-selection path to reset currentFieldQueryParams whenever currentFieldValue
is set to null.
- Around line 272-274: Rename the private method withNullOption to
_withNullOption and update all internal call sites that invoke
this.withNullOption(...) to use this._withNullOption(...); keep the method
marked private and preserve its behavior (returning allowsNull() ?
[...suggestions, nullSuggestion] : suggestions) so other class members like
_tables, _connections, and _debounceTimer follow the underscore-prefixed private
naming convention.
- Around line 261-270: The updateRelatedLink handler and the field
currentFieldQueryParams should use null instead of undefined for clearer typing
and template checks: in updateRelatedLink (and any other places setting
currentFieldQueryParams) assign null when clearing the selection (e.g., when
selected?.isNullOption) and set the field to selected.primaryKeys otherwise;
change the currentFieldQueryParams declaration/type from Record<string, unknown>
to Record<string, unknown> | null and initialize it to null so the template and
other code read a consistent nullable value; keep currentFieldValue and
onFieldChange logic as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ebb78d32-39cb-48ca-99bc-3735d1119955

📥 Commits

Reviewing files that changed from the base of the PR and between fc516b4 and b210f09.

📒 Files selected for processing (5)
  • frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html
  • frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.css
  • frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.html
  • frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.spec.ts
  • frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.ts
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/app/components/ui-components/record-edit-fields/foreign-key/foreign-key.component.css

@lyubov-voloshko lyubov-voloshko merged commit f42cea1 into main Apr 23, 2026
16 of 17 checks passed
@lyubov-voloshko lyubov-voloshko deleted the feature/guard-null-primary-key branch April 23, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants