feat(versioning): role-aware family collapse, family-latest resolver, EntityVersionPicker (#442 phase 3)#448
Closed
larsgeorge-db wants to merge 4 commits into
Closed
Conversation
larsgeorge-db
added a commit
that referenced
this pull request
May 27, 2026
…ck (#442 phase 3 follow-up) Two issues uncovered during Playwright smoke testing of #448: 1. **camelCase serialization on list endpoints.** The ``DataContractSummary`` and ``DataProduct`` Pydantic models declared their PRD-#442 fields (``versionFamilyId``, ``versionCount``, ``parentContractId/parentProductId``, ``baseName``, ``changeSummary``, ``draftOwnerId``) with snake_case ``alias``-es. FastAPI's default ``response_model_by_alias=True`` then emitted snake_case, but the FE TS types use camelCase — so ``row.versionCount`` was silently ``undefined`` and the count badge never rendered. Switch each field to use Pydantic v2's ``serialization_alias`` so the wire format is camelCase regardless of the by-alias toggle, while the existing snake_case ``alias`` keeps ORM ``from_attributes=True`` reading working for both the SQLAlchemy attribute and snake_case JSON input. 2. **Picker hid valid published versions inside elevated families.** With role-aware ranking landed in #448, a family whose newest row is a draft now collapses to that draft as its representative. The EntityVersionPicker only ever saw collapsed reps, so a port-link dialog scoped to ``statusFilter=['active','approved','certified']`` would silently drop families that had a perfectly valid published version available — e.g. Customer Data Contract disappeared from the POS Transaction Stream port picker after a 2.0.0 draft was created. When ``statusFilter`` is supplied, fetch with ``?include_history=true`` and collapse client-side after status filtering, preferring the newest matching version per family. ``versionCount`` is computed BEFORE collapse so the badge still reflects the full family size. Smoke-tested end-to-end via Playwright MCP against a multi-version family. Original v1.0.0 (active) is now selectable, picker write persists the entity-pinned contract id correctly.
This was referenced May 27, 2026
Open
POST /data-contracts/{id}/clone returns 400 despite successful clone (Pydantic v2 ServerConfig)
#455
Closed
Introduce version_family_id as a canonical, immutable grouping key on
both data_contracts and data_products so all versions of an entity are
retrievable via a single indexed equality lookup. Replaces the brittle
parent-walk + base_name heuristics with a stable family identity that
survives renames and is propagated through every clone path.
Backend
- Alembic j1_add_version_family_id: add column, backfill via recursive
parent-walk CTE, then mark NOT NULL on both tables.
- before_insert event listeners on DataContractDb/DataProductDb seed
id and version_family_id so the NOT NULL invariant holds on any
insert path (repo, demo loader, direct ORM construction).
- Repositories get get_family_versions and list_family_representatives
with personal-draft visibility filtering and a window-function-based
latest-visible collapse.
- All clone paths (create_new_version, ContractCloner,
clone_product_for_new_version) propagate version_family_id from the
source. ContractCloner no longer mutates name with a _v<version>
suffix — name stays stable across the family, version is its own
column.
- GET /api/data-contracts/{id}/versions rewritten and new
GET /api/data-products/{id}/versions added — both visibility-aware
and returning a lightweight EntityVersionRow shape.
Frontend
- versionFamilyId on DataContract, DataContractSummary, DataProduct.
- New shared components/common/version-selector.tsx generic over
entityKind: 'contract' | 'product'.
- New components/common/version-navigator.tsx wraps prev/next chevrons
+ selector and hides itself for single-version families.
- Wired into data-contract-details and data-product-details, replacing
the contract-specific implementation.
Deferred to follow-up PRs: list-view collapse with include_history
toggle, unified EntityVersionPicker (entity vs family-follow-latest),
and *_family_id reference-storage columns on join tables.
See docs/prds/prd-version-family-and-unified-selector.md (PRD #442).
…se 2) Make the data-contracts and data-products list views show one row per version_family_id by default, with a "Show all versions" toggle that expands every visible version. Each collapsed row surfaces a small "N versions" badge so users can see at a glance which entities have history, without paying the row-count cost. Builds on PRD #442 / PR #446 (Phase 1). Backend - DataContractSummary + DataProduct API models gain a `versionCount` (alias `version_count`) field. Populated only on the collapsed view so the expanded view never lies about per-row counts. - DataContractsManager._query_contracts: drop the legacy base_name Python-side dedupe in favor of canonical version_family_id grouping, and return (rows, family_counts) so the builder can attach counts without a second query. Same shape feeds both collapsed and include_history paths. - DataContractsManager._build_contract_summaries now accepts an optional family_counts map and emits parent/family/draft/changeSummary fields that were previously left null on the summary even though they exist on the row. - list_contracts_from_db and the /data-contracts route propagate the include_history flag end-to-end (route was already accepting it). - DataProductsManager.list_products: collapse by version_family_id after the visibility cascade, attach version_count to the surviving row. include_history=True bypasses the collapse and suppresses the count. - /data-products route surfaces include_history. Frontend - DataContractListItem and DataProduct gain `versionCount`. - New components/common/version-count-badge.tsx — compact pill that hides itself for single-version families and otherwise renders a click-through history hint next to the version cell. - data-contracts.tsx and data-products.tsx: family badge next to the version column + "Show all versions" switch in the toolbar; both list-fetch paths plumb include_history through the query string. Tests - 5 new unit tests cover the collapse default, the family-count emission, include_history bypass, and product-side parity. Note: existing dev uvicorn instances have a stale --reload-dir pointing at the pre-Phase-1 src/api layout (see /tmp/backend.log statreload trace). Restart the backend to pick up the new include_history wiring locally.
bc07763 to
fb74752
Compare
… EntityVersionPicker (PRD #442 phase 3) Phase 3 of the unified version-family work. Three vertically-aligned slices: 1. **Role-aware visibility ranking** — adds `src/common/version_visibility.py` with elevated vs consumer status rank tables and a `collapse_by_family` helper. Plumbed into the contracts and products list managers so the family representative is picked according to the caller's role: * Consumer: ACTIVE > DEPRECATED (drafts hidden entirely) * Owner / team member / subscriber / admin: DRAFT > PROPOSED > UNDER_REVIEW > APPROVED > ACTIVE > DEPRECATED Subscription-based elevation is wired in for products via the existing `DataProductSubscriptionDb` table; contracts use ownership only (no subscription table today). 2. **Family-latest resolver endpoints** — `GET /api/data-contracts/families/{family_id}/latest` and the products counterpart. These resolve a family-follow-latest reference to a concrete row using the same role-aware rank, so any caller storing only a family id can read back the right version. 3. **EntityVersionPicker** — new shared component in `components/common/entity-version-picker.tsx`. Combobox over the collapsed list endpoint (one row per family with inline version badges), optional Entity-pinned / Family-follow-latest scope toggle, and an inline sub-picker for refining the version when pinning. The `link-contract-to-port-dialog` (closes #69) is migrated as the first call site, with `allowedScopes=['entity']` until the output-port reference-storage slice lands. Tests added: * `test_version_visibility.py` — 12 cases covering status visibility, rank ordering, tie-breaks, and the consumer-only / admin-only edges. * `test_version_family.py` — two new manager-level cases proving consumers see the active rep while team members of the same family see the in-flight draft. Note: the dev backend reloader has a pre-existing misconfiguration (noted in PR #447). Logic is covered by unit tests; live verification requires a manual `uvicorn` restart.
…ck (#442 phase 3 follow-up) Two issues uncovered during Playwright smoke testing of #448: 1. **camelCase serialization on list endpoints.** The ``DataContractSummary`` and ``DataProduct`` Pydantic models declared their PRD-#442 fields (``versionFamilyId``, ``versionCount``, ``parentContractId/parentProductId``, ``baseName``, ``changeSummary``, ``draftOwnerId``) with snake_case ``alias``-es. FastAPI's default ``response_model_by_alias=True`` then emitted snake_case, but the FE TS types use camelCase — so ``row.versionCount`` was silently ``undefined`` and the count badge never rendered. Switch each field to use Pydantic v2's ``serialization_alias`` so the wire format is camelCase regardless of the by-alias toggle, while the existing snake_case ``alias`` keeps ORM ``from_attributes=True`` reading working for both the SQLAlchemy attribute and snake_case JSON input. 2. **Picker hid valid published versions inside elevated families.** With role-aware ranking landed in #448, a family whose newest row is a draft now collapses to that draft as its representative. The EntityVersionPicker only ever saw collapsed reps, so a port-link dialog scoped to ``statusFilter=['active','approved','certified']`` would silently drop families that had a perfectly valid published version available — e.g. Customer Data Contract disappeared from the POS Transaction Stream port picker after a 2.0.0 draft was created. When ``statusFilter`` is supplied, fetch with ``?include_history=true`` and collapse client-side after status filtering, preferring the newest matching version per family. ``versionCount`` is computed BEFORE collapse so the badge still reflects the full family size. Smoke-tested end-to-end via Playwright MCP against a multi-version family. Original v1.0.0 (active) is now selectable, picker write persists the entity-pinned contract id correctly.
2462f44 to
3a62825
Compare
larsgeorge-db
added a commit
that referenced
this pull request
May 28, 2026
…455) POST /api/data-contracts/{id}/clone returned HTTP 400 ("Invalid contract data") even though the cloned version was successfully written to the database. Root cause was a two-layer Pydantic v2 incompatibility in the response serialization path: 1. `DataContractRead.model_validate(new_contract).model_dump()` did not pass `from_attributes=True`, so the flag was not propagated into nested model validation. The nested `ServerConfig` (no `from_attributes`) then rejected the ORM `DataContractServerDb` rows it received via the `servers` relationship -> ValidationError -> caught as 400. 2. Even with `from_attributes=True` propagating, `ServerConfig.properties` (typed `Dict[str, Any]`) cannot validate the SQLAlchemy `InstrumentedList` of `DataContractServerPropertyDb` rows that the relationship returns. This was a latent second-layer bug exposed by fix #1. Fixes: - Pass `from_attributes=True` at the four call sites that validate `DataContractRead` from ORM instances (clone, version history, personal drafts). - Add `from_attributes = True` to `ServerConfig.Config` so any future caller validating a nested `DataContractServerDb` works without remembering to pass the flag. - Add a `@field_validator('properties', mode='before')` on `ServerConfig` that flattens a list of `{key, value}` rows/dicts into the expected `Dict[str, Any]`. Plain dict input keeps working unchanged. Regression coverage in `test_data_contracts_api_models.py`: - `DataContractRead.model_validate(orm_row, from_attributes=True)` now round-trips populated `servers`, `team`, `roles`, `support`, `pricing`, and `sla_properties` collections. - The `ServerConfig.properties` coercion is exercised directly for object-rows, plain dict, and list-of-`{key,value}` shapes. Stacked on PR #448 (PRD #442 phase 3); closes #455.
fb74752 to
0fd78e4
Compare
larsgeorge-db
added a commit
that referenced
this pull request
May 28, 2026
…ck (#442 phase 3 follow-up) Two issues uncovered during Playwright smoke testing of #448: 1. **camelCase serialization on list endpoints.** The ``DataContractSummary`` and ``DataProduct`` Pydantic models declared their PRD-#442 fields (``versionFamilyId``, ``versionCount``, ``parentContractId/parentProductId``, ``baseName``, ``changeSummary``, ``draftOwnerId``) with snake_case ``alias``-es. FastAPI's default ``response_model_by_alias=True`` then emitted snake_case, but the FE TS types use camelCase — so ``row.versionCount`` was silently ``undefined`` and the count badge never rendered. Switch each field to use Pydantic v2's ``serialization_alias`` so the wire format is camelCase regardless of the by-alias toggle, while the existing snake_case ``alias`` keeps ORM ``from_attributes=True`` reading working for both the SQLAlchemy attribute and snake_case JSON input. 2. **Picker hid valid published versions inside elevated families.** With role-aware ranking landed in #448, a family whose newest row is a draft now collapses to that draft as its representative. The EntityVersionPicker only ever saw collapsed reps, so a port-link dialog scoped to ``statusFilter=['active','approved','certified']`` would silently drop families that had a perfectly valid published version available — e.g. Customer Data Contract disappeared from the POS Transaction Stream port picker after a 2.0.0 draft was created. When ``statusFilter`` is supplied, fetch with ``?include_history=true`` and collapse client-side after status filtering, preferring the newest matching version per family. ``versionCount`` is computed BEFORE collapse so the badge still reflects the full family size. Smoke-tested end-to-end via Playwright MCP against a multi-version family. Original v1.0.0 (active) is now selectable, picker write persists the entity-pinned contract id correctly.
3 tasks
larsgeorge-db
added a commit
that referenced
this pull request
May 28, 2026
… EntityVersionPicker (PRD #442 phase 3) (#467) * feat(versioning): role-aware family collapse, family-latest resolver, EntityVersionPicker (PRD #442 phase 3) Phase 3 of the unified version-family work. Three vertically-aligned slices: 1. **Role-aware visibility ranking** — adds `src/common/version_visibility.py` with elevated vs consumer status rank tables and a `collapse_by_family` helper. Plumbed into the contracts and products list managers so the family representative is picked according to the caller's role: * Consumer: ACTIVE > DEPRECATED (drafts hidden entirely) * Owner / team member / subscriber / admin: DRAFT > PROPOSED > UNDER_REVIEW > APPROVED > ACTIVE > DEPRECATED Subscription-based elevation is wired in for products via the existing `DataProductSubscriptionDb` table; contracts use ownership only (no subscription table today). 2. **Family-latest resolver endpoints** — `GET /api/data-contracts/families/{family_id}/latest` and the products counterpart. These resolve a family-follow-latest reference to a concrete row using the same role-aware rank, so any caller storing only a family id can read back the right version. 3. **EntityVersionPicker** — new shared component in `components/common/entity-version-picker.tsx`. Combobox over the collapsed list endpoint (one row per family with inline version badges), optional Entity-pinned / Family-follow-latest scope toggle, and an inline sub-picker for refining the version when pinning. The `link-contract-to-port-dialog` (closes #69) is migrated as the first call site, with `allowedScopes=['entity']` until the output-port reference-storage slice lands. Tests added: * `test_version_visibility.py` — 12 cases covering status visibility, rank ordering, tie-breaks, and the consumer-only / admin-only edges. * `test_version_family.py` — two new manager-level cases proving consumers see the active rep while team members of the same family see the in-flight draft. Note: the dev backend reloader has a pre-existing misconfiguration (noted in PR #447). Logic is covered by unit tests; live verification requires a manual `uvicorn` restart. * fix(versioning): camelCase API output + picker include_history fallback (#442 phase 3 follow-up) Two issues uncovered during Playwright smoke testing of #448: 1. **camelCase serialization on list endpoints.** The ``DataContractSummary`` and ``DataProduct`` Pydantic models declared their PRD-#442 fields (``versionFamilyId``, ``versionCount``, ``parentContractId/parentProductId``, ``baseName``, ``changeSummary``, ``draftOwnerId``) with snake_case ``alias``-es. FastAPI's default ``response_model_by_alias=True`` then emitted snake_case, but the FE TS types use camelCase — so ``row.versionCount`` was silently ``undefined`` and the count badge never rendered. Switch each field to use Pydantic v2's ``serialization_alias`` so the wire format is camelCase regardless of the by-alias toggle, while the existing snake_case ``alias`` keeps ORM ``from_attributes=True`` reading working for both the SQLAlchemy attribute and snake_case JSON input. 2. **Picker hid valid published versions inside elevated families.** With role-aware ranking landed in #448, a family whose newest row is a draft now collapses to that draft as its representative. The EntityVersionPicker only ever saw collapsed reps, so a port-link dialog scoped to ``statusFilter=['active','approved','certified']`` would silently drop families that had a perfectly valid published version available — e.g. Customer Data Contract disappeared from the POS Transaction Stream port picker after a 2.0.0 draft was created. When ``statusFilter`` is supplied, fetch with ``?include_history=true`` and collapse client-side after status filtering, preferring the newest matching version per family. ``versionCount`` is computed BEFORE collapse so the badge still reflects the full family size. Smoke-tested end-to-end via Playwright MCP against a multi-version family. Original v1.0.0 (active) is now selectable, picker write persists the entity-pinned contract id correctly.
larsgeorge-db
added a commit
that referenced
this pull request
May 28, 2026
…455) POST /api/data-contracts/{id}/clone returned HTTP 400 ("Invalid contract data") even though the cloned version was successfully written to the database. Root cause was a two-layer Pydantic v2 incompatibility in the response serialization path: 1. `DataContractRead.model_validate(new_contract).model_dump()` did not pass `from_attributes=True`, so the flag was not propagated into nested model validation. The nested `ServerConfig` (no `from_attributes`) then rejected the ORM `DataContractServerDb` rows it received via the `servers` relationship -> ValidationError -> caught as 400. 2. Even with `from_attributes=True` propagating, `ServerConfig.properties` (typed `Dict[str, Any]`) cannot validate the SQLAlchemy `InstrumentedList` of `DataContractServerPropertyDb` rows that the relationship returns. This was a latent second-layer bug exposed by fix #1. Fixes: - Pass `from_attributes=True` at the four call sites that validate `DataContractRead` from ORM instances (clone, version history, personal drafts). - Add `from_attributes = True` to `ServerConfig.Config` so any future caller validating a nested `DataContractServerDb` works without remembering to pass the flag. - Add a `@field_validator('properties', mode='before')` on `ServerConfig` that flattens a list of `{key, value}` rows/dicts into the expected `Dict[str, Any]`. Plain dict input keeps working unchanged. Regression coverage in `test_data_contracts_api_models.py`: - `DataContractRead.model_validate(orm_row, from_attributes=True)` now round-trips populated `servers`, `team`, `roles`, `support`, `pricing`, and `sla_properties` collections. - The `ServerConfig.properties` coercion is exercised directly for object-rows, plain dict, and list-of-`{key,value}` shapes. Stacked on PR #448 (PRD #442 phase 3); closes #455.
larsgeorge-db
added a commit
that referenced
this pull request
May 28, 2026
…455) (#457) POST /api/data-contracts/{id}/clone returned HTTP 400 ("Invalid contract data") even though the cloned version was successfully written to the database. Root cause was a two-layer Pydantic v2 incompatibility in the response serialization path: 1. `DataContractRead.model_validate(new_contract).model_dump()` did not pass `from_attributes=True`, so the flag was not propagated into nested model validation. The nested `ServerConfig` (no `from_attributes`) then rejected the ORM `DataContractServerDb` rows it received via the `servers` relationship -> ValidationError -> caught as 400. 2. Even with `from_attributes=True` propagating, `ServerConfig.properties` (typed `Dict[str, Any]`) cannot validate the SQLAlchemy `InstrumentedList` of `DataContractServerPropertyDb` rows that the relationship returns. This was a latent second-layer bug exposed by fix #1. Fixes: - Pass `from_attributes=True` at the four call sites that validate `DataContractRead` from ORM instances (clone, version history, personal drafts). - Add `from_attributes = True` to `ServerConfig.Config` so any future caller validating a nested `DataContractServerDb` works without remembering to pass the flag. - Add a `@field_validator('properties', mode='before')` on `ServerConfig` that flattens a list of `{key, value}` rows/dicts into the expected `Dict[str, Any]`. Plain dict input keeps working unchanged. Regression coverage in `test_data_contracts_api_models.py`: - `DataContractRead.model_validate(orm_row, from_attributes=True)` now round-trips populated `servers`, `team`, `roles`, `support`, `pricing`, and `sla_properties` collections. - The `ServerConfig.properties` coercion is exercised directly for object-rows, plain dict, and list-of-`{key,value}` shapes. Stacked on PR #448 (PRD #442 phase 3); closes #455.
Collaborator
Author
|
Auto-closed by GitHub when stacked base branch |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 3 of the unified version-family work, stacked on #447. Closes #69 along the way.
Summary
Three vertically-aligned slices, all driven by PRD #442:
Role-aware visibility ranking — new
src/common/version_visibility.pymodule with elevated vs consumer status rank tables and acollapse_by_familyhelper. Wired into the contracts and products list managers, so the family representative is now picked according to caller role:ACTIVE > DEPRECATED(drafts hidden entirely)DRAFT > PROPOSED > UNDER_REVIEW > APPROVED > ACTIVE > DEPRECATEDSubscription-based elevation is wired in for products via the existing
DataProductSubscriptionDbtable; contracts use ownership-only elevation (no contract subscription table exists today).Family-latest resolver endpoints —
GET /api/data-contracts/families/{family_id}/latestand the products counterpart. These resolve a family-follow-latest reference to a concrete row using the same role-aware rank, so any caller storing only a family id can read back the right version.EntityVersionPicker — new shared component in
components/common/entity-version-picker.tsx. Combobox over the collapsed list endpoint (one row per family, inline version badges), optional Entity-pinned / Family-follow-latest scope toggle, and an inline sub-picker for refining the version when pinning. Thelink-contract-to-port-dialogis migrated as the first call site (closes [Feature]: UI support for contract versions #69) with `allowedScopes=['entity']` until the output-port reference-storage slice lands.Closes
Stacked on
Test plan
Notes
Remaining PRD scope (deferred)