feat(v1.1.1-kv): migrations + KvService trait + Postgres impl
First v1.1.1 commit. Adds the KV store the design notes commit to: `(app_id, collection, key)` identity with JSONB value and a per-app index. Trait lives in `picloud-shared` so the executor-core Rhai bridge (next commit), the Postgres impl, and tests all depend on the same surface without coupling crates. The `Services` bundle grows from empty to three fields: `kv`, `dead_letters` (NoopDeadLetterService stub — replaced by the Postgres impl in commit 8), and `events` (NoopEventEmitter until the outbox emitter lands with the dispatcher). Tests use `Services::default()` for an all-noop bundle. New capabilities `AppKvRead` / `AppKvWrite` join the Capability enum. They map onto the existing seven-value `Scope` (script:read / script:write) — the scope vocabulary stays locked per the `docs/versioning.md` commitment. Script-as-gate semantics in `KvServiceImpl`: capability check runs when `cx.principal.is_some()`, skipped when None (public HTTP). Cross-app isolation is enforced independently by deriving every row's `app_id` from `cx.app_id` rather than a script-passed argument. In-memory `KvRepo` impl + unit tests cover the round-trips, the cross-app isolation property, empty-collection rejection, script-as-gate behaviour for both anonymous and authed contexts, and cursor-style pagination. Postgres impl exists; integration testing waits for a real DB harness (see HANDBACK). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,19 +14,19 @@ use picloud_manager_core::{
|
||||
attach_principal_if_present, auth_router, compile_routes, migrations, require_authenticated,
|
||||
route_admin_router, AdminSessionRepository, AdminState, AdminUserRepository, AdminsState,
|
||||
ApiKeyRepository, ApiKeysState, AppDomainRepository, AppMembersRepository, AppMembersState,
|
||||
AppRepository, AppsState, AuthState, AuthzRepo, PostgresAdminSessionRepository,
|
||||
AppRepository, AppsState, AuthState, AuthzRepo, KvServiceImpl, PostgresAdminSessionRepository,
|
||||
PostgresAdminUserRepository, PostgresApiKeyRepository, PostgresAppDomainRepository,
|
||||
PostgresAppMembersRepository, PostgresAppRepository, PostgresExecutionLogRepository,
|
||||
PostgresExecutionLogSink, PostgresRouteRepository, PostgresScriptRepository, RepoResolver,
|
||||
RouteAdminState, RouteRepository, SandboxCeiling,
|
||||
PostgresExecutionLogSink, PostgresKvRepo, PostgresRouteRepository, PostgresScriptRepository,
|
||||
RepoResolver, RouteAdminState, RouteRepository, SandboxCeiling,
|
||||
};
|
||||
use picloud_orchestrator_core::routing::{AppDomainTable, RouteTable};
|
||||
use picloud_orchestrator_core::{
|
||||
data_plane_router, user_routes_router, DataPlaneState, ExecutionGate, LocalExecutorClient,
|
||||
};
|
||||
use picloud_shared::{
|
||||
ExecutionLogSink, ScriptValidator, Services, API_VERSION, PRODUCT_VERSION, SDK_VERSION,
|
||||
WIRE_VERSION,
|
||||
ExecutionLogSink, KvService, NoopDeadLetterService, NoopEventEmitter, ScriptValidator,
|
||||
ServiceEventEmitter, Services, API_VERSION, PRODUCT_VERSION, SDK_VERSION, WIRE_VERSION,
|
||||
};
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::PgPool;
|
||||
@@ -83,10 +83,6 @@ fn read_session_ttl() -> Duration {
|
||||
/// `/version`) stays open — it's the public ingress for user scripts.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub async fn build_app(pool: PgPool, auth: AuthDeps) -> anyhow::Result<Router> {
|
||||
// `Services` is the SDK service bundle. Empty in v1.1.0; the
|
||||
// v1.1.1 KV PR will populate it with `kv: Arc::new(...)` here.
|
||||
let engine = Arc::new(Engine::new(Limits::default(), Services::new()));
|
||||
|
||||
let script_repo = Arc::new(PostgresScriptRepository::new(pool.clone()));
|
||||
let log_repo = Arc::new(PostgresExecutionLogRepository::new(pool.clone()));
|
||||
let log_sink: Arc<dyn ExecutionLogSink> = Arc::new(PostgresExecutionLogSink::new(pool.clone()));
|
||||
@@ -98,10 +94,21 @@ pub async fn build_app(pool: PgPool, auth: AuthDeps) -> anyhow::Result<Router> {
|
||||
// (CRUD over the table) and `AuthzRepo` (single-row membership lookup
|
||||
// for capability checks). Construct it once and clone the Arc into
|
||||
// both trait views — same allocation, two vtables.
|
||||
let members_concrete = Arc::new(PostgresAppMembersRepository::new(pool));
|
||||
let members_concrete = Arc::new(PostgresAppMembersRepository::new(pool.clone()));
|
||||
let members: Arc<dyn AppMembersRepository> = members_concrete.clone();
|
||||
let authz: Arc<dyn AuthzRepo> = members_concrete;
|
||||
|
||||
// SDK services bundle. v1.1.1 ships the KV store; the outbox-backed
|
||||
// event emitter replaces `NoopEventEmitter` once the triggers
|
||||
// dispatcher lands. `NoopDeadLetterService` is a v1.1.1 stub that
|
||||
// errors loudly until the real `PostgresDeadLetterService` ships.
|
||||
let kv_repo = Arc::new(PostgresKvRepo::new(pool.clone()));
|
||||
let events: Arc<dyn ServiceEventEmitter> = Arc::new(NoopEventEmitter);
|
||||
let kv: Arc<dyn KvService> =
|
||||
Arc::new(KvServiceImpl::new(kv_repo, authz.clone(), events.clone()));
|
||||
let services = Services::new(kv, Arc::new(NoopDeadLetterService), events);
|
||||
let engine = Arc::new(Engine::new(Limits::default(), services));
|
||||
|
||||
// Compile the routes table once at startup; admin writes refresh it.
|
||||
let route_table = Arc::new(RouteTable::new());
|
||||
let initial = route_repo.list_all().await?;
|
||||
|
||||
Reference in New Issue
Block a user