Complete permission-aware native search indexing

This commit is contained in:
2026-08-04 03:05:03 +02:00
parent 912db10a8a
commit 1bb338117f
12 changed files with 680 additions and 26 deletions
+23 -1
View File
@@ -11,6 +11,7 @@ from govoplan_core.core.module_guards import (
persistent_table_uninstall_guard,
)
from govoplan_core.core.modules import (
DocumentationLink,
DocumentationTopic,
FrontendModule,
FrontendRoute,
@@ -152,6 +153,13 @@ manifest = ModuleManifest(
label="Search results",
order=20,
),
ViewSurface(
id="search.admin.index",
module_id=MODULE_ID,
kind="section",
label="Search index administration",
order=30,
),
),
),
migration_spec=MigrationSpec(
@@ -201,12 +209,26 @@ manifest = ModuleManifest(
"Search works with the built-in database index and can aggregate "
"optional providers. Source modules announce searchable types, "
"context scopes, and ACL-aware index entries. External engines "
"remain optional adapters."
"remain optional adapters. F3 or the title-bar field opens the "
"keyboard-navigable search overlay; filters never broaden the "
"current principal's source permissions. Provider failures are "
"shown as partial diagnostics without discarding safe results."
" Search administrators can inspect native source coverage, "
"process queued changes, reconcile enabled modules, and run "
"bounded source rebuilds from Administration. Quarantined "
"changes remain visible until repaired and reconciled."
),
layer="available",
documentation_types=("admin", "user"),
audience=("administrator", "user"),
related_modules=("connectors", "views"),
links=(
DocumentationLink(
label="Search interface pattern audit",
href="govoplan-search/docs/INTERFACE_PATTERN_MIGRATION.md",
kind="repository",
),
),
order=12,
),
),
+70
View File
@@ -27,10 +27,12 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from govoplan_core.core.external_references import ExternalObjectReference
from govoplan_core.core.events import PlatformEvent
from govoplan_core.core.search import (
SearchAuthorizationRequest,
SearchBackfillRequest,
SearchDocument,
SearchEventSourceProvider,
SearchIndexChange,
SearchQuery,
SearchResourceReference,
@@ -231,6 +233,74 @@ class SearchIndexService:
return False
return True
def ingest_event(
self,
session: object,
*,
event: PlatformEvent,
delivery_key: str,
) -> dict[str, int]:
"""Queue source-owned index deltas from one committed platform event."""
db = _session(session)
result = {
"sources": 0,
"changes": 0,
"queued": 0,
"duplicates": 0,
}
for registered, provider in _search_sources(self.registry):
if not isinstance(provider, SearchEventSourceProvider):
continue
changes = tuple(
provider.index_changes_for_event(
db,
event=event,
delivery_key=delivery_key,
)
)
if not changes:
continue
result["sources"] += 1
descriptors = {
descriptor.resource_type: descriptor
for descriptor in provider.resource_types()
if descriptor.provider_id == registered.registration.id
}
for change in changes:
descriptor = descriptors.get(
change.reference.resource_type
)
if (
change.provider_id != registered.registration.id
or change.reference.module_id != registered.module_id
or descriptor is None
or descriptor.module_id != registered.module_id
):
raise ValueError(
"Search event source returned a change outside its "
"registered provider boundary."
)
if (
event.tenant is not None
and change.reference.tenant_id != event.tenant.id
):
raise ValueError(
"Search event source returned a cross-tenant change."
)
if change.document is not None:
_validate_backfill_document(
change.document,
descriptor=descriptor,
tenant_id=change.reference.tenant_id,
)
result["changes"] += 1
if self.enqueue_change(db, change=change):
result["queued"] += 1
else:
result["duplicates"] += 1
return result
def process_changes(
self,
session: object,