Complete permission-aware native search indexing
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user