Filesystem-backed blob storage as the fifth concrete trigger kind.
- `files::collection(c).{create,head,get,update,delete,list}` Rhai SDK
(blob in/out; metadata maps; missing-field throws naming the field).
- `FilesService` trait in picloud-shared; `FsFilesRepo` (atomic
write: temp→fsync→rename→fsync-dir→DB; single-pass SHA-256;
checksum-verified reads → Corrupted) + `FilesServiceImpl` in
manager-core. Metadata in Postgres (0018), bytes on disk under
PICLOUD_FILES_ROOT with 0o700 shard dirs.
- `files:*` trigger kind via the Layout-E pattern (0019: widen both
CHECKs + files_trigger_details), TriggerEvent::Files (metadata only,
no bytes), emit_files fan-out, dispatcher arm, admin endpoint
POST /triggers/files (reuses validate_trigger_target).
- AppFilesRead/AppFilesWrite capabilities → script:read/script:write
(seven-scope commitment held). AppPubsubPublish reserved for v1.1.6.
- Admin files API (list + delete) + dashboard Files view per app.
Cross-app isolation keyed on cx.app_id at every layer. ~45 new tests
(service in-memory, fs tempdir, bridge integration). No DB required
for the suite. publish_ephemeral and the orphan sweep stay deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
4.5 KiB
Rust
121 lines
4.5 KiB
Rust
//! `Services` — bundle of stateful SDK service handles plumbed from the
|
|
//! host binary into every Rhai execution.
|
|
//!
|
|
//! Constructed once at startup in the picloud binary; cloned (cheap —
|
|
//! every field is an `Arc`) into the per-call sdk bridge so script
|
|
//! invocations don't need to re-resolve dependencies. The bundle is
|
|
//! handed to `executor-core::sdk::register_all` alongside an
|
|
//! `SdkCallCx` to wire each `::` namespace.
|
|
//!
|
|
//! v1.1.0 shipped this empty; v1.1.1 added the first two service fields
|
|
//! (`kv`, `dead_letters`) plus the `events` emitter that bound services
|
|
//! use to publish events into the triggers outbox. v1.1.3 adds the
|
|
//! `modules` field — the `ModuleSource` consulted by the per-call
|
|
//! `PicloudModuleResolver` to load `import`ed module scripts.
|
|
//!
|
|
//! `#[non_exhaustive]` so adding fields is a non-breaking change for
|
|
//! consumers that only *pattern-match* a `&Services`; only crates that
|
|
//! *construct* a `Services` (the picloud binary and tests) update.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use crate::{
|
|
DeadLetterService, DocsService, FilesService, HttpService, KvService, ModuleSource,
|
|
NoopDeadLetterService, NoopDocsService, NoopEventEmitter, NoopFilesService, NoopHttpService,
|
|
NoopKvService, NoopModuleSource, ServiceEventEmitter,
|
|
};
|
|
|
|
/// SDK service bundle. See module docs for the lifecycle and the v1.1.x
|
|
/// expansion plan.
|
|
#[non_exhaustive]
|
|
pub struct Services {
|
|
/// KV store (v1.1.1). Backed by Postgres in the picloud binary;
|
|
/// in-memory in tests.
|
|
pub kv: Arc<dyn KvService>,
|
|
|
|
/// Document store (v1.1.2). Backed by Postgres in the picloud
|
|
/// binary; in-memory in tests.
|
|
pub docs: Arc<dyn DocsService>,
|
|
|
|
/// Dead-letter management (v1.1.1). Scripts get
|
|
/// `dead_letters::replay(id)` and `dead_letters::resolve(id, reason)`.
|
|
pub dead_letters: Arc<dyn DeadLetterService>,
|
|
|
|
/// Event emitter for the triggers outbox. Mutating service methods
|
|
/// (`KvService::set/delete`, `DocsService::create/update/delete`,
|
|
/// future `files::*`, etc.) call `events.emit(cx, event)` after
|
|
/// the write succeeds. The outbox-backed impl in
|
|
/// `manager-core::outbox_event_emitter` replaces v1.1.0's
|
|
/// `NoopEventEmitter`.
|
|
pub events: Arc<dyn ServiceEventEmitter>,
|
|
|
|
/// Module source (v1.1.3). The `PicloudModuleResolver` consults
|
|
/// this to load `kind = 'module'` scripts that other scripts
|
|
/// `import`. Backed by Postgres in the picloud binary; in-memory
|
|
/// fakes in resolver tests.
|
|
pub modules: Arc<dyn ModuleSource>,
|
|
|
|
/// Outbound HTTP (v1.1.4). Scripts get `http::{get,post,…}`.
|
|
/// Backed by a reqwest client with the SSRF deny-list resolver in
|
|
/// the picloud binary; `NoopHttpService` in tests that don't make
|
|
/// network calls.
|
|
pub http: Arc<dyn HttpService>,
|
|
|
|
/// Filesystem-backed blob storage (v1.1.5). Scripts get
|
|
/// `files::collection(name).{create,head,get,update,delete,list}`.
|
|
/// Backed by a Postgres-metadata + on-disk-bytes repo in the
|
|
/// picloud binary; `NoopFilesService` in tests that don't touch
|
|
/// files.
|
|
pub files: Arc<dyn FilesService>,
|
|
}
|
|
|
|
impl Services {
|
|
/// Construct a bundle from already-constructed `Arc<dyn …>` handles.
|
|
/// The picloud binary's `main` wires this up after the DB pool is
|
|
/// open; tests build it from in-memory fakes.
|
|
#[must_use]
|
|
pub fn new(
|
|
kv: Arc<dyn KvService>,
|
|
docs: Arc<dyn DocsService>,
|
|
dead_letters: Arc<dyn DeadLetterService>,
|
|
events: Arc<dyn ServiceEventEmitter>,
|
|
modules: Arc<dyn ModuleSource>,
|
|
http: Arc<dyn HttpService>,
|
|
files: Arc<dyn FilesService>,
|
|
) -> Self {
|
|
Self {
|
|
kv,
|
|
docs,
|
|
dead_letters,
|
|
events,
|
|
modules,
|
|
http,
|
|
files,
|
|
}
|
|
}
|
|
|
|
/// All-noop bundle for tests that build an `Engine` but don't
|
|
/// exercise the stateful services. Returns the same shape as
|
|
/// `Services::new` so callers can't accidentally rely on a stub
|
|
/// silently doing the right thing — every call into a noop
|
|
/// service surfaces an explicit error.
|
|
#[must_use]
|
|
pub fn with_noop_services() -> Self {
|
|
Self::new(
|
|
Arc::new(NoopKvService),
|
|
Arc::new(NoopDocsService),
|
|
Arc::new(NoopDeadLetterService),
|
|
Arc::new(NoopEventEmitter),
|
|
Arc::new(NoopModuleSource),
|
|
Arc::new(NoopHttpService),
|
|
Arc::new(NoopFilesService),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Default for Services {
|
|
fn default() -> Self {
|
|
Self::with_noop_services()
|
|
}
|
|
}
|