fix(manager-core): F-Q-011 blanket Arc<T: ScriptRepository> impl; delete PostgresScriptRepoHandle

A 70-line newtype lived in picloud/src/lib.rs wrapping
Arc<PostgresScriptRepository> and re-implementing every ScriptRepository
method as `self.0.method(...).await`. Hand-delegation invited silent
skew when the trait gained a method, and forced 8 call sites to know
about the wrapper.

Add a blanket
  impl<T: ScriptRepository + ?Sized> ScriptRepository for Arc<T>
in manager-core::repo. The implementations forward via (**self).method(...)
so any owner of an Arc<dyn ScriptRepository> (or Arc<ConcreteImpl>) can
be passed where the trait is expected.

Migrate the 7 picloud-binary call sites:
  Arc::new(PostgresScriptRepoHandle(script_repo.clone())) → script_repo.clone()

Delete the newtype + its hand-delegated impl. Leaves a 6-line comment
documenting why the boilerplate is gone.

AUDIT.md anchor: F-Q-011.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 21:10:46 +02:00
parent 6d35eaad92
commit 1911691420
2 changed files with 76 additions and 88 deletions

View File

@@ -76,6 +76,68 @@ pub trait ScriptRepository: Send + Sync {
-> Result<Vec<Script>, ScriptRepositoryError>;
}
/// F-Q-011: blanket impl for `Arc<T: ScriptRepository>`. Lets callers
/// pass a shared `Arc<PostgresScriptRepository>` directly anywhere a
/// `dyn ScriptRepository` is wanted — replacing the hand-delegated
/// `PostgresScriptRepoHandle` newtype that previously had to be
/// rewritten in lockstep every time the trait gained a method.
#[async_trait::async_trait]
impl<T: ScriptRepository + ?Sized> ScriptRepository for std::sync::Arc<T> {
async fn get(&self, id: ScriptId) -> Result<Option<Script>, ScriptRepositoryError> {
(**self).get(id).await
}
async fn get_by_name(
&self,
app_id: AppId,
name: &str,
) -> Result<Option<Script>, ScriptRepositoryError> {
(**self).get_by_name(app_id, name).await
}
async fn list(&self) -> Result<Vec<Script>, ScriptRepositoryError> {
(**self).list().await
}
async fn list_for_app(&self, app_id: AppId) -> Result<Vec<Script>, ScriptRepositoryError> {
(**self).list_for_app(app_id).await
}
async fn list_for_user(
&self,
user_id: AdminUserId,
) -> Result<Vec<Script>, ScriptRepositoryError> {
(**self).list_for_user(user_id).await
}
async fn create(&self, input: NewScript) -> Result<Script, ScriptRepositoryError> {
(**self).create(input).await
}
async fn update(
&self,
id: ScriptId,
patch: ScriptPatch,
) -> Result<Script, ScriptRepositoryError> {
(**self).update(id, patch).await
}
async fn delete(&self, id: ScriptId) -> Result<(), ScriptRepositoryError> {
(**self).delete(id).await
}
async fn count_routes_for_script(
&self,
script_id: ScriptId,
) -> Result<i64, ScriptRepositoryError> {
(**self).count_routes_for_script(script_id).await
}
async fn count_triggers_for_script(
&self,
script_id: ScriptId,
) -> Result<i64, ScriptRepositoryError> {
(**self).count_triggers_for_script(script_id).await
}
async fn list_imports(
&self,
script_id: ScriptId,
) -> Result<Vec<Script>, ScriptRepositoryError> {
(**self).list_imports(script_id).await
}
}
/// Inbound shape for create. Defaults match the migration's CHECK
/// constraints; the repo enforces them in the DB regardless.
#[derive(Debug, Clone)]

View File

@@ -299,7 +299,7 @@ pub async fn build_app(
// that the dispatcher fires through the standard executor path.
let invoke: Arc<dyn picloud_shared::InvokeService> = Arc::new(
picloud_manager_core::invoke_service::InvokeServiceImpl::new(
Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
script_repo.clone(),
route_table.clone(),
outbox_repo.clone(),
),
@@ -348,9 +348,7 @@ pub async fn build_app(
.collect();
app_domain_table.replace(compiled_domains);
let resolver = Arc::new(RepoResolver::new(PostgresScriptRepoHandle(
script_repo.clone(),
)));
let resolver = Arc::new(RepoResolver::new(script_repo.clone()));
// Single global gate — overflow is rejected with 503 + Retry-After.
// See `ExecutionGate` docs and `PICLOUD_MAX_CONCURRENT_EXECUTIONS`.
let gate = Arc::new(ExecutionGate::from_env());
@@ -360,7 +358,7 @@ pub async fn build_app(
// due rows to the executor. Shares the `ExecutionGate` with sync
// HTTP per design notes §2 (one cap for everything).
let dispatcher_script_repo: Arc<dyn ScriptRepository> =
Arc::new(PostgresScriptRepoHandle(script_repo.clone()));
script_repo.clone();
let principals: Arc<dyn PrincipalResolver> =
Arc::new(AdminPrincipalResolver::new(auth.users.clone()));
// The InboxRegistry is constructed once and shared between the
@@ -385,7 +383,7 @@ pub async fn build_app(
.spawn();
let admin = AdminState {
repo: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
repo: script_repo.clone(),
logs: log_repo,
apps: apps_repo.clone(),
authz: authz.clone(),
@@ -394,7 +392,7 @@ pub async fn build_app(
};
let route_admin = RouteAdminState {
routes: route_repo.clone(),
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
scripts: script_repo.clone(),
domains: domains_repo.clone(),
table: route_table.clone(),
authz: authz.clone(),
@@ -439,7 +437,7 @@ pub async fn build_app(
triggers: trigger_repo.clone(),
apps: apps_repo.clone(),
authz: authz.clone(),
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
scripts: script_repo.clone(),
config: trigger_config,
master_key: master_key.clone(),
};
@@ -533,7 +531,7 @@ pub async fn build_app(
triggers: trigger_repo_for_queues.clone(),
apps: apps_repo.clone(),
authz: authz.clone(),
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
scripts: script_repo.clone(),
},
))
.merge(files_admin_router(files_admin_state))
@@ -638,82 +636,10 @@ async fn version() -> Json<serde_json::Value> {
}))
}
// ----------------------------------------------------------------------------
// Bridge: a single `PostgresScriptRepository` Arc is shared between the
// admin router (writes) and the resolver (reads). The resolver wants
// owned `impl ScriptRepository`, so we wrap the Arc in a delegating
// handle here rather than instantiating two repos against the same pool.
// ----------------------------------------------------------------------------
struct PostgresScriptRepoHandle(Arc<PostgresScriptRepository>);
#[async_trait::async_trait]
impl picloud_manager_core::ScriptRepository for PostgresScriptRepoHandle {
async fn get(
&self,
id: picloud_shared::ScriptId,
) -> Result<Option<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
self.0.get(id).await
}
async fn get_by_name(
&self,
app_id: picloud_shared::AppId,
name: &str,
) -> Result<Option<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
self.0.get_by_name(app_id, name).await
}
async fn list(
&self,
) -> Result<Vec<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
self.0.list().await
}
async fn list_for_app(
&self,
app_id: picloud_shared::AppId,
) -> Result<Vec<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
self.0.list_for_app(app_id).await
}
async fn list_for_user(
&self,
user_id: picloud_shared::AdminUserId,
) -> Result<Vec<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
self.0.list_for_user(user_id).await
}
async fn create(
&self,
input: picloud_manager_core::NewScript,
) -> Result<picloud_shared::Script, picloud_manager_core::ScriptRepositoryError> {
self.0.create(input).await
}
async fn update(
&self,
id: picloud_shared::ScriptId,
patch: picloud_manager_core::ScriptPatch,
) -> Result<picloud_shared::Script, picloud_manager_core::ScriptRepositoryError> {
self.0.update(id, patch).await
}
async fn delete(
&self,
id: picloud_shared::ScriptId,
) -> Result<(), picloud_manager_core::ScriptRepositoryError> {
self.0.delete(id).await
}
async fn count_routes_for_script(
&self,
script_id: picloud_shared::ScriptId,
) -> Result<i64, picloud_manager_core::ScriptRepositoryError> {
self.0.count_routes_for_script(script_id).await
}
async fn count_triggers_for_script(
&self,
script_id: picloud_shared::ScriptId,
) -> Result<i64, picloud_manager_core::ScriptRepositoryError> {
self.0.count_triggers_for_script(script_id).await
}
async fn list_imports(
&self,
script_id: picloud_shared::ScriptId,
) -> Result<Vec<picloud_shared::Script>, picloud_manager_core::ScriptRepositoryError> {
self.0.list_imports(script_id).await
}
}
// F-Q-011: the PostgresScriptRepoHandle newtype lived here so the
// resolver could take an owned `impl ScriptRepository` from the shared
// Arc<PostgresScriptRepository>. The blanket
// `impl<T: ScriptRepository + ?Sized> ScriptRepository for Arc<T>` in
// manager-core::repo lets us pass `script_repo.clone()` directly,
// eliminating 70 lines of hand-delegated boilerplate that had to be
// rewritten every time the trait gained a method.