Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,7 @@ specs/93-migration-assessment/reports/*/pre-upgrade.dump

# Local cargo target override (SPEC-150 builds)
.cargo-target/

# Isolated SPEC-180 browser-test build output
/edgequake_webui/.next-spec180*/
!/specs/180-improve-sec-doc/e2e/screenshots/*.png
22 changes: 11 additions & 11 deletions edgequake/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 6 additions & 60 deletions edgequake/crates/edgequake-api/src/document_read_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,6 @@ pub async fn list_relational_document_summaries_limited(
title_patterns: &[String],
) -> Result<RelationalDocumentList, crate::error::ApiError> {
use crate::error::ApiError;
use sqlx::Row;

if title_patterns.len() > 16 {
return Err(ApiError::BadRequest(
Expand Down Expand Up @@ -923,65 +922,7 @@ pub async fn list_relational_document_summaries_limited(
let documents = rows
.into_iter()
.take(max_entries)
.map(|row| {
let title: String = row.get("title");
let status: String = row.get("status");
let chunk_count: Option<i32> = row.get("chunk_count");
let entity_count: Option<i32> = row.get("entity_count");
let content_length: Option<i64> = row.get("content_length");
let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at");
let updated_at: chrono::DateTime<chrono::Utc> = row.get("updated_at");
let cost_usd: Option<f64> = row.get("cost_usd");
let input_tokens: Option<i64> = row.get("input_tokens");
let output_tokens: Option<i64> = row.get("output_tokens");
let total_tokens: Option<i64> = row.get("total_tokens");
let track_id: Option<String> = row.get("track_id");
let current_stage: Option<String> = row.get("current_stage");
let content_preview: Option<String> = row.get("content_preview");
let content_summary = content_preview.filter(|s| !s.is_empty());

DocumentSummary {
id: row.get("id"),
title: Some(title.clone()),
file_name: Some(title),
content_summary,
content_length: content_length
.filter(|&n| n >= 0)
.and_then(|n| usize::try_from(n).ok()),
chunk_count: chunk_count.unwrap_or(0).max(0) as usize,
entity_count: entity_count.map(|n| n.max(0) as usize),
status: Some(normalize_relational_status(&status)),
error_message: row.get("error_message"),
warning_message: None,
track_id,
created_at: Some(created_at.to_rfc3339()),
updated_at: Some(updated_at.to_rfc3339()),
cost_usd,
input_tokens: input_tokens.map(|n| n as usize),
output_tokens: output_tokens.map(|n| n as usize),
total_tokens: total_tokens.map(|n| n as usize),
llm_model: None,
embedding_model: None,
source_type: None,
current_stage,
stage_progress: None,
stage_message: None,
pdf_id: None,
display_status: None,
ui_phase: None,
progress_counts: None,
run_progress: None,
queue_position: None,
eta_seconds: None,
eta_basis: None,
query_ready: None,
cancelled_from_stage: None,
extraction: Default::default(),

classification: None,
compartments: None,
}
})
.map(relational_summary_from_row)
.collect();

Ok(RelationalDocumentList {
Expand Down Expand Up @@ -1786,6 +1727,11 @@ pub async fn reconcile_entity_counts_with_graph(
}
}

#[cfg(feature = "postgres")]
mod summary_row;
#[cfg(feature = "postgres")]
pub(crate) use summary_row::relational_summary_from_row;

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! Shared relational document projection for REST and scoped inventory.
use super::{normalize_relational_status, DocumentSummary};

pub(crate) fn relational_summary_from_row(
row: sqlx::postgres::PgRow,
) -> crate::handlers::documents_types::DocumentSummary {
use sqlx::Row;

let title: String = row.get("title");
let status: String = row.get("status");
let chunk_count: Option<i32> = row.get("chunk_count");
let entity_count: Option<i32> = row.get("entity_count");
let content_length: Option<i64> = row.get("content_length");
let created_at: chrono::DateTime<chrono::Utc> = row.get("created_at");
let updated_at: chrono::DateTime<chrono::Utc> = row.get("updated_at");
let cost_usd: Option<f64> = row.get("cost_usd");
let input_tokens: Option<i64> = row.get("input_tokens");
let output_tokens: Option<i64> = row.get("output_tokens");
let total_tokens: Option<i64> = row.get("total_tokens");
let track_id: Option<String> = row.get("track_id");
let current_stage: Option<String> = row.get("current_stage");
let content_preview: Option<String> = row.get("content_preview");
let content_summary = content_preview.filter(|s| !s.is_empty());

DocumentSummary {
id: row.get("id"),
title: Some(title.clone()),
file_name: Some(title),
content_summary,
content_length: content_length
.filter(|&n| n >= 0)
.and_then(|n| usize::try_from(n).ok()),
chunk_count: chunk_count.unwrap_or(0).max(0) as usize,
entity_count: entity_count.map(|n| n.max(0) as usize),
status: Some(normalize_relational_status(&status)),
error_message: row.get("error_message"),
warning_message: None,
track_id,
created_at: Some(created_at.to_rfc3339()),
updated_at: Some(updated_at.to_rfc3339()),
cost_usd,
input_tokens: input_tokens.map(|n| n as usize),
output_tokens: output_tokens.map(|n| n as usize),
total_tokens: total_tokens.map(|n| n as usize),
llm_model: None,
embedding_model: None,
source_type: None,
current_stage,
stage_progress: None,
stage_message: None,
pdf_id: None,
display_status: None,
ui_phase: None,
progress_counts: None,
run_progress: None,
queue_position: None,
eta_seconds: None,
eta_basis: None,
query_ready: None,
cancelled_from_stage: None,
extraction: Default::default(),

classification: None,
compartments: None,
}
}
93 changes: 68 additions & 25 deletions edgequake/crates/edgequake-api/src/handlers/clearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// type-checking, so this lint is allowed for the module.

use axum::extract::{Path, State};
use axum::http::HeaderMap;
use axum::Json;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
Expand All @@ -17,7 +18,7 @@ use uuid::Uuid;
use crate::error::{ApiError, ApiResult};
use crate::handlers::auth::{ApiAuthenticated, RequestAuthContext};
use crate::middleware::TenantContext;
use crate::services::clearance::{self, scope_on};
use crate::services::clearance;
use crate::state::AppState;

#[derive(Debug, Serialize, ToSchema)]
Expand Down Expand Up @@ -113,6 +114,8 @@ pub struct SimulateResponse {
pub struct PatchLabelsRequest {
pub classification: String,
pub compartment_ids: Vec<Uuid>,
pub expected_policy_epoch: i64,
pub reason: Option<String>,
}

fn workspace_of(ctx: &TenantContext, path_id: Uuid) -> Result<Uuid, ApiError> {
Expand Down Expand Up @@ -820,42 +823,82 @@ pub async fn patch_document_labels(
State(state): State<AppState>,
ApiAuthenticated(auth): ApiAuthenticated,
Path(document_id): Path<Uuid>,
tenant_ctx: TenantContext,
mut tenant_ctx: TenantContext,
headers: HeaderMap,
Json(body): Json<PatchLabelsRequest>,
) -> ApiResult<axum::http::StatusCode> {
let workspace_id = tenant_ctx
.workspace_id
.as_deref()
.and_then(|value| Uuid::parse_str(value).ok())
.ok_or_else(|| ApiError::BadRequest("Workspace is required".into()))?;
) -> ApiResult<(HeaderMap, axum::http::StatusCode)> {
tenant_ctx.user_id = Some(auth.user_id);
#[cfg(feature = "postgres")]
{
use crate::services::{document_access as access, document_access_types::DocumentAccess};
let pool = state
.optional_pg_pool()
.ok_or_else(|| ApiError::Internal("PostgreSQL is required".into()))?;
let user_id = Uuid::parse_str(&auth.user_id).ok();
let (scope, _) = clearance::load_scope(pool, workspace_id, user_id).await?;
let _ = scope_on;
let label = clearance::RelabelRequest {
workspace_id,
document_id,
classification: &body.classification,
compartment_ids: &body.compartment_ids,
};
clearance::relabel(
let version = headers
.get("if-match")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("\"label-"))
.and_then(|v| v.strip_suffix('"'))
.and_then(|v| v.parse::<i64>().ok())
.ok_or_else(|| {
access::coded(
axum::http::StatusCode::PRECONDITION_REQUIRED,
"LABEL_PRECONDITION_REQUIRED",
"Refresh access and send its current version",
)
})?;
let operation_id = headers
.get("idempotency-key")
.and_then(|v| v.to_str().ok())
.and_then(|v| Uuid::parse_str(v).ok())
.ok_or_else(|| {
ApiError::bad_request_coded(
"INVALID_ACCESS_PAYLOAD",
"An operation key is required",
)
})?;
let outcome = access::mutate(
pool,
label,
&scope,
tenant_ctx.tenant_id.as_deref().unwrap_or(""),
&auth.user_id,
&access::AccessActor::from_context(&tenant_ctx)?,
access::AccessMutation {
items: vec![access::AccessMutationItem {
document_id,
label_version: version,
access: DocumentAccess {
classification: body.classification,
compartment_ids: body.compartment_ids,
expected_policy_epoch: body.expected_policy_epoch,
},
}],
reason: body.reason,
operation_id,
},
)
.await?;
return Ok(axum::http::StatusCode::NO_CONTENT);
let mut response_headers = HeaderMap::new();
response_headers.insert(
"etag",
format!(
"\"label-{}\"",
outcome["items"][0]["label_version"]
.as_i64()
.unwrap_or(version)
)
.parse()
.map_err(|_| ApiError::Internal("Invalid access version".into()))?,
);
response_headers.insert(
"x-policy-epoch",
outcome["policy_epoch"]
.to_string()
.parse()
.map_err(|_| ApiError::Internal("Invalid policy epoch".into()))?,
);
return Ok((response_headers, axum::http::StatusCode::NO_CONTENT));
}

#[cfg(not(feature = "postgres"))]
{
let _ = (state, auth, document_id, body, scope_on);
let _ = (state, tenant_ctx, headers, body, document_id);
Err(ApiError::Internal("PostgreSQL is required".into()))
}
}
Expand Down
Loading
Loading