feat(v1.1.9): admin HTTP endpoints — queue triggers + read-only queues
triggers_api.rs adds:
POST /api/v1/admin/apps/{id}/triggers/queue
body: { script_id, queue_name, visibility_timeout_secs?,
dispatch_mode?, retry_max_attempts?, retry_backoff?,
retry_base_ms? }
cap: AppManageTriggers
409-equiv via the repo's "queue 'X' already has a consumer" error
(TriggerRepoError::Invalid → 422 Invalid in the existing mapping)
queues_api.rs (new) adds read-only inspection endpoints:
GET /api/v1/admin/apps/{id}/queues
-> [{queue_name, total, pending, claimed}]
GET /api/v1/admin/apps/{id}/queues/{queue_name}
-> {queue_name, total, pending, claimed,
consumer: Some(QueueConsumer) | None}
QueueConsumer = { trigger_id, script_id, script_name,
visibility_timeout_secs, last_fired_at }
Capability: AppLogRead (same trust tier as execution-log read — these
are read-only operational views, not config changes).
No mutating queue endpoints — enqueue lives on the SDK side; purge /
requeue / delete-message stays at v1.2.
picloud/lib.rs wires the queues router into the guarded /admin chain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,7 @@ pub fn triggers_router(state: TriggersState) -> Router {
|
||||
post(create_dl_trigger),
|
||||
)
|
||||
.route("/apps/{app_id}/triggers/email", post(create_email_trigger))
|
||||
.route("/apps/{app_id}/triggers/queue", post(create_queue_trigger))
|
||||
.route(
|
||||
"/apps/{app_id}/triggers/{trigger_id}",
|
||||
delete(delete_trigger),
|
||||
@@ -376,6 +377,26 @@ pub struct CreatePubsubTriggerRequest {
|
||||
pub retry_base_ms: Option<u32>,
|
||||
}
|
||||
|
||||
/// v1.1.9. Inbound shape for a queue:receive trigger create.
|
||||
/// `visibility_timeout_secs` defaults to TriggerConfig's per-queue
|
||||
/// default if omitted; retry fields default to the standard trigger
|
||||
/// retry policy.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct CreateQueueTriggerRequest {
|
||||
pub script_id: ScriptId,
|
||||
pub queue_name: String,
|
||||
#[serde(default)]
|
||||
pub visibility_timeout_secs: Option<u32>,
|
||||
#[serde(default = "default_dispatch")]
|
||||
pub dispatch_mode: TriggerDispatchMode,
|
||||
#[serde(default)]
|
||||
pub retry_max_attempts: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub retry_backoff: Option<BackoffShape>,
|
||||
#[serde(default)]
|
||||
pub retry_base_ms: Option<u32>,
|
||||
}
|
||||
|
||||
async fn create_pubsub_trigger(
|
||||
State(s): State<TriggersState>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
@@ -527,6 +548,45 @@ async fn create_email_trigger(
|
||||
Ok((StatusCode::CREATED, Json(created)))
|
||||
}
|
||||
|
||||
async fn create_queue_trigger(
|
||||
State(s): State<TriggersState>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
Path(app_id): Path<AppId>,
|
||||
Json(input): Json<CreateQueueTriggerRequest>,
|
||||
) -> Result<(StatusCode, Json<Trigger>), TriggersApiError> {
|
||||
ensure_app_exists(&*s.apps, app_id).await?;
|
||||
require(
|
||||
s.authz.as_ref(),
|
||||
&principal,
|
||||
Capability::AppManageTriggers(app_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if input.queue_name.trim().is_empty() {
|
||||
return Err(TriggersApiError::Invalid(
|
||||
"queue_name must not be empty".into(),
|
||||
));
|
||||
}
|
||||
validate_trigger_target(&*s.scripts, app_id, input.script_id).await?;
|
||||
|
||||
let req = crate::trigger_repo::CreateQueueTrigger {
|
||||
script_id: input.script_id,
|
||||
queue_name: input.queue_name,
|
||||
visibility_timeout_secs: input
|
||||
.visibility_timeout_secs
|
||||
.unwrap_or(s.config.queue_default_visibility_timeout_secs),
|
||||
dispatch_mode: input.dispatch_mode,
|
||||
retry_max_attempts: input
|
||||
.retry_max_attempts
|
||||
.unwrap_or(s.config.retry_max_attempts),
|
||||
retry_backoff: input.retry_backoff.unwrap_or(s.config.retry_backoff),
|
||||
retry_base_ms: input.retry_base_ms.unwrap_or(s.config.retry_base_ms),
|
||||
registered_by_principal: principal.user_id,
|
||||
};
|
||||
let created = s.triggers.create_queue_trigger(app_id, req).await?;
|
||||
Ok((StatusCode::CREATED, Json(created)))
|
||||
}
|
||||
|
||||
async fn delete_trigger(
|
||||
State(s): State<TriggersState>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
|
||||
Reference in New Issue
Block a user