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:
@@ -76,6 +76,68 @@ pub trait ScriptRepository: Send + Sync {
|
|||||||
-> Result<Vec<Script>, ScriptRepositoryError>;
|
-> 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
|
/// Inbound shape for create. Defaults match the migration's CHECK
|
||||||
/// constraints; the repo enforces them in the DB regardless.
|
/// constraints; the repo enforces them in the DB regardless.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ pub async fn build_app(
|
|||||||
// that the dispatcher fires through the standard executor path.
|
// that the dispatcher fires through the standard executor path.
|
||||||
let invoke: Arc<dyn picloud_shared::InvokeService> = Arc::new(
|
let invoke: Arc<dyn picloud_shared::InvokeService> = Arc::new(
|
||||||
picloud_manager_core::invoke_service::InvokeServiceImpl::new(
|
picloud_manager_core::invoke_service::InvokeServiceImpl::new(
|
||||||
Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
script_repo.clone(),
|
||||||
route_table.clone(),
|
route_table.clone(),
|
||||||
outbox_repo.clone(),
|
outbox_repo.clone(),
|
||||||
),
|
),
|
||||||
@@ -348,9 +348,7 @@ pub async fn build_app(
|
|||||||
.collect();
|
.collect();
|
||||||
app_domain_table.replace(compiled_domains);
|
app_domain_table.replace(compiled_domains);
|
||||||
|
|
||||||
let resolver = Arc::new(RepoResolver::new(PostgresScriptRepoHandle(
|
let resolver = Arc::new(RepoResolver::new(script_repo.clone()));
|
||||||
script_repo.clone(),
|
|
||||||
)));
|
|
||||||
// Single global gate — overflow is rejected with 503 + Retry-After.
|
// Single global gate — overflow is rejected with 503 + Retry-After.
|
||||||
// See `ExecutionGate` docs and `PICLOUD_MAX_CONCURRENT_EXECUTIONS`.
|
// See `ExecutionGate` docs and `PICLOUD_MAX_CONCURRENT_EXECUTIONS`.
|
||||||
let gate = Arc::new(ExecutionGate::from_env());
|
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
|
// due rows to the executor. Shares the `ExecutionGate` with sync
|
||||||
// HTTP per design notes §2 (one cap for everything).
|
// HTTP per design notes §2 (one cap for everything).
|
||||||
let dispatcher_script_repo: Arc<dyn ScriptRepository> =
|
let dispatcher_script_repo: Arc<dyn ScriptRepository> =
|
||||||
Arc::new(PostgresScriptRepoHandle(script_repo.clone()));
|
script_repo.clone();
|
||||||
let principals: Arc<dyn PrincipalResolver> =
|
let principals: Arc<dyn PrincipalResolver> =
|
||||||
Arc::new(AdminPrincipalResolver::new(auth.users.clone()));
|
Arc::new(AdminPrincipalResolver::new(auth.users.clone()));
|
||||||
// The InboxRegistry is constructed once and shared between the
|
// The InboxRegistry is constructed once and shared between the
|
||||||
@@ -385,7 +383,7 @@ pub async fn build_app(
|
|||||||
.spawn();
|
.spawn();
|
||||||
|
|
||||||
let admin = AdminState {
|
let admin = AdminState {
|
||||||
repo: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
repo: script_repo.clone(),
|
||||||
logs: log_repo,
|
logs: log_repo,
|
||||||
apps: apps_repo.clone(),
|
apps: apps_repo.clone(),
|
||||||
authz: authz.clone(),
|
authz: authz.clone(),
|
||||||
@@ -394,7 +392,7 @@ pub async fn build_app(
|
|||||||
};
|
};
|
||||||
let route_admin = RouteAdminState {
|
let route_admin = RouteAdminState {
|
||||||
routes: route_repo.clone(),
|
routes: route_repo.clone(),
|
||||||
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
scripts: script_repo.clone(),
|
||||||
domains: domains_repo.clone(),
|
domains: domains_repo.clone(),
|
||||||
table: route_table.clone(),
|
table: route_table.clone(),
|
||||||
authz: authz.clone(),
|
authz: authz.clone(),
|
||||||
@@ -439,7 +437,7 @@ pub async fn build_app(
|
|||||||
triggers: trigger_repo.clone(),
|
triggers: trigger_repo.clone(),
|
||||||
apps: apps_repo.clone(),
|
apps: apps_repo.clone(),
|
||||||
authz: authz.clone(),
|
authz: authz.clone(),
|
||||||
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
scripts: script_repo.clone(),
|
||||||
config: trigger_config,
|
config: trigger_config,
|
||||||
master_key: master_key.clone(),
|
master_key: master_key.clone(),
|
||||||
};
|
};
|
||||||
@@ -533,7 +531,7 @@ pub async fn build_app(
|
|||||||
triggers: trigger_repo_for_queues.clone(),
|
triggers: trigger_repo_for_queues.clone(),
|
||||||
apps: apps_repo.clone(),
|
apps: apps_repo.clone(),
|
||||||
authz: authz.clone(),
|
authz: authz.clone(),
|
||||||
scripts: Arc::new(PostgresScriptRepoHandle(script_repo.clone())),
|
scripts: script_repo.clone(),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.merge(files_admin_router(files_admin_state))
|
.merge(files_admin_router(files_admin_state))
|
||||||
@@ -638,82 +636,10 @@ async fn version() -> Json<serde_json::Value> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// F-Q-011: the PostgresScriptRepoHandle newtype lived here so the
|
||||||
// Bridge: a single `PostgresScriptRepository` Arc is shared between the
|
// resolver could take an owned `impl ScriptRepository` from the shared
|
||||||
// admin router (writes) and the resolver (reads). The resolver wants
|
// Arc<PostgresScriptRepository>. The blanket
|
||||||
// owned `impl ScriptRepository`, so we wrap the Arc in a delegating
|
// `impl<T: ScriptRepository + ?Sized> ScriptRepository for Arc<T>` in
|
||||||
// handle here rather than instantiating two repos against the same pool.
|
// 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.
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user