feat(databases): render, protect, and filter relationship attributes in browse#1438
feat(databases): render, protect, and filter relationship attributes in browse#1438dawsontoth wants to merge 5 commits into
Conversation
…in browse Relationship attributes (@relationship) reported by describe_table have no explicit flag; detect them by their type/elements naming a sibling table (Harper 4.7 shape — 5.1 omits them entirely, so this stays dormant there). - Browse resolves relationship columns via nested get_attributes selects ({name, select: [relatedPk]} before '*') and renders them as link chips to the related record instead of a blank box (#1278) - Chip links deep-link with ?filters={pk: value}; the browse page applies filters from the URL search on arrival - Add/edit record JSON and random-record generation exclude relationship/ computed attributes — the server rejects writes that assign them, even null (#1202, #1317) - Relationship columns are filterable by sub-property: a picker inserts '.property', translated to search_attribute: [column, subProperty] which the server runs as a join against the related table (#1286) - Plain cells stringify objects/arrays and booleans instead of rendering '[object Object]' or nothing Verified against a local Harper 4.7.28 (features work end-to-end) and the stage 5.1.18 cluster (describe omits relationships; behavior unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for relationship and computed (synthetic) attributes in the database table view, adapting to differences across Harper server versions. Key updates include stripping read-only synthetic attributes from the record add/edit modals to prevent server-side write rejections, rendering relationship values as interactive link badges via a new RelationshipCell component, and enabling sub-property filtering on related tables with a dropdown picker. Additionally, the search APIs were updated to support nested selects for resolving relationships, and comprehensive unit tests were added to verify these behaviors. There are no review comments to address, and the implementation is clean and well-tested.
…ip column The relationship chips show the same key values the foreign key stores (the related record's primary key), so showing both 'product' and 'productId' is redundant. The foreign key backing a relationship is inferred by naming convention — product→productId/product_id, pets→petIds (describe_table does not expose the @relationship from/to mapping) — and hidden by default. It stays listed in the Columns picker, and re-showing it stores an explicit override that wins over the default, so unconventional pairings or raw-key sorting are one click away. Reverse (to:) relationships have no local key and collapse nothing. Record add/edit JSON is unaffected: the foreign key remains the writable field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Added per follow-up feedback: the foreign-key column backing a relationship is now collapsed by default — |
…e link chips
Tables created before Harper 5 carry legacy relationship attributes whose
describe output is just {type: 'array'} with no element type (e.g.
Albums.tracks): detect those by matching the attribute name against sibling
table names (tracks → Tracks/Track). They can't be resolved via nested
selects, so the cell instead links to the related table filtered by the
reverse foreign key — inferred from a back-reference to-one relationship on
the related table (RelReview.product → productId) or the <singular owner
table>Id naming convention (Tracks.albumId) — landing on exactly this row's
related records. Resolved to-many cells reuse the same link for '+N more'.
Chips now use the app's link blue (blue / blue-300 in dark) instead of
outline's text-foreground, so they read as links in both themes instead of
blending into neighboring values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Two more follow-ups pushed: Reverse-key links for legacy relationships. Tables created under older Harper (e.g. Readable chips. The chips previously used the badge |
DavidCockerill
left a comment
There was a problem hiding this comment.
The feature reviews well and I'm ready to approve — one build issue to clear first, plus one open question.
🔴 Build is red on the current head — fix before merge
Commit 6eb8b07 (one past where I first read the PR) added a required resolvable: boolean to RelationshipAttributeInfo but didn't update the fixtures that construct that interface, so tsc fails with 8 errors and Verify PR is failing. Offending fixtures: RelationshipCell.test.ts:5 and the relationshipInfo fixture in getSearchByConditions.test.ts (~lines 338–406). The PR body's "tsc … clean" note predates that commit. Fix: add resolvable (and reverseForeignKey where needed) to those fixtures, or make the field optional. Ping me when it's green and I'll approve — the rest checks out.
Open question (non-blocking): collapsedForeignKeyNames can hide a real column by naming convention
In relationshipAttributes.ts, if a table has a scalar FK productId and a to-one relationship product that's actually a reverse relationship reading from a differently-named key, relationshipForeignKeyName('product', …) matches productId purely by the product+id convention and collapses it — even though the relationship doesn't read from that column. So a real, independent stored column is hidden by default, and its relationship chips could show different values than the hidden productId.
I've graded this a suggestion, not a blocker, because it's purely a default-visibility concern: the column def/header/data are all still present, it's one click to restore via the Columns picker, the choice persists (an explicit true in storedColumnVisibility is spread after the collapsed defaults, so it wins — verified against PickColumnsDropdown + the merge useMemo), and CSV export uses raw records so the FK still exports. So: is the false-positive-hide acceptable as-is? The 6eb8b07 reverse-key inference looks like it could gate the collapse to non-reverse relationships if you want to tighten it, or a small "a column was auto-hidden" hint would remove the surprise.
Everything else I checked was clean: filter value casting (Date/Boolean sub-properties, the numeric >= 4 comparator path), useMemo deps / no render loop, per-cell cost is cheap with no N+1 (relationships resolve in the single get_attributes request), the EditTableRowModal synthetic-attr strip, and defensive scalar handling in relationshipKeyValues. One cosmetic nit: the SubPropertyPicker regex value.replace(/^\s*\.\S*\s*/, '') can eat an adjacent comparator if you type .rat mid-word with no space — self-corrects on the next keystroke, not worth changing.
|
There are a few more fixes and enhancements coming, but I exhausted my allotment. Will update the PR soon. |
… 5.1 Harper 5.1 omits @relationship attributes from describe_table entirely (an unintentional regression from the v5.0 rewrite: describe now reads the persisted attribute registry, which never holds runtime-only relationship attrs — traced to Harper's RE-7/#1183, no server flag to re-surface them). So browse also parses the component schema.graphql files (get_components + get_component_file) to learn relationships — the only source on 5.1, and authoritative for the exact from/to key mappings even on 4.7. - schemaRelationships.ts: fetch + parse schema files into database->table-> relationship declarations (reuses the applications schema parser) - getRelationshipInfoMap merges describe-detected and schema-declared relationships; foreignKeyAttribute is now exact (from:) when declared - schema-only relationships (absent from describe) render as a synthesized column: to-one reads the stored foreign key from the row; to-many links to the related table filtered by the reverse key (to:) - filtering a relationship by its related primary key queries the local foreign key directly (indexed; works on 5.1's ops API which can't join) - clearing URL filters (nav away / back to bare table) now clears the stale filter inputs, keeping them in step with appliedSearchConditions Verified end-to-end on the live 5.1.18 Anvils cluster: Product shows a 'category' chip (from stored categoryId), Category shows a 'products' reverse-link, both navigations filter correctly. Full suite 1308 pass; tsc/oxlint/dprint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Relationship columns now work on Harper 5.1 (via schema files)The reason
The fix (studio-side, works regardless of Harper version)Browse now also reads the component A schema-only relationship (absent from describe) renders as a synthesized column:
Also fixed a stale-filter-input bug this surfaced: navigating away from / back to a table now clears the filter inputs in step with the applied conditions. Verified end-to-end on the live 5.1.18 Anvils cluster (no mocks)
Worth filing the describe regression upstream with the Harper team (it also breaks nested GraphQL queries after any reset/hot-reload, per RE-7). |
Two review fixes: - Build was red: commit 6eb8b07 added a required `resolvable` to RelationshipAttributeInfo but left the RelationshipCell.test.ts and getSearchByConditions.test.ts fixtures constructing it without the field, so `tsc -b` failed with 8 errors. (`npx tsc --noEmit` missed them because the root tsconfig excludes tests; `tsc -b`, which CI runs, includes them.) Fixed the fixtures and added coverage for the related-primary-key filter shortcut. - collapsedForeignKeyNames could hide a real column by naming-convention guess (a to-one 'product' collapsing an independent 'productId' that the relationship doesn't actually read from). foreignKeyAttribute is now taken ONLY from an exact schema `@relationship(from:)` — never guessed — so column collapse, unresolved-cell rendering, and the PK filter shortcut only ever act on the column the relationship truly reads. Describe-detected relationships resolve their values via nested select, so they never needed the guess. Full suite 1310 pass; tsc -b / oxlint / dprint clean. Live 5.1 behavior unchanged (schema is present there, so the exact from: key still drives collapse). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@DavidCockerill thanks — both addressed in 🔴 Build (blocker) — fixed. Commit Open question (collapse false-positive) — tightened, not left as-is. You're right that a convention guess shouldn't hide a real column. Cosmetic nits (the 🤖 Addressed by Claude Code |
Closes #1278, closes #1202, closes #1317, closes #1286.
What
All four open relationship issues on the instance database browse page:
+N more) instead of a blank box. Clicking a chip deep-links to the related table with?filters={pk: value}; the browse page now applies filters carried in the URL search, so the chip genuinely lands on "the item in the other table".@computedattributes. The server rejects any write whose record merely contains one of those keys ("Computed property X may not be directly assigned a value", even fornull), so they must never appear in editor JSON..rating >= 4translates tosearch_attribute: ["reviews", "rating"], which Harper executes as a join against the related table. The existing comparator syntax (>=,*suffix,&chaining) composes with it, and values are cast by the sub-property's type.Also fixes plain-cell rendering while in there: objects/arrays stringify as JSON and booleans render as text (previously multi-element arrays rendered as
[object Object],[object Object]andfalserendered as nothing).How relationships are detected
describe_tablehas no explicit relationship flag. A to-one relationship reportstype: "<RelatedTable>"; a to-many reportstype: "array", elements: "<RelatedTable>"— so an attribute is treated as a relationship when its type/elements names a sibling table in the same database (functions/relationshipAttributes.ts). Resolution works by naming the attributes inget_attributeswith nested selects ({name, select: [relatedPk]}) before'*'— a leading'*'makes the server return raw records and ignore the rest of the list.Version behavior (verified empirically)
type/elementsname the table)["rel","sub"]join conditions filter, writes containing relationship keys rejectBrowser-verified on the worktree dev server against the Anvils cluster: chips render, chip → related-table-with-filter navigation works end to end, the sub-property picker emits the verified wire payload, the Add-record skeleton omits the relationship field, and plain tables (catalog.Product, data.Albums) render unchanged. Test tables/component used for verification were dropped afterwards.
Tests
New unit coverage for relationship detection (
relationshipAttributes.test.ts), join-path filter translation (getSearchByConditions.test.ts), column construction (formatBrowseDataTableHeader.test.ts), and cell key extraction (RelationshipCell.test.ts). Full suite: 194 files / 1288 tests pass;tsc, oxlint, and dprint clean.🤖 Generated with Claude Code