feat(modules): group-docs schema + kind-generalized registry (§11.6 docs C1)

Data layer for extending shared group collections from KV to docs. KV behavior
unchanged (callers pass kind="kv").

- 0054_group_docs.sql: widen the group_collections kind CHECK to ('kv','docs')
  and add the group-keyed group_docs store (clone of docs/0013, keyed by
  group_id, CASCADE on group delete) + its (group_id,collection) and data GIN
  indexes.
- group_collection_repo: thread a `kind` parameter through list_for_owner,
  resolve_owning_group, insert/delete_collection_tx; add list_all_for_owner ->
  (name,kind) for the kind-aware diff (D4). Relocate the injectable
  GroupCollectionResolver trait + Postgres impl here (now shared by kv+docs;
  resolve takes kind) — was in group_kv_service.
- group_docs_repo: a near-clone of docs_repo keyed by group_id; find reuses the
  generalized build_find_query.
- docs_repo: generalize build_find_query(table, owner_col, owner_id, …) — both
  are compile-time literals (no injection); app docs passes ("docs","app_id"),
  group docs ("group_docs","group_id"). row_to_doc/build_find_query are now
  pub(crate) for reuse.

Schema snapshot re-blessed (55 migrations).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-30 07:33:39 +02:00
parent af525e71bd
commit 5d1d4b3ff6
8 changed files with 479 additions and 87 deletions

View File

@@ -22,48 +22,15 @@
use std::sync::Arc;
use async_trait::async_trait;
use picloud_shared::{AppId, GroupId, GroupKvError, GroupKvService, KvListPage, SdkCallCx};
use sqlx::PgPool;
use picloud_shared::{GroupId, GroupKvError, GroupKvService, KvListPage, SdkCallCx};
use crate::authz::{self, AuthzRepo, Capability};
use crate::group_collection_repo;
use crate::group_collection_repo::GroupCollectionResolver;
use crate::group_kv_repo::{GroupKvRepo, GroupKvRepoError};
use crate::kv_service::kv_max_value_bytes_from_env;
/// Resolves a shared-collection name to its owning group for a calling app
/// (nearest ancestor group on the app's chain that declares it). Behind a
/// trait so unit tests can inject a fake without Postgres.
#[async_trait]
pub trait GroupCollectionResolver: Send + Sync {
async fn resolve_owning_group(
&self,
app_id: AppId,
name: &str,
) -> Result<Option<GroupId>, sqlx::Error>;
}
/// Postgres-backed resolver — delegates to [`group_collection_repo`].
pub struct PostgresGroupCollectionResolver {
pool: PgPool,
}
impl PostgresGroupCollectionResolver {
#[must_use]
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
#[async_trait]
impl GroupCollectionResolver for PostgresGroupCollectionResolver {
async fn resolve_owning_group(
&self,
app_id: AppId,
name: &str,
) -> Result<Option<GroupId>, sqlx::Error> {
group_collection_repo::resolve_owning_group(&self.pool, app_id, name).await
}
}
/// The registry `kind` this service resolves.
const KIND_KV: &str = "kv";
pub struct GroupKvServiceImpl {
repo: Arc<dyn GroupKvRepo>,
@@ -109,7 +76,7 @@ impl GroupKvServiceImpl {
return Err(GroupKvError::InvalidCollection);
}
self.resolver
.resolve_owning_group(cx.app_id, collection)
.resolve_owning_group(cx.app_id, collection, KIND_KV)
.await
.map_err(|e| GroupKvError::Backend(e.to_string()))?
.ok_or_else(|| GroupKvError::CollectionNotShared(collection.to_string()))
@@ -220,7 +187,8 @@ mod tests {
use super::*;
use crate::authz::{AuthzError, AuthzRepo};
use picloud_shared::{
AdminUserId, AppRole, ExecutionId, InstanceRole, Principal, RequestId, ScriptId, UserId,
AdminUserId, AppId, AppRole, ExecutionId, InstanceRole, Principal, RequestId, ScriptId,
UserId,
};
use std::collections::{BTreeMap, HashMap};
use tokio::sync::Mutex;
@@ -321,6 +289,7 @@ mod tests {
&self,
app_id: AppId,
name: &str,
_kind: &str,
) -> Result<Option<GroupId>, sqlx::Error> {
Ok(self.map.get(&(app_id, name.to_lowercase())).copied())
}