Phase-1 `enabled` three-state lifecycle (§4.3), data-model half. Triggers already carried `enabled`; this adds it to scripts and routes and threads it through the declarative project tool. Runtime honoring (disabled route 404 / script non-invocable / dispatcher fire-time re-check) is the next commit — this change only stores and reconciles the flag. - Migration 0045: `enabled BOOLEAN NOT NULL DEFAULT TRUE` on scripts + routes. Default true ⇒ no behavior change on migrate. - `Script`/`Route` (shared) gain `enabled` (serde default true via the new `picloud_shared::default_true`); repos' SELECT/INSERT/UPDATE SQL, row structs, `NewScript`/`NewRoute`/`ScriptPatch` all carry it. - Apply diff treats `enabled` as a declarative field (omitted ⇒ active): `script_update_reason` + `diff_routes` detect a toggle as an Update, and the create/update/insert paths persist it. The bound-plan `state_token` re-includes script/route `enabled` (removed in the earlier review fix precisely because the diff didn't key on it yet — now it does). - CLI manifest model + `build_bundle` + `pull` round-trip `enabled` (serialized only when false; omitted ⇒ active). `pic init` scaffold and the interactive script/route create paths default active. Tested: manager-core lib 365 (incl. enabled diff + token sensitivity) + cli bins 31 (incl. manifest skip-serialize) green; clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
134 lines
4.4 KiB
Rust
134 lines
4.4 KiB
Rust
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::{AppId, ScriptId, ScriptSandbox};
|
|
|
|
/// Semantic role of a script (v1.1.3).
|
|
///
|
|
/// `Endpoint` scripts have an executable entry point — they bind to HTTP
|
|
/// routes and act as trigger handlers. `Module` scripts are libraries of
|
|
/// `fn`/`const` declarations imported by other scripts via Rhai's
|
|
/// `import "<name>" as <alias>;` syntax. Modules cannot be invoked
|
|
/// directly: route binding and trigger creation reject `Module` targets.
|
|
///
|
|
/// Serialized as `"endpoint"` / `"module"` so the wire shape is the
|
|
/// same string the SQL `CHECK (kind IN ('endpoint','module'))`
|
|
/// constraint enforces.
|
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum ScriptKind {
|
|
#[default]
|
|
Endpoint,
|
|
Module,
|
|
}
|
|
|
|
impl ScriptKind {
|
|
/// Wire / SQL representation. Inverse of `parse_str`.
|
|
#[must_use]
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Endpoint => "endpoint",
|
|
Self::Module => "module",
|
|
}
|
|
}
|
|
|
|
/// Parse the canonical wire / SQL form. Returns `None` for any
|
|
/// other input; callers map that to a 400 / `ValidationError`.
|
|
/// Named `parse_str` (not `from_str`) to dodge the
|
|
/// `std::str::FromStr` lint without taking on the trait's
|
|
/// `Result<Self, Self::Err>` shape that this caller doesn't need.
|
|
#[must_use]
|
|
pub fn parse_str(s: &str) -> Option<Self> {
|
|
match s {
|
|
"endpoint" => Some(Self::Endpoint),
|
|
"module" => Some(Self::Module),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod kind_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn default_is_endpoint() {
|
|
assert_eq!(ScriptKind::default(), ScriptKind::Endpoint);
|
|
}
|
|
|
|
#[test]
|
|
fn round_trips_through_serde_lowercase() {
|
|
assert_eq!(
|
|
serde_json::to_string(&ScriptKind::Endpoint).unwrap(),
|
|
"\"endpoint\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_string(&ScriptKind::Module).unwrap(),
|
|
"\"module\""
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<ScriptKind>("\"endpoint\"").unwrap(),
|
|
ScriptKind::Endpoint
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_str::<ScriptKind>("\"module\"").unwrap(),
|
|
ScriptKind::Module
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_str_round_trip() {
|
|
for k in [ScriptKind::Endpoint, ScriptKind::Module] {
|
|
assert_eq!(ScriptKind::parse_str(k.as_str()), Some(k));
|
|
}
|
|
assert_eq!(ScriptKind::parse_str("invalid"), None);
|
|
assert_eq!(ScriptKind::parse_str(""), None);
|
|
}
|
|
}
|
|
|
|
/// A user-uploaded Rhai script and its execution configuration.
|
|
///
|
|
/// This is the canonical representation that flows between manager (storage),
|
|
/// orchestrator (dispatch), and executor (run). It must stay serializable
|
|
/// because in cluster mode it crosses process boundaries on every invocation.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Script {
|
|
pub id: ScriptId,
|
|
/// Owning app. Set on create, immutable thereafter — a "move to
|
|
/// another app" is a copy+delete, not an in-place edit (snapshot
|
|
/// semantics — see blueprint §11.5).
|
|
pub app_id: AppId,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
pub version: i32,
|
|
pub source: String,
|
|
|
|
/// `Endpoint` (default; the only kind v1.0 through v1.1.2 supported)
|
|
/// or `Module` (v1.1.3 — imported by other scripts, never bound
|
|
/// directly to a route or trigger).
|
|
#[serde(default)]
|
|
pub kind: ScriptKind,
|
|
|
|
pub timeout_seconds: u32,
|
|
|
|
/// Per-script overrides for Rhai sandbox limits. Empty = platform
|
|
/// defaults. Values are admin-ceiling-clamped at write time.
|
|
#[serde(default)]
|
|
pub sandbox: ScriptSandbox,
|
|
|
|
/// **v1.3+ advisory only.** Real heap limits require OS-level
|
|
/// isolation (cgroups, process limits) which lands with cluster-mode
|
|
/// executor sandboxing. The field stays in the schema so we don't
|
|
/// have to add it back when that's built.
|
|
pub memory_limit_mb: u32,
|
|
|
|
/// Three-state lifecycle (§4.3): `false` means deployed-but-inert — the
|
|
/// script is not invocable (route 404s, trigger doesn't fire) but stays
|
|
/// as desired state (not pruned). Defaults `true`.
|
|
#[serde(default = "crate::default_true")]
|
|
pub enabled: bool,
|
|
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|