feat(BA-5814): AppConfigPolicy foundation (data / repository / service / adapter, bulk-aware)#11266
Draft
jopemachine wants to merge 26 commits intoBA-5822from
Draft
feat(BA-5814): AppConfigPolicy foundation (data / repository / service / adapter, bulk-aware)#11266jopemachine wants to merge 26 commits intoBA-5822from
jopemachine wants to merge 26 commits intoBA-5822from
Conversation
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
Member
Author
|
Please write repository tests following the existing code conventions. |
Member
Author
|
Repository tests added (real-DB |
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 24, 2026
3 tasks
jopemachine
commented
Apr 24, 2026
jopemachine
commented
Apr 27, 2026
jopemachine
commented
Apr 27, 2026
jopemachine
commented
Apr 27, 2026
…s + ORM/data types First slice of the BEP-1052 foundation: schema, ORM models, and data types. Repositories follow in subsequent commits. - Migration: create `app_config_policies` (config_name UNIQUE) and `app_config_fragments` (UNIQUE on (scope_type, scope_id, name); FK name → app_config_policies.config_name with NO ACTION default). Stacks on top of the legacy-drop migration in BA-5822. - Data types: AppConfigScopeType (PUBLIC / DOMAIN / DOMAIN_USER_DEFAULTS / USER), AppConfigFragmentKey, AppConfigFragmentData, AppConfigPolicyData. No `user_writable` field — user-path writes are intentionally not supported in this iteration. - ORM: AppConfigFragmentRow + AppConfigPolicyRow, both with `to_data()`.
Second slice of the BEP-1052 foundation: repository layer with single-SQL merge resolution and the standard 6-op CRUD shape. - AppConfigPolicyRepository: get / get_by_id / create / update / purge / search. Update keeps `config_name` immutable per BEP-1052 §1; the FK on app_config_fragments.name is the defense-in-depth backstop for purge. - AppConfigFragmentRepository: scope-bound CRUD on AppConfigFragmentKey (`get / get_by_id / create / update / purge`) plus scope-bound `search` and cross-scope `admin_search`. Plus the merged-view reads (`app_config`, `search_app_configs`, `admin_search_app_configs`) — the single-doc path is implemented; the search variants are placeholders that land with the GraphQL surface in subsequent issues. - DB source `_merge_chain` helper joins app_config_policies in one SQL to derive the chain (no separate policy lookup round-trip), matching the pattern in BEP-1052 §5. - Wire AppConfigFragmentRepositories / AppConfigPolicyRepositories into the central Repositories container.
Resolves BA-5814.
Address PR review:
- Rename `policy()` / `policy_by_id()` → `get` / `get_by_id` (and the
same on AppConfigFragmentRepository).
- Split admin operations into a separate `*AdminRepository`:
- AppConfigPolicyAdminRepository: create / update / purge / search
- AppConfigFragmentAdminRepository: create / update / purge /
admin_search / admin_search_app_configs
Read-side `Repository` keeps `get` / `get_by_id` and (for fragments)
scope-bound `search` + the per-user `app_config` / `search_app_configs`
merged-view reads. Each repository pair is exposed via the per-domain
Repositories container.
- Add real-DB repository tests under tests/unit/manager/repositories/
for both AppConfigPolicy and AppConfigFragment, covering CRUD, the
required-policy invariant (FK violation when a fragment has no
matching policy), purge round-trip, and update-not-found behavior.
…rge_chain
Replace the bare `tuple[list[AppConfigFragmentData], Mapping[str,
Any] | None]` return with a frozen-slots dataclass `_MergedChain
{ fragments, config }`. Internal to db_source.py; callers re-shape
into `AppConfigData` with the `(user_id, name)` they were resolving.
…er classes, make extra_config / updated_at nullable
Address PR review:
- `id`: replace `pgsql.UUID(as_uuid=True)` with the project-wide
`GUID` type (matches the `account_manager/userprofile.py` pattern
and other rows in the codebase). Drop the redundant `"id"` column
name and `nullable=False` since GUID + primary_key cover both.
- `extra_config` (fragment): nullable with no default — the
fragment may not carry stored config when the policy lists scopes
the admin hasn't seeded yet. `_merge_chain` skips None
contributions instead of treating them as `{}`.
- `updated_at` (fragment + policy): nullable, no `server_default`,
`Mapped[datetime | None]` — matches the `role_invitation` and
legacy-style `modified_at` patterns where the column is null
until the first update.
- Remove the `to_data()` method from both Row classes and introduce
dedicated `AppConfigFragmentAdapter` / `AppConfigPolicyAdapter`
classes under `models/<entity>/adapter.py` to perform the Row →
Data conversion. The db_source modules call the adapter directly,
keeping Row classes free of business logic per the models/CLAUDE.md
rule. Re-exported from each `models/<entity>/__init__.py`.
- Migration follows the new shape (extra_config / updated_at
nullable, no server_default on updated_at).
…,Policy}Adapter in api/adapters/registry - Row.to_data() is the dominant codebase pattern (141 usages); the earlier model-level Adapter classes lacked precedent. Re-add to_data() to AppConfigFragmentRow and AppConfigPolicyRow, handling the now-nullable extra_config / updated_at, and delete the model-level adapter modules. db_source modules call row.to_data() directly. - Add api/adapters/app_config_fragment.py and api/adapters/app_config_policy.py inheriting BaseAdapter, register them in the central Adapters registry. Both are skeletons today — concrete create / read / update / purge / search methods land with the GraphQL / REST surface in subsequent issues, but the registry plumbing is in place from day one so downstream PRs only add methods to the existing classes.
…t merged-view searches Address PR review: - Move `AppConfigData`, `AppConfigSearchResult`, `AppConfigFragmentSearchResult` from `repositories/.../types.py` to the `data/` package (frozen dataclasses with no framework imports — they belong with the rest of the value-type layer per data/CLAUDE.md). The `repositories/.../types.py` modules now hold only the SearchScope classes that legitimately depend on SQLAlchemy. - Implement `search_user_app_configs(scope, querier)` and `admin_search_app_configs(querier)` in the db_source. Both join `app_config_policies`, group results by `name` (and `user_id` for the cross-user variant), and feed each group through `_merge_chain`. Pagination at the merged-group level is left to the GQL Connection that wraps these calls; the previous `NotImplementedError` placeholders are gone.
…ice moves to BA-5827 The combined Fragment + Policy foundation PR was too large for review. Split into two stacked PRs: - BA-5814 (this PR) keeps just the AppConfigPolicy slice — schema, ORM, data types, repository pair, adapter skeleton. - BA-5827 stacks on top with the AppConfigFragment slice; the Fragment migration FKs to `app_config_policies.config_name` so the Policy table must exist first. This PR removes everything Fragment-related: - Migration shrunk to `app_config_policies` only and renamed to `5df264862995_add_app_config_policies.py`. - Drop `data/app_config_fragment/`, `data/app_config/`, `models/app_config_fragment/`, `repositories/app_config_fragment/`, `api/adapters/app_config_fragment.py`, and the matching tests. - Repositories container and Adapters registry no longer reference the Fragment side.
Extends the Policy foundation with the Service / Processor slice so the adapter can reach domain operations through the central Processors registry from day one (matches the pattern of every other domain wired into services/processors.py). What lands here: - `services/app_config_policy/actions/` — 5 frozen-dataclass actions (`create`, `get`, `search`, `update`, `purge`) plus a shared `AppConfigPolicyAction` base bound to `EntityType.APP_CONFIG`. - `services/app_config_policy/service.py` — `AppConfigPolicyService` delegating to `AppConfigPolicyRepository` / `AppConfigPolicyAdminRepository` (1 repo call per method). Uses `ObjectNotFound` for missing-config-name on update. - `services/app_config_policy/processors.py` — `AppConfigPolicyProcessors(AbstractProcessorPackage)` wrapping each service method in an `ActionProcessor`, with `supported_actions()`. - `services/processors.py` — `Services` / `Processors` containers + `supported_actions()` updated to include Policy. - `services/factory.py` — `create_services()` / `create_processors()` wire `AppConfigPolicyService` and `AppConfigPolicyProcessors`. No DTO / GQL / REST wiring here — those land with the vertical (BA-5815, #11269). `delete` is intentionally omitted: policies have no soft-delete concept (see BEP-1052 §1); `purge` is the only removal path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y repo Addresses review feedback on #11266: - Apply the `Resilience` decorator (MetricPolicy + RetryPolicy) to every public method on both `AppConfigPolicyRepository` and `AppConfigPolicyAdminRepository`, matching the `prometheus_query_preset` reference. Adds `LayerType.APP_CONFIG_POLICY_REPOSITORY` and `LayerType.APP_CONFIG_POLICY_ADMIN_REPOSITORY` in `common.metrics.metric`. - Introduce the shared Creator / Updater / Purger pattern for mutations: * `creators.AppConfigPolicyCreatorSpec` — builds the row and carries the `UniqueConstraintViolationError → AppConfigPolicyConflict` integrity check (new error class in `errors/app_config.py`). * `updaters.AppConfigPolicyUpdaterSpec` — exposes only `scope_sources` (config_name is immutable per BEP-1052 §1). * `db_source.create/update/purge` now accept `Creator[Row]` / `UpdaterSpec` / resolve `config_name → id` and delegate to `execute_creator` / `execute_updater` / `execute_purger`. - `AppConfigPolicyAdminRepository` keeps its (`config_name`, `scope_sources`) surface and internally builds the spec+helper — the service layer is unaffected. Also drops the stray `AppConfigFragmentRow` reference from `test_app_config_policy.py` — Fragment lives in BA-5827, and `with_tables` did not need a child table to exercise Policy tests (the FK runs the other direction). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to b9c2da8 — align with the `audit_log` / `agent` pattern (DomainType.DB_SOURCE + `_DB_SOURCE` layer) so retries and metrics sit where the actual DB I/O boundary is, not at the orchestration layer. - `LayerType.APP_CONFIG_POLICY_REPOSITORY` / `_ADMIN_REPOSITORY` entries dropped; `APP_CONFIG_POLICY_DB_SOURCE` added under the DB-source section. - `app_config_policy_db_source_resilience` now wraps every public method on `AppConfigPolicyDBSource` (including reads — matches audit_log). - `repository.py` / `admin_repository.py` no longer carry `@*_resilience.apply()` decorators — they just delegate. No behavior change at the service boundary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation PR now exposes a callable adapter surface so REST/GQL vertical work in BA-5815 can plug straight in. Previously the adapter was an empty `BaseAdapter` skeleton while the Processor layer already existed. Adds: - `common/dto/manager/v2/app_config_policy/`: `types`, `request`, `response`, `__init__` — `CreateAppConfigPolicyInput`, `UpdateAppConfigPolicyInput`, `PurgeAppConfigPolicyInput`, `SearchAppConfigPoliciesInput` (with filter / order / cursor + offset pagination), plus matching `AppConfigPolicyNode` and `*Payload` response models. - `models/app_config_policy/conditions.py` — `AppConfigPolicyConditions` (by_ids, by_config_name_{contains,equals,starts_with,ends_with,in}, cursor forward/backward). - `models/app_config_policy/orders.py` — `AppConfigPolicyOrders` (config_name, created_at). - `api/adapters/app_config_policy.AppConfigPolicyAdapter`: five methods (`create` / `get` / `search` / `update` / `purge`) calling the Processors via `wait_for_complete`, with filter / order conversion and cursor pagination via the shared `PaginationSpec` utility. `update` falls back to `ObjectNotFound` if the service returns `None` (defense-in-depth — the service raises first). No REST/GQL wiring here; that is the subject of BA-5815. Adapter is registered in `Adapters` (done previously in 54eeaf9). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…urge `execute_updater` runs an UPDATE ... RETURNING via `from_statement`, which re-loads the row into the session's identity map. When the preceding `select(AppConfigPolicyRow)` already loaded that row with the stale pre-update state, SQLAlchemy merged the two and the returned instance carried the stale values — update tests observed the old `scope_sources` even though the DB was updated correctly. Fixes by selecting only `AppConfigPolicyRow.id` (so no ORM instance is attached to the session), feeding `pk_value=id` to the Updater/ Purger helper. Same shape as the Fragment counterpart in BA-5827. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds bulk service operations alongside the single-item ones:
- New action files: admin_bulk_{create,update,purge} with per-item failure types
- data/app_config_policy/bulk_types.py for shared service-layer dataclasses
- Service methods admin_bulk_{create,update,purge} with per-item try/except
for partial-success semantics (BEP-1052 §3)
- Processor wiring + supported_actions registration
- Adapter exposes admin_bulk_{create,update,purge} for downstream API layers
- DTO additions: AdminAppConfigPolicyItemInput, bulk inputs/payloads,
AppConfigPolicyBulkError
Single-item create/update/purge service methods stay so they can back the
read paths and any internal callers; the public API surface (added in
BA-5815) is bulk-only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review feedback on AppConfigPolicy DTO + types: - Switch the `config_name` validator to the project-wide `strip_and_validate_name` pattern (matches resource_group / resource_policy / vfolder DTOs); apply it on every input that takes a `config_name`. - Add `UPDATED_AT` to `AppConfigPolicyOrderField`, the matching `AppConfigPolicyOrders.updated_at` factory, and the adapter's order-mapping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rites)
Writes are bulk-only by design (BEP-1052 §3); single-item create /
update / purge actions, service methods, adapter methods, and the
matching DTOs were dead weight after the foundation lands. Drop them
so the surface area matches what the API exposes.
- Remove `actions/{create,update,purge}.py`
- Strip the matching service methods + processors registration
- Strip the matching adapter methods (and ObjectNotFound import)
- Strip `Create/Update/PurgeAppConfigPolicyInput` and matching
`Payload` DTOs from `common/dto/manager/v2/app_config_policy/`
Bulk service methods continue to call `admin_repository.create /
update / purge` directly, which is the intended split: the repository
keeps single-row primitives, the service exposes only bulk operations
to upper layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y bulk Bulk Policy actions now extend BaseBulkAction[T] / BaseBulkActionResult and run through BulkActionProcessor (the processor framework's bulk primitive) instead of plain ActionProcessor. This gives the bulk mutations a consistent surface for future RBAC / validator filtering without rewriting service-layer code. - admin_bulk_create / admin_bulk_update extend BaseBulkAction[ AppConfigPolicyBulkItem]; entity_ids carries the per-item config_names, items carries the matching payloads. The framework only ever rebuilds the action via entity_ids=..., so the items default to [] and stays in sync with the validated entity_ids slice (none today). - admin_bulk_purge extends BaseBulkAction[str] — entity_ids is the natural identifier list with no extra payload. - *ActionResult classes extend BaseBulkActionResult and surface the newly-created / -updated / -purged ids via entity_ids(). - Processors swap ActionProcessor → BulkActionProcessor. - Adapter unwraps BulkProcessResult.result; per-item failure routing stays in the service-layer ActionResult (validator decisions are intentionally discarded — RBAC reasons travel via the failed list). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New `EntityType.APP_CONFIG_POLICY` enum value; Policy actions now carry that type so audit / RBAC routing distinguishes Policy from the broader `APP_CONFIG` (which the merged-view AppConfig and Fragment surfaces continue to use). - `AppConfigPolicyFilter` gains `created_at` / `updated_at` `DateTimeFilter` fields, with matching `by_created_at_*` / `by_updated_at_*` condition factories on `AppConfigPolicyConditions` and adapter wiring through `DateTimeFilter.build_query_condition`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing None Reads (`get(...)`) keep nullable semantics, but `update(...)` should fail loudly when the natural key resolves to no row — silently returning None forces every caller to re-implement the same "row missing" branch. Adds `AppConfigPolicyNotFound` (subclass of the shared `ObjectNotFound`) and raises it from `db_source.update` whenever the `config_name` lookup or the post-resolve UPDATE comes up empty (the latter handles concurrent purge between resolve and write). The Policy `admin_bulk_update` service drops its now-dead `policy is None` branch — the surrounding `try/except` already catches the new exception and reports it through the per-item failure list.
…g key The repository now raises rather than returning `None` for an unresolved `config_name`; flip the assertion accordingly.
- Wrap AppConfigPolicy repository / admin_repository methods with their own Resilience instances (mirrors keypair_resource_policy convention) so a future cache layer can plug in at the repository boundary. - Switch order-field dispatch in AppConfigPolicyAdapter from `.value` string matching to direct StrEnum match. - Promote bulk per-item failure logging to log.warning. - Drop BEP-1052 §X references from doctrings, comments, and the migration module — keep BEP wording only in the news fragment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…construction Mirror the BA-5827 pattern (and the wider scaling_group / vfolder convention): db_source now exposes `resolve_pk_by_config_name` and accepts pre-built `Updater` / `Purger` objects. The admin_repository builds the wrappers and translates the missing-row case into `AppConfigPolicyNotFound` itself, so the data layer no longer imports the typed domain error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ews fragment Remove the dead `user_writable` mentions from `AppConfigPolicyBulkItem` and `AdminAppConfigPolicyItemInput` docstrings (the field was never shipped, so the comments are noise) and strip the BEP §1 reference from the news fragment so the changelog reads on its own without linking back to the proposal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address PR review feedback: • Merge bulk_types.py into data/types.py (single types module per layer). • Drop AppConfigPolicyRepository.get(config_name); keep get_by_id only. • Drop db_source.get(config_name) and resolve_pk_by_config_name. • Switch admin_repository.update / purge to take id: UUID directly. • Drop the _missing(config_name) helper from admin_repository. • Reorganize the adapter: public surface first, private helpers grouped. DTO changes propagated: • Split AdminAppConfigPolicyItemInput into Create/Update variants. • AdminBulkPurgeAppConfigPoliciesInput.ids: list[UUID]. • AppConfigPolicyBulkError drops config_name. • AdminBulkPurgeAppConfigPoliciesPayload exposes purged_ids: list[UUID].
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.
📚 Stacked PRs
This PR is part of a 10-PR stack delivering BEP-1052. Merge in order:
chore(BA-5822): drop legacy AppConfig layerfeat(BA-5814): AppConfigPolicy foundation← you are herefeat(BA-5815): AppConfigPolicy GraphQLfeat(BA-5844): AppConfigPolicy REST v2feat(BA-5827): AppConfigFragment foundationfeat(BA-5836): AppConfigFragment service verticalfeat(BA-5829): AppConfigFragment + AppConfig GraphQLfeat(BA-5830): AppConfigFragment + AppConfig REST v2feat(BA-5832): AppConfig v2 SDK + CLIfeat(BA-5837): ValkeyCache for AppConfigFragment merged-view readsCI on intermediate PRs may show test churn since each one only lands a slice of the new layer. The full picture is guaranteed to build at the tip (#11298).
Summary
Foundation for the AppConfigPolicy domain (BEP-1052 §1) — data, model, repository (
Resilienceat db_source layer; Creator / Updater / Purger split), service, processor, adapter, and the v2 DTOs. Bulk service operations (adminBulkCreate / Update / Purge) are added here so the API PR (#11269) is purely a GQL / REST wiring layer with no service-layer churn.Service / processor:
get,search,create,update,purgeadminBulkCreate / Update / Purgewith per-item try / except for partial successdata/app_config_policy/bulk_types.pycarriesAppConfigPolicyBulkItemandAppConfigPolicyBulkItemErrorAdapter:
get/search) + bulk admin mutations exposed for feat(BA-5815): AppConfigPolicy GraphQL #11269DTOs (
common/dto/manager/v2/app_config_policy/):AppConfigPolicyNode,AdminAppConfigPolicyItemInput, bulk inputs / payloads,AppConfigPolicyBulkErrorResolves BA-5814.
📚 Documentation preview 📚: https://sorna--11266.org.readthedocs.build/en/11266/
📚 Documentation preview 📚: https://sorna-ko--11266.org.readthedocs.build/ko/11266/