feat(cli): add pic docs/dead-letters ls group read mirrors
Two read-only operator surfaces existed server-side but the CLI was app-only:
- `pic dead-letters ls --group <slug>` — mirrors the app listing onto a group's
shared-queue dead-letters (§11.6 D3, `GET /groups/{id}/dead-letters`), via the
existing `require_one_owner`/`OwnerRef` dispatch. List-only (show/replay/
resolve stay app-only, matching the server's operator surface); a group DL row
shows its `collection` and carries no trigger, so it renders its own columns.
- `pic docs ls/get --group <slug>` — a new `Docs` command (`cmds/docs.rs`)
wrapping `GET /groups/{id}/docs[/{collection}/{id}]`, mirroring `pic kv`.
Group-only: per-app docs have no admin read route yet (unlike kv/files), so
there is no `--app` variant — noted for a later pass.
Read-only by design (writes go through the SDK). New client methods +
`GroupDocsListDto`/`GroupDeadLetterDto`. Pinned by a new
`operator_reads_shared_docs_and_group_dead_letters_via_cli` journey
(152/152 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,33 +18,73 @@ pub async fn count(app: &str, mode: OutputMode) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ls(app: &str, unresolved: bool, limit: u32, mode: OutputMode) -> Result<()> {
|
||||
/// `pic dead-letters ls` — list rows for an app (`--app`) or a group's shared
|
||||
/// queues (`--group`, §11.6 D3; list-only, no show/replay/resolve — those stay
|
||||
/// app-only, matching the server's operator surface).
|
||||
pub async fn ls(
|
||||
app: Option<&str>,
|
||||
group: Option<&str>,
|
||||
unresolved: bool,
|
||||
limit: u32,
|
||||
mode: OutputMode,
|
||||
) -> Result<()> {
|
||||
let creds = config::resolve()?;
|
||||
let client = Client::from_creds(&creds)?;
|
||||
let resp = client.dead_letters_list(app, unresolved, limit).await?;
|
||||
let mut table = Table::new([
|
||||
"id",
|
||||
"source",
|
||||
"op",
|
||||
"attempts",
|
||||
"resolved",
|
||||
"last_error",
|
||||
"created_at",
|
||||
]);
|
||||
for d in resp.dead_letters {
|
||||
let last_err = truncate(&d.last_error, 60);
|
||||
let resolved = d.resolution.unwrap_or_else(|| "-".into());
|
||||
table.row([
|
||||
d.id,
|
||||
d.source,
|
||||
d.op,
|
||||
d.attempt_count.to_string(),
|
||||
resolved,
|
||||
last_err,
|
||||
d.created_at.to_rfc3339(),
|
||||
]);
|
||||
match crate::cmds::require_one_owner(app, group)? {
|
||||
crate::cmds::OwnerRef::App(a) => {
|
||||
let resp = client.dead_letters_list(a, unresolved, limit).await?;
|
||||
let mut table = Table::new([
|
||||
"id",
|
||||
"source",
|
||||
"op",
|
||||
"attempts",
|
||||
"resolved",
|
||||
"last_error",
|
||||
"created_at",
|
||||
]);
|
||||
for d in resp.dead_letters {
|
||||
let resolved = d.resolution.unwrap_or_else(|| "-".into());
|
||||
table.row([
|
||||
d.id,
|
||||
d.source,
|
||||
d.op,
|
||||
d.attempt_count.to_string(),
|
||||
resolved,
|
||||
truncate(&d.last_error, 60),
|
||||
d.created_at.to_rfc3339(),
|
||||
]);
|
||||
}
|
||||
table.print(mode);
|
||||
}
|
||||
crate::cmds::OwnerRef::Group(g) => {
|
||||
let rows = client.group_dead_letters_list(g, unresolved, limit).await?;
|
||||
// A group DL carries the `collection` it belonged to and no trigger.
|
||||
let mut table = Table::new([
|
||||
"id",
|
||||
"collection",
|
||||
"source",
|
||||
"op",
|
||||
"attempts",
|
||||
"resolved",
|
||||
"last_error",
|
||||
"created_at",
|
||||
]);
|
||||
for d in rows {
|
||||
let resolved = d.resolved_at.unwrap_or_else(|| "-".into());
|
||||
table.row([
|
||||
d.id,
|
||||
d.collection,
|
||||
d.source,
|
||||
d.op,
|
||||
d.attempt_count.to_string(),
|
||||
resolved,
|
||||
truncate(&d.last_error, 60),
|
||||
d.created_at,
|
||||
]);
|
||||
}
|
||||
table.print(mode);
|
||||
}
|
||||
}
|
||||
table.print(mode);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
38
crates/picloud-cli/src/cmds/docs.rs
Normal file
38
crates/picloud-cli/src/cmds/docs.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
//! `pic docs ls | get --group` — read-only inspection of a group's shared-docs
|
||||
//! collection (§11.6). Group-only: per-app docs have no admin read route yet
|
||||
//! (unlike kv/files), so there is no `--app` variant here.
|
||||
//!
|
||||
//! Read-only by design (matching `pic kv`): docs writes go through
|
||||
//! `docs::shared_collection(...).create/update` in scripts, which emit the
|
||||
//! change events shared triggers depend on; an admin write would bypass that.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::config;
|
||||
use crate::output::{OutputMode, Table};
|
||||
|
||||
pub async fn ls(group: &str, collection: &str, limit: u32, mode: OutputMode) -> Result<()> {
|
||||
let creds = config::resolve()?;
|
||||
let client = Client::from_creds(&creds)?;
|
||||
let page = client.group_docs_list(group, collection, limit).await?;
|
||||
let mut table = Table::new(["id"]);
|
||||
for d in page.docs {
|
||||
table.row([d.id]);
|
||||
}
|
||||
table.print(mode);
|
||||
if page.next_cursor.is_some() {
|
||||
eprintln!("(more documents available — raise --limit to see them)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get(group: &str, collection: &str, doc_id: &str) -> Result<()> {
|
||||
let creds = config::resolve()?;
|
||||
let client = Client::from_creds(&creds)?;
|
||||
let value = client.group_docs_get(group, collection, doc_id).await?;
|
||||
// Emit the document JSON (pretty) so it pipes cleanly into jq.
|
||||
let pretty = serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string());
|
||||
println!("{pretty}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod apps_domains;
|
||||
pub mod collections;
|
||||
pub mod config;
|
||||
pub mod dead_letters;
|
||||
pub mod docs;
|
||||
pub mod extension_points;
|
||||
pub mod files;
|
||||
pub mod groups;
|
||||
|
||||
Reference in New Issue
Block a user