1use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::Caller;
13
14#[derive(Debug, Clone)]
15pub struct ReportScope {
16 pub org_id: i64,
17 pub caller: Caller,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
23#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
24pub struct ReportTimeRange {
25 #[cfg_attr(feature = "openapi", schema(example = "2026-04-24T00:00:00Z"))]
27 pub from: DateTime<Utc>,
28 #[cfg_attr(feature = "openapi", schema(example = "2026-05-24T00:00:00Z"))]
30 pub to: DateTime<Utc>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
37#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
38pub struct ReportQuery {
39 #[cfg_attr(feature = "openapi", schema(example = "sessions"))]
41 pub dataset: String,
42 pub time_range: ReportTimeRange,
44 #[serde(default)]
46 #[cfg_attr(feature = "openapi", schema(example = json!(["status"])))]
47 pub dimensions: Vec<String>,
48 #[serde(default)]
50 #[cfg_attr(feature = "openapi", schema(example = json!(["session_count", "avg_duration_ms"])))]
51 pub measures: Vec<String>,
52 #[serde(default)]
54 pub filters: Vec<ReportFilter>,
55 #[serde(default)]
57 pub order_by: Vec<ReportOrderBy>,
58 #[serde(default = "default_report_limit")]
60 #[cfg_attr(feature = "openapi", schema(example = 100))]
61 pub limit: u32,
62}
63
64fn default_report_limit() -> u32 {
65 100
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
71#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
72pub struct ReportFilter {
73 #[cfg_attr(feature = "openapi", schema(example = "status"))]
75 pub field: String,
76 pub op: ReportFilterOp,
79 pub value: Value,
82}
83
84#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(rename_all = "snake_case")]
88#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
89pub enum ReportFilterOp {
90 Eq,
91 Neq,
92 In,
93 Gt,
94 Gte,
95 Lt,
96 Lte,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
102#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
103pub struct ReportOrderBy {
104 #[serde(default)]
106 #[cfg_attr(feature = "openapi", schema(example = "org_id"))]
107 pub dimension: Option<String>,
108 #[serde(default)]
110 #[cfg_attr(feature = "openapi", schema(example = "session_count"))]
111 pub measure: Option<String>,
112 #[serde(default = "default_order_direction")]
114 pub direction: ReportOrderDirection,
115}
116
117fn default_order_direction() -> ReportOrderDirection {
118 ReportOrderDirection::Asc
119}
120
121#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
123#[serde(rename_all = "snake_case")]
124#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
125pub enum ReportOrderDirection {
126 Asc,
127 Desc,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
133#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
134pub struct ReportResult {
135 pub as_of: DateTime<Utc>,
138 #[serde(skip_serializing_if = "Option::is_none")]
142 pub freshness_lag_ms: Option<i64>,
143 pub columns: Vec<ReportColumn>,
146 pub rows: Vec<Value>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
155#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
156pub struct ReportColumn {
157 #[cfg_attr(feature = "openapi", schema(example = "session_count"))]
159 pub name: String,
160 pub kind: ReportColumnKind,
163}
164
165#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
167#[serde(rename_all = "snake_case")]
168#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
169pub enum ReportColumnKind {
170 Dimension,
171 Measure,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
177#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
178pub struct DatasetCatalog {
179 pub datasets: Vec<DatasetCatalogEntry>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
186#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
187pub struct DatasetCatalogEntry {
188 pub name: String,
190 pub dimensions: Vec<String>,
192 pub measures: Vec<String>,
194 pub filter_fields: Vec<String>,
196}
197
198#[derive(Debug, Clone)]
199pub struct SourceKey {
200 pub source_type: String,
201 pub source_id: String,
202}
203
204#[derive(Debug, Clone)]
205pub struct FactBatch {
206 pub records: Vec<FactRecord>,
207}
208
209#[derive(Debug, Clone)]
210pub struct FactRecord {
211 pub dataset: String,
212 pub org_id: i64,
213 pub source_key: String,
214 pub values: Value,
215}
216
217#[async_trait]
218pub trait ReportingProjectionSink: Send + Sync {
219 async fn upsert_facts(&self, batch: FactBatch) -> anyhow::Result<()>;
220 async fn supersede_source(&self, source: SourceKey) -> anyhow::Result<()>;
221}
222
223#[async_trait]
224pub trait ReportingQueryBackend: Send + Sync {
225 async fn query(&self, scope: ReportScope, query: ReportQuery) -> anyhow::Result<ReportResult>;
226}