use crate::error::{Error, Result};
use crate::id::Uid;
use crate::model::{Content, StoredValue, Value};
use crate::storage::Position;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchMode {
#[default]
Exact,
ExactCi,
Prefix,
Contains,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetFilter {
pub entity_type: String,
pub field: String,
pub value: Value,
#[serde(default = "default_include_past")]
pub include_past: bool,
#[serde(default)]
pub mode: MatchMode,
}
fn default_include_past() -> bool {
true
}
impl TargetFilter {
pub fn exact(entity_type: impl Into<String>, field: impl Into<String>, value: Value) -> Self {
Self {
entity_type: entity_type.into(),
field: field.into(),
value,
include_past: true,
mode: MatchMode::Exact,
}
}
pub fn contains(mut self) -> Self {
self.mode = MatchMode::Contains;
self
}
pub fn exact_ci(mut self) -> Self {
self.mode = MatchMode::ExactCi;
self
}
pub fn prefix(mut self) -> Self {
self.mode = MatchMode::Prefix;
self
}
pub fn latest_only(mut self) -> Self {
self.include_past = false;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Order {
Asc,
#[default]
Desc,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct LogQuery {
pub from_micros: Option<u64>,
pub to_micros: Option<u64>,
pub method: Option<String>,
pub url_prefix: Option<String>,
pub actor: Option<TargetFilter>,
pub targets: Vec<TargetFilter>,
pub custom: BTreeMap<String, Value>,
pub limit: Option<usize>,
pub order: Order,
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryPage {
pub logs: Vec<LogView>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
pub segments_scanned: u64,
}
const CURSOR_V1: u8 = 1;
const CURSOR_LEN: usize = 1 + 1 + 8 + 8;
pub(crate) fn encode_cursor(order: Order, pos: Position) -> String {
let mut b = [0u8; CURSOR_LEN];
b[0] = CURSOR_V1;
b[1] = (order == Order::Desc) as u8;
b[2..10].copy_from_slice(&pos.seq.to_le_bytes());
b[10..18].copy_from_slice(&pos.idx.to_le_bytes());
URL_SAFE_NO_PAD.encode(b)
}
pub(crate) fn decode_cursor(cursor: &str, order: Order) -> Result<Position> {
let bytes = URL_SAFE_NO_PAD
.decode(cursor)
.map_err(|_| Error::InvalidCursor("not a query cursor".into()))?;
let b: [u8; CURSOR_LEN] = bytes
.try_into()
.map_err(|_| Error::InvalidCursor("not a query cursor".into()))?;
if b[0] != CURSOR_V1 {
return Err(Error::InvalidCursor(format!(
"unsupported cursor version {}",
b[0]
)));
}
let cursor_desc = b[1] != 0;
if cursor_desc != (order == Order::Desc) {
return Err(Error::InvalidCursor(
"cursor was issued under the opposite sort order".into(),
));
}
Ok(Position {
seq: u64::from_le_bytes(b[2..10].try_into().unwrap()),
idx: u64::from_le_bytes(b[10..18].try_into().unwrap()),
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetSnapshot {
pub entity_registry_uid: Uid,
pub entity_type: String,
pub entity_id: String,
pub version: u32,
pub fields: BTreeMap<String, StoredValue>,
pub deleted: bool,
#[serde(default)]
pub missing: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogView {
#[serde(with = "crate::id::hex_serde")]
pub log_id: Uid,
pub timestamp_micros: u64,
pub timestamp: String,
pub actor: TargetSnapshot,
pub method: String,
pub url: String,
pub content: Content,
pub targets: Vec<TargetSnapshot>,
pub custom: BTreeMap<String, Value>,
}