blob: b6f0f7b7dc0f7047848d892cb0283111cd26c25a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::{EventLog, StoreError};
use futures_util::TryStreamExt;
use maka_presentation::shell::{ResourceUpdate, local_metadata, local_update};
use sha2::{Digest, Sha256};
use sqlx::{Connection, Row};
/// A bounded page from one read transaction. The digest covers the projected
/// scope (one resource for get, the whole session for list), not OS handles.
pub struct ShellResourcePage {
pub resources: Vec<ResourceUpdate>,
pub revision: String,
pub total: u64,
pub next_offset: Option<u64>,
}
impl EventLog {
pub async fn query_shell_resources(
&self,
session: &str,
id: Option<&str>,
offset: u64,
) -> Result<ShellResourcePage, StoreError> {
self.query_shell_page(session, id, offset, None, false)
.await
}
/// Provider filtering happens before counting, digesting and paging. The
/// list projects metadata only; full canonical output belongs to detail reads.
pub async fn query_shell_catalog(
&self,
session: &str,
provider_prefix: &str,
offset: u64,
) -> Result<ShellResourcePage, StoreError> {
maka_agent_facts::interaction::entity_id(provider_prefix).map_err(super::invalid)?;
self.query_shell_page(session, None, offset, Some(provider_prefix), true)
.await
}
async fn query_shell_page(
&self,
session: &str,
id: Option<&str>,
offset: u64,
provider_prefix: Option<&str>,
compact: bool,
) -> Result<ShellResourcePage, StoreError> {
self.validate_root()?;
maka_agent_facts::interaction::entity_id(session).map_err(super::invalid)?;
if let Some(id) = id {
maka_agent_facts::interaction::entity_id(id).map_err(super::invalid)?;
}
let (session, id) = (session.to_owned(), id.map(str::to_owned));
let provider_prefix = provider_prefix.map(str::to_owned);
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
let exists: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM session_control WHERE id = ?)",
)
.bind(&session)
.fetch_one(&mut *tx)
.await?;
if !exists {
return Err(StoreError::SessionNotFound);
}
let mut rows = sqlx::query(
"SELECT id, record_json FROM shell_runs \
WHERE session_id = ? AND (? IS NULL OR id = ?) \
AND (? IS NULL OR substr(id, 1, length(?)) = ?) ORDER BY id",
)
.bind(&session)
.bind(&id)
.bind(&id)
.bind(&provider_prefix)
.bind(&provider_prefix)
.bind(&provider_prefix)
.fetch(&mut *tx);
let mut hash = Sha256::new();
hash.update(b"[");
let mut total = 0_u64;
let mut resources = Vec::new();
// The page envelope (IDs, digest, cursor and keys) is <512 bytes.
// JSON-escaped source tool IDs belong to each serialized resource.
let mut page_bytes = 512;
let mut page_full = false;
while let Some(row) = rows.try_next().await? {
let record = super::decode(
row.try_get("record_json")?,
&session,
row.try_get("id")?,
)?;
let projected = if compact {
local_metadata(record)?
} else {
local_update(record)?
};
let bytes = serde_json::to_vec(&projected)?;
if total != 0 {
hash.update(b",");
}
hash.update(&bytes);
if total >= offset && !page_full {
if resources.len() == 64
|| (!resources.is_empty()
&& page_bytes + bytes.len() + 1 > 52 * 1024)
{
page_full = true;
} else {
page_bytes += bytes.len() + 1;
resources.push(projected);
}
}
total += 1;
}
drop(rows);
tx.rollback().await?;
hash.update(b"]");
let next = offset.saturating_add(resources.len() as u64);
Ok(ShellResourcePage {
resources,
revision: format!("sha256:{}", hex::encode(hash.finalize())),
total,
next_offset: (next < total).then_some(next),
})
})
})
.await
}
}