1#[cfg(feature = "replication")]
2use crate::infrastructure::replication::ReplicationMode;
3use crate::{
4 application::{
5 dto::{
6 AckRequest, ConsumerEventDto, ConsumerEventsResponse, ConsumerResponse,
7 DetectDuplicatesRequest, DetectDuplicatesResponse, DuplicateGroup, EntitySummary,
8 EventDto, IngestEventRequest, IngestEventResponse, IngestEventsBatchRequest,
9 IngestEventsBatchResponse, ListEntitiesRequest, ListEntitiesResponse,
10 QueryEventsRequest, QueryEventsResponse, RegisterConsumerRequest,
11 },
12 services::{
13 analytics::{
14 AnalyticsEngine, CorrelationRequest, CorrelationResponse, EventFrequencyRequest,
15 EventFrequencyResponse, StatsSummaryRequest, StatsSummaryResponse,
16 },
17 pipeline::{PipelineConfig, PipelineStats},
18 replay::{ReplayProgress, StartReplayRequest, StartReplayResponse},
19 schema::{
20 CompatibilityMode, RegisterSchemaRequest, RegisterSchemaResponse,
21 ValidateEventRequest, ValidateEventResponse,
22 },
23 webhook::{RegisterWebhookRequest, UpdateWebhookRequest},
24 },
25 },
26 domain::{
27 entities::{Event, SchemaEnforcement},
28 value_objects::TenantId,
29 },
30 error::Result,
31 infrastructure::{
32 persistence::{
33 compaction::CompactionResult,
34 snapshot::{
35 CreateSnapshotRequest, CreateSnapshotResponse, ListSnapshotsRequest,
36 ListSnapshotsResponse, SnapshotInfo,
37 },
38 },
39 query::{
40 geospatial::GeoQueryRequest,
41 graphql::{GraphQLError, GraphQLRequest, GraphQLResponse},
42 },
43 security::middleware::OptionalAuth,
44 web::api_v1::AppState,
45 },
46 store::{EventStore, EventTypeInfo, StreamInfo},
47};
48use axum::{
49 Json, Router,
50 extract::{Path, Query, State, WebSocketUpgrade},
51 response::{IntoResponse, Response},
52 routing::{get, post, put},
53};
54use serde::Deserialize;
55use std::sync::Arc;
56use tower_http::{
57 cors::{Any, CorsLayer},
58 trace::TraceLayer,
59};
60
61type SharedStore = Arc<EventStore>;
62
63#[cfg(feature = "replication")]
69async fn await_replication_ack(state: &AppState) {
70 let shipper_guard = state.wal_shipper.read().await;
71 if let Some(ref shipper) = *shipper_guard {
72 let mode = shipper.replication_mode();
73 if mode == ReplicationMode::Async {
74 return;
75 }
76
77 let target_offset = shipper.current_leader_offset();
78 if target_offset == 0 {
79 return;
80 }
81
82 let shipper = Arc::clone(shipper);
83 drop(shipper_guard);
85
86 let timer = state
87 .store
88 .metrics()
89 .replication_ack_wait_seconds
90 .start_timer();
91 let acked = shipper.wait_for_ack(target_offset).await;
92 timer.observe_duration();
93
94 if !acked {
95 tracing::warn!(
96 "Replication ACK timeout in {} mode (offset {}). \
97 Write succeeded locally but follower confirmation pending.",
98 mode,
99 target_offset,
100 );
101 }
102 }
103}
104#[cfg(not(feature = "replication"))]
105async fn await_replication_ack(_state: &AppState) {
106 }
108
109pub async fn serve(store: SharedStore, addr: &str) -> anyhow::Result<()> {
110 let app = Router::new()
111 .route("/health", get(health))
112 .route("/metrics", get(prometheus_metrics)) .route("/api/v1/events", post(ingest_event))
114 .route("/api/v1/events/batch", post(ingest_events_batch))
115 .route("/api/v1/events/query", get(query_events))
116 .route("/api/v1/events/{event_id}", get(get_event_by_id))
117 .route("/api/v1/events/stream", get(events_websocket)) .route("/api/v1/streams", get(list_streams))
120 .route("/api/v1/event-types", get(list_event_types))
121 .route("/api/v1/entities/duplicates", get(detect_duplicates))
122 .route("/api/v1/entities/{entity_id}/state", get(get_entity_state))
123 .route(
124 "/api/v1/entities/{entity_id}/snapshot",
125 get(get_entity_snapshot),
126 )
127 .route("/api/v1/stats", get(get_stats))
128 .route("/api/v1/analytics/frequency", get(analytics_frequency))
130 .route("/api/v1/analytics/summary", get(analytics_summary))
131 .route("/api/v1/analytics/correlation", get(analytics_correlation))
132 .route("/api/v1/snapshots", post(create_snapshot))
134 .route("/api/v1/snapshots", get(list_snapshots))
135 .route(
136 "/api/v1/snapshots/{entity_id}/latest",
137 get(get_latest_snapshot),
138 )
139 .route("/api/v1/compaction/trigger", post(trigger_compaction))
141 .route("/api/v1/compaction/stats", get(compaction_stats))
142 .route("/api/v1/schemas", post(register_schema))
144 .route("/api/v1/schemas", get(list_subjects))
145 .route("/api/v1/schemas/{subject}", get(get_schema))
146 .route(
147 "/api/v1/schemas/{subject}/versions",
148 get(list_schema_versions),
149 )
150 .route("/api/v1/schemas/validate", post(validate_event_schema))
151 .route(
152 "/api/v1/schemas/{subject}/compatibility",
153 put(set_compatibility_mode),
154 )
155 .route("/api/v1/replay", post(start_replay))
157 .route("/api/v1/replay", get(list_replays))
158 .route("/api/v1/replay/{replay_id}", get(get_replay_progress))
159 .route("/api/v1/replay/{replay_id}/cancel", post(cancel_replay))
160 .route(
161 "/api/v1/replay/{replay_id}",
162 axum::routing::delete(delete_replay),
163 )
164 .route("/api/v1/pipelines", post(register_pipeline))
166 .route("/api/v1/pipelines", get(list_pipelines))
167 .route("/api/v1/pipelines/stats", get(all_pipeline_stats))
168 .route("/api/v1/pipelines/{pipeline_id}", get(get_pipeline))
169 .route(
170 "/api/v1/pipelines/{pipeline_id}",
171 axum::routing::delete(remove_pipeline),
172 )
173 .route(
174 "/api/v1/pipelines/{pipeline_id}/stats",
175 get(get_pipeline_stats),
176 )
177 .route("/api/v1/pipelines/{pipeline_id}/reset", put(reset_pipeline))
178 .route("/api/v1/projections", get(list_projections))
180 .route("/api/v1/projections/{name}", get(get_projection))
181 .route(
182 "/api/v1/projections/{name}",
183 axum::routing::delete(delete_projection),
184 )
185 .route(
186 "/api/v1/projections/{name}/state",
187 get(get_projection_state_summary),
188 )
189 .route("/api/v1/projections/{name}/reset", post(reset_projection))
190 .route(
191 "/api/v1/projections/{name}/{entity_id}/state",
192 get(get_projection_state),
193 )
194 .route(
195 "/api/v1/projections/{name}/{entity_id}/state",
196 post(save_projection_state),
197 )
198 .route(
199 "/api/v1/projections/{name}/{entity_id}/state",
200 put(save_projection_state),
201 )
202 .route(
203 "/api/v1/projections/{name}/bulk",
204 post(bulk_get_projection_states),
205 )
206 .route(
207 "/api/v1/projections/{name}/bulk/save",
208 post(bulk_save_projection_states),
209 )
210 .route("/api/v1/webhooks", post(register_webhook))
212 .route("/api/v1/webhooks", get(list_webhooks))
213 .route("/api/v1/webhooks/{webhook_id}", get(get_webhook))
214 .route("/api/v1/webhooks/{webhook_id}", put(update_webhook))
215 .route(
216 "/api/v1/webhooks/{webhook_id}",
217 axum::routing::delete(delete_webhook),
218 )
219 .route(
220 "/api/v1/webhooks/{webhook_id}/deliveries",
221 get(list_webhook_deliveries),
222 )
223 .route("/api/v1/graphql", post(graphql_query))
225 .route("/api/v1/geospatial/query", post(geo_query))
226 .route("/api/v1/geospatial/stats", get(geo_stats))
227 .route("/api/v1/exactly-once/stats", get(exactly_once_stats))
228 .route(
229 "/api/v1/schema-evolution/history/{event_type}",
230 get(schema_evolution_history),
231 )
232 .route(
233 "/api/v1/schema-evolution/schema/{event_type}",
234 get(schema_evolution_schema),
235 )
236 .route(
237 "/api/v1/schema-evolution/stats",
238 get(schema_evolution_stats),
239 )
240 .layer(
241 CorsLayer::new()
242 .allow_origin(Any)
243 .allow_methods(Any)
244 .allow_headers(Any),
245 )
246 .layer(TraceLayer::new_for_http())
247 .with_state(store);
248
249 let listener = tokio::net::TcpListener::bind(addr).await?;
250 axum::serve(listener, app).await?;
251
252 Ok(())
253}
254
255pub async fn health() -> impl IntoResponse {
256 Json(serde_json::json!({
257 "status": "healthy",
258 "service": "allsource-core",
259 "version": env!("CARGO_PKG_VERSION")
260 }))
261}
262
263pub async fn prometheus_metrics(State(store): State<SharedStore>) -> impl IntoResponse {
265 let metrics = store.metrics();
266
267 match metrics.encode() {
268 Ok(encoded) => Response::builder()
269 .status(200)
270 .header("Content-Type", "text/plain; version=0.0.4")
271 .body(encoded)
272 .unwrap()
273 .into_response(),
274 Err(e) => Response::builder()
275 .status(500)
276 .body(format!("Error encoding metrics: {e}"))
277 .unwrap()
278 .into_response(),
279 }
280}
281
282pub async fn ingest_event(
283 State(store): State<SharedStore>,
284 Json(req): Json<IngestEventRequest>,
285) -> Result<Json<IngestEventResponse>> {
286 let expected_version = req.expected_version;
287
288 let tenant_id = req.tenant_id.unwrap_or_else(|| "default".to_string());
289 let event = Event::from_strings(
290 req.event_type,
291 req.entity_id,
292 tenant_id,
293 req.payload,
294 req.metadata,
295 )?;
296
297 let event_id = event.id;
298 let timestamp = event.timestamp;
299
300 let new_version = store.ingest_with_expected_version(&event, expected_version)?;
301
302 tracing::info!("Event ingested: {}", event_id);
303
304 Ok(Json(IngestEventResponse {
305 event_id,
306 timestamp,
307 version: Some(new_version),
308 }))
309}
310
311async fn enforce_schema_if_configured(
332 state: &AppState,
333 tenant_id: &str,
334 event: &Event,
335) -> Result<()> {
336 let Ok(parsed) = TenantId::new(tenant_id.to_string()) else {
340 return Ok(());
341 };
342 let mode = match state.tenant_repo.find_by_id(&parsed).await {
343 Ok(Some(t)) => t.schema_enforcement(),
344 _ => SchemaEnforcement::Permissive,
347 };
348 if matches!(mode, SchemaEnforcement::Permissive) {
349 return Ok(());
350 }
351
352 let registry = state.store.schema_registry();
356 let Ok(schema) = registry.get_schema(event.event_type.as_str(), None) else {
357 return Ok(());
360 };
361
362 let result = registry
363 .validate(
364 event.event_type.as_str(),
365 Some(schema.version),
366 &event.payload,
367 )
368 .map_err(|e| crate::error::AllSourceError::InternalError(e.to_string()))?;
369
370 if result.valid {
371 return Ok(());
372 }
373
374 match mode {
375 SchemaEnforcement::Strict => Err(crate::error::AllSourceError::SchemaViolation {
376 event_type: event.event_type.as_str().to_string(),
377 schema_version: result.schema_version,
378 errors: result.errors,
379 }),
380 SchemaEnforcement::Warn => {
381 tracing::warn!(
382 tenant = %tenant_id,
383 event_type = %event.event_type.as_str(),
384 schema_version = result.schema_version,
385 errors = ?result.errors,
386 "schema violation (warn mode — write accepted)"
387 );
388 Ok(())
389 }
390 SchemaEnforcement::Permissive => Ok(()),
392 }
393}
394
395pub async fn ingest_event_v1(
396 State(state): State<AppState>,
397 Json(req): Json<IngestEventRequest>,
398) -> Result<Json<IngestEventResponse>> {
399 let expected_version = req.expected_version;
400
401 let tenant_id = req.tenant_id.unwrap_or_else(|| "default".to_string());
402
403 let event = Event::from_strings(
404 req.event_type,
405 req.entity_id,
406 tenant_id.clone(),
407 req.payload,
408 req.metadata,
409 )?;
410
411 enforce_schema_if_configured(&state, &tenant_id, &event).await?;
414
415 let event_id = event.id;
416 let timestamp = event.timestamp;
417
418 let new_version = state
419 .store
420 .ingest_with_expected_version(&event, expected_version)?;
421
422 await_replication_ack(&state).await;
424
425 tracing::info!("Event ingested: {}", event_id);
426
427 Ok(Json(IngestEventResponse {
428 event_id,
429 timestamp,
430 version: Some(new_version),
431 }))
432}
433
434pub async fn ingest_events_batch(
439 State(store): State<SharedStore>,
440 Json(req): Json<IngestEventsBatchRequest>,
441) -> Result<Json<IngestEventsBatchResponse>> {
442 let total = req.events.len();
443 let mut ingested_events = Vec::with_capacity(total);
444
445 for event_req in req.events {
446 let tenant_id = event_req.tenant_id.unwrap_or_else(|| "default".to_string());
447 let expected_version = event_req.expected_version;
448
449 let event = Event::from_strings(
450 event_req.event_type,
451 event_req.entity_id,
452 tenant_id,
453 event_req.payload,
454 event_req.metadata,
455 )?;
456
457 let event_id = event.id;
463 let timestamp = event.timestamp;
464
465 let new_version = store.ingest_with_expected_version(&event, expected_version)?;
466
467 ingested_events.push(IngestEventResponse {
468 event_id,
469 timestamp,
470 version: Some(new_version),
471 });
472 }
473
474 let ingested = ingested_events.len();
475 tracing::info!("Batch ingested {} events", ingested);
476
477 Ok(Json(IngestEventsBatchResponse {
478 total,
479 ingested,
480 events: ingested_events,
481 }))
482}
483
484pub async fn ingest_events_batch_v1(
490 State(state): State<AppState>,
491 Json(req): Json<IngestEventsBatchRequest>,
492) -> Result<Json<IngestEventsBatchResponse>> {
493 let total = req.events.len();
494 let mut ingested_events = Vec::with_capacity(total);
495
496 for event_req in req.events {
497 let tenant_id = event_req.tenant_id.unwrap_or_else(|| "default".to_string());
498 let expected_version = event_req.expected_version;
499
500 let event = Event::from_strings(
501 event_req.event_type,
502 event_req.entity_id,
503 tenant_id.clone(),
504 event_req.payload,
505 event_req.metadata,
506 )?;
507
508 enforce_schema_if_configured(&state, &tenant_id, &event).await?;
509
510 let event_id = event.id;
511 let timestamp = event.timestamp;
512
513 let new_version = state
514 .store
515 .ingest_with_expected_version(&event, expected_version)?;
516
517 ingested_events.push(IngestEventResponse {
518 event_id,
519 timestamp,
520 version: Some(new_version),
521 });
522 }
523
524 await_replication_ack(&state).await;
526
527 let ingested = ingested_events.len();
528 tracing::info!("Batch ingested {} events", ingested);
529
530 Ok(Json(IngestEventsBatchResponse {
531 total,
532 ingested,
533 events: ingested_events,
534 }))
535}
536
537#[derive(Debug, Deserialize)]
541pub struct EventOrderParam {
542 pub order: Option<String>,
544}
545
546pub async fn query_events(
547 OptionalAuth(auth): OptionalAuth,
548 Query(req): Query<QueryEventsRequest>,
549 Query(order_param): Query<EventOrderParam>,
550 State(store): State<SharedStore>,
551) -> Result<Json<QueryEventsResponse>> {
552 let requested_limit = req.limit;
553 let queried_entity_id = req.entity_id.clone();
554
555 let descending = match order_param.order.as_deref() {
560 None => false,
561 Some(o) if o.eq_ignore_ascii_case("asc") => false,
562 Some(o) if o.eq_ignore_ascii_case("desc") => true,
563 Some(other) => {
564 return Err(crate::error::AllSourceError::InvalidInput(format!(
565 "invalid 'order' value '{other}': expected 'asc' or 'desc'"
566 )));
567 }
568 };
569
570 let enforced_tenant = req
578 .tenant_id
579 .clone()
580 .or_else(|| auth.as_ref().map(|a| a.tenant_id().to_string()));
581
582 let unlimited_req = QueryEventsRequest {
584 entity_id: req.entity_id,
585 event_type: req.event_type,
586 tenant_id: enforced_tenant,
587 as_of: req.as_of,
588 since: req.since,
589 until: req.until,
590 limit: None,
591 event_type_prefix: req.event_type_prefix,
592 payload_filter: req.payload_filter,
593 };
594 let mut all_events = store.query(&unlimited_req)?;
595 let total_count = all_events.len();
596
597 if descending {
600 all_events.reverse();
601 }
602
603 let limited_events: Vec<Event> = if let Some(limit) = requested_limit {
605 all_events.into_iter().take(limit).collect()
606 } else {
607 all_events
608 };
609
610 let count = limited_events.len();
611 let has_more = count < total_count;
612 let events: Vec<EventDto> = limited_events.iter().map(EventDto::from).collect();
613
614 let entity_version = queried_entity_id
616 .as_deref()
617 .map(|eid| store.get_entity_version(eid));
618
619 tracing::debug!("Query returned {} events (total: {})", count, total_count);
620
621 Ok(Json(QueryEventsResponse {
622 events,
623 count,
624 total_count,
625 has_more,
626 entity_version,
627 }))
628}
629
630pub async fn list_entities(
631 State(store): State<SharedStore>,
632 Query(req): Query<ListEntitiesRequest>,
633) -> Result<Json<ListEntitiesResponse>> {
634 use std::collections::HashMap;
635
636 let query_req = QueryEventsRequest {
638 entity_id: None,
639 event_type: None,
640 tenant_id: None,
641 as_of: None,
642 since: None,
643 until: None,
644 limit: None,
645 event_type_prefix: req.event_type_prefix,
646 payload_filter: req.payload_filter,
647 };
648 let events = store.query(&query_req)?;
649
650 let mut entity_map: HashMap<String, Vec<&Event>> = HashMap::new();
652 for event in &events {
653 entity_map
654 .entry(event.entity_id().to_string())
655 .or_default()
656 .push(event);
657 }
658
659 let ascending = match req.order.as_deref() {
663 None => false,
664 Some(o) if o.eq_ignore_ascii_case("desc") => false,
665 Some(o) if o.eq_ignore_ascii_case("asc") => true,
666 Some(other) => {
667 return Err(crate::error::AllSourceError::InvalidInput(format!(
668 "invalid 'order' value '{other}': expected 'asc' or 'desc'"
669 )));
670 }
671 };
672
673 let mut summaries: Vec<EntitySummary> = entity_map
677 .into_iter()
678 .map(|(entity_id, events)| {
679 let last = events.iter().max_by_key(|e| e.timestamp()).unwrap();
680 EntitySummary {
681 entity_id,
682 event_count: events.len(),
683 last_event_type: last.event_type_str().to_string(),
684 last_event_at: last.timestamp(),
685 }
686 })
687 .collect();
688 summaries.sort_by(|a, b| {
689 let by_time = a.last_event_at.cmp(&b.last_event_at);
690 let by_time = if ascending {
691 by_time
692 } else {
693 by_time.reverse()
694 };
695 by_time.then_with(|| a.entity_id.cmp(&b.entity_id))
696 });
697
698 let total = summaries.len();
699
700 let offset = req.offset.unwrap_or(0);
702 let summaries: Vec<EntitySummary> = summaries.into_iter().skip(offset).collect::<Vec<_>>();
703 let summaries = if let Some(limit) = req.limit {
704 let has_more = summaries.len() > limit;
705 let truncated: Vec<EntitySummary> = summaries.into_iter().take(limit).collect();
706 return Ok(Json(ListEntitiesResponse {
707 entities: truncated,
708 total,
709 has_more,
710 }));
711 } else {
712 summaries
713 };
714
715 Ok(Json(ListEntitiesResponse {
716 entities: summaries,
717 total,
718 has_more: false,
719 }))
720}
721
722pub async fn detect_duplicates(
723 State(store): State<SharedStore>,
724 Query(req): Query<DetectDuplicatesRequest>,
725) -> Result<Json<DetectDuplicatesResponse>> {
726 use std::collections::HashMap;
727
728 let group_by_fields: Vec<&str> = req.group_by.split(',').map(str::trim).collect();
729
730 let query_req = QueryEventsRequest {
732 entity_id: None,
733 event_type: None,
734 tenant_id: None,
735 as_of: None,
736 since: None,
737 until: None,
738 limit: None,
739 event_type_prefix: Some(req.event_type_prefix),
740 payload_filter: None,
741 };
742 let events = store.query(&query_req)?;
743
744 let mut entity_latest: HashMap<String, &Event> = HashMap::new();
747 for event in &events {
748 let eid = event.entity_id().to_string();
749 entity_latest
750 .entry(eid)
751 .and_modify(|existing| {
752 if event.timestamp() > existing.timestamp() {
753 *existing = event;
754 }
755 })
756 .or_insert(event);
757 }
758
759 let mut groups: HashMap<String, Vec<String>> = HashMap::new();
761 for (entity_id, event) in &entity_latest {
762 let payload = event.payload();
763 let mut key_parts = serde_json::Map::new();
764 for field in &group_by_fields {
765 let value = payload
766 .get(*field)
767 .cloned()
768 .unwrap_or(serde_json::Value::Null);
769 key_parts.insert((*field).to_string(), value);
770 }
771 let key_str = serde_json::to_string(&key_parts).unwrap_or_default();
772 groups.entry(key_str).or_default().push(entity_id.clone());
773 }
774
775 let mut duplicate_groups: Vec<DuplicateGroup> = groups
777 .into_iter()
778 .filter(|(_, ids)| ids.len() > 1)
779 .map(|(key_str, mut ids)| {
780 ids.sort();
781 let key: serde_json::Value =
782 serde_json::from_str(&key_str).unwrap_or(serde_json::Value::Null);
783 let count = ids.len();
784 DuplicateGroup {
785 key,
786 entity_ids: ids,
787 count,
788 }
789 })
790 .collect();
791
792 duplicate_groups.sort_by(|a, b| b.count.cmp(&a.count));
794
795 let total = duplicate_groups.len();
796
797 let offset = req.offset.unwrap_or(0);
799 let duplicate_groups: Vec<DuplicateGroup> = duplicate_groups.into_iter().skip(offset).collect();
800
801 if let Some(limit) = req.limit {
802 let has_more = duplicate_groups.len() > limit;
803 let truncated: Vec<DuplicateGroup> = duplicate_groups.into_iter().take(limit).collect();
804 return Ok(Json(DetectDuplicatesResponse {
805 duplicates: truncated,
806 total,
807 has_more,
808 }));
809 }
810
811 Ok(Json(DetectDuplicatesResponse {
812 duplicates: duplicate_groups,
813 total,
814 has_more: false,
815 }))
816}
817
818#[derive(Deserialize)]
819pub struct EntityStateParams {
820 as_of: Option<chrono::DateTime<chrono::Utc>>,
821}
822
823pub async fn get_entity_state(
824 State(store): State<SharedStore>,
825 Path(entity_id): Path<String>,
826 Query(params): Query<EntityStateParams>,
827) -> Result<Json<serde_json::Value>> {
828 let state = store.reconstruct_state(&entity_id, params.as_of)?;
829
830 tracing::info!("State reconstructed for entity: {}", entity_id);
831
832 Ok(Json(state))
833}
834
835pub async fn get_entity_snapshot(
836 State(store): State<SharedStore>,
837 Path(entity_id): Path<String>,
838) -> Result<Json<serde_json::Value>> {
839 let snapshot = store.get_snapshot(&entity_id)?;
840
841 tracing::debug!("Snapshot retrieved for entity: {}", entity_id);
842
843 Ok(Json(snapshot))
844}
845
846pub async fn get_stats(State(store): State<SharedStore>) -> impl IntoResponse {
847 let stats = store.stats();
848 Json(stats)
849}
850
851#[derive(Debug, Deserialize)]
854pub struct ListStreamsParams {
855 pub limit: Option<usize>,
857 pub offset: Option<usize>,
859}
860
861#[derive(Debug, serde::Serialize)]
863pub struct ListStreamsResponse {
864 pub streams: Vec<StreamInfo>,
865 pub total: usize,
866}
867
868pub async fn list_streams(
869 State(store): State<SharedStore>,
870 Query(params): Query<ListStreamsParams>,
871) -> Json<ListStreamsResponse> {
872 let mut streams = store.list_streams();
873 let total = streams.len();
874
875 streams.sort_by(|a, b| b.last_event_at.cmp(&a.last_event_at));
877
878 if let Some(offset) = params.offset {
880 if offset < streams.len() {
881 streams = streams[offset..].to_vec();
882 } else {
883 streams = vec![];
884 }
885 }
886
887 if let Some(limit) = params.limit {
888 streams.truncate(limit);
889 }
890
891 tracing::debug!("Listed {} streams (total: {})", streams.len(), total);
892
893 Json(ListStreamsResponse { streams, total })
894}
895
896#[derive(Debug, Deserialize)]
899pub struct ListEventTypesParams {
900 pub limit: Option<usize>,
902 pub offset: Option<usize>,
904}
905
906#[derive(Debug, serde::Serialize)]
908pub struct ListEventTypesResponse {
909 pub event_types: Vec<EventTypeInfo>,
910 pub total: usize,
911}
912
913pub async fn list_event_types(
914 State(store): State<SharedStore>,
915 Query(params): Query<ListEventTypesParams>,
916) -> Json<ListEventTypesResponse> {
917 let mut event_types = store.list_event_types();
918 let total = event_types.len();
919
920 event_types.sort_by(|a, b| b.event_count.cmp(&a.event_count));
922
923 if let Some(offset) = params.offset {
925 if offset < event_types.len() {
926 event_types = event_types[offset..].to_vec();
927 } else {
928 event_types = vec![];
929 }
930 }
931
932 if let Some(limit) = params.limit {
933 event_types.truncate(limit);
934 }
935
936 tracing::debug!(
937 "Listed {} event types (total: {})",
938 event_types.len(),
939 total
940 );
941
942 Json(ListEventTypesResponse { event_types, total })
943}
944
945#[derive(Debug, Deserialize)]
947pub struct WebSocketParams {
948 pub consumer_id: Option<String>,
949}
950
951pub async fn events_websocket(
952 ws: WebSocketUpgrade,
953 State(store): State<SharedStore>,
954 Query(params): Query<WebSocketParams>,
955) -> Response {
956 let websocket_manager = store.websocket_manager();
957
958 ws.on_upgrade(move |socket| async move {
959 if let Some(consumer_id) = params.consumer_id {
960 websocket_manager
961 .handle_socket_with_consumer(socket, consumer_id, store)
962 .await;
963 } else {
964 websocket_manager.handle_socket(socket).await;
965 }
966 })
967}
968
969pub async fn analytics_frequency(
971 State(store): State<SharedStore>,
972 Query(req): Query<EventFrequencyRequest>,
973) -> Result<Json<EventFrequencyResponse>> {
974 let response = AnalyticsEngine::event_frequency(&store, &req)?;
975
976 tracing::debug!(
977 "Frequency analysis returned {} buckets",
978 response.buckets.len()
979 );
980
981 Ok(Json(response))
982}
983
984pub async fn analytics_summary(
986 State(store): State<SharedStore>,
987 Query(req): Query<StatsSummaryRequest>,
988) -> Result<Json<StatsSummaryResponse>> {
989 let response = AnalyticsEngine::stats_summary(&store, &req)?;
990
991 tracing::debug!(
992 "Stats summary: {} events across {} entities",
993 response.total_events,
994 response.unique_entities
995 );
996
997 Ok(Json(response))
998}
999
1000pub async fn analytics_correlation(
1002 State(store): State<SharedStore>,
1003 Query(req): Query<CorrelationRequest>,
1004) -> Result<Json<CorrelationResponse>> {
1005 let response = AnalyticsEngine::analyze_correlation(&store, req)?;
1006
1007 tracing::debug!(
1008 "Correlation analysis: {}/{} correlated pairs ({:.2}%)",
1009 response.correlated_pairs,
1010 response.total_a,
1011 response.correlation_percentage
1012 );
1013
1014 Ok(Json(response))
1015}
1016
1017pub async fn create_snapshot(
1019 State(store): State<SharedStore>,
1020 Json(req): Json<CreateSnapshotRequest>,
1021) -> Result<Json<CreateSnapshotResponse>> {
1022 store.create_snapshot(&req.entity_id)?;
1023
1024 let snapshot_manager = store.snapshot_manager();
1025 let snapshot = snapshot_manager
1026 .get_latest_snapshot(&req.entity_id)
1027 .ok_or_else(|| crate::error::AllSourceError::EntityNotFound(req.entity_id.clone()))?;
1028
1029 tracing::info!("📸 Created snapshot for entity: {}", req.entity_id);
1030
1031 Ok(Json(CreateSnapshotResponse {
1032 snapshot_id: snapshot.id,
1033 entity_id: snapshot.entity_id,
1034 created_at: snapshot.created_at,
1035 event_count: snapshot.event_count,
1036 size_bytes: snapshot.metadata.size_bytes,
1037 }))
1038}
1039
1040pub async fn list_snapshots(
1042 State(store): State<SharedStore>,
1043 Query(req): Query<ListSnapshotsRequest>,
1044) -> Result<Json<ListSnapshotsResponse>> {
1045 let snapshot_manager = store.snapshot_manager();
1046
1047 let snapshots: Vec<SnapshotInfo> = if let Some(entity_id) = req.entity_id {
1048 snapshot_manager
1049 .get_all_snapshots(&entity_id)
1050 .into_iter()
1051 .map(SnapshotInfo::from)
1052 .collect()
1053 } else {
1054 let entities = snapshot_manager.list_entities();
1056 entities
1057 .iter()
1058 .flat_map(|entity_id| {
1059 snapshot_manager
1060 .get_all_snapshots(entity_id)
1061 .into_iter()
1062 .map(SnapshotInfo::from)
1063 })
1064 .collect()
1065 };
1066
1067 let total = snapshots.len();
1068
1069 tracing::debug!("Listed {} snapshots", total);
1070
1071 Ok(Json(ListSnapshotsResponse { snapshots, total }))
1072}
1073
1074pub async fn get_latest_snapshot(
1076 State(store): State<SharedStore>,
1077 Path(entity_id): Path<String>,
1078) -> Result<Json<serde_json::Value>> {
1079 let snapshot_manager = store.snapshot_manager();
1080
1081 let snapshot = snapshot_manager
1082 .get_latest_snapshot(&entity_id)
1083 .ok_or_else(|| crate::error::AllSourceError::EntityNotFound(entity_id.clone()))?;
1084
1085 tracing::debug!("Retrieved latest snapshot for entity: {}", entity_id);
1086
1087 Ok(Json(serde_json::json!({
1088 "snapshot_id": snapshot.id,
1089 "entity_id": snapshot.entity_id,
1090 "created_at": snapshot.created_at,
1091 "as_of": snapshot.as_of,
1092 "event_count": snapshot.event_count,
1093 "size_bytes": snapshot.metadata.size_bytes,
1094 "snapshot_type": snapshot.metadata.snapshot_type,
1095 "state": snapshot.state
1096 })))
1097}
1098
1099pub async fn trigger_compaction(
1101 State(store): State<SharedStore>,
1102) -> Result<Json<CompactionResult>> {
1103 let compaction_manager = store.compaction_manager().ok_or_else(|| {
1104 crate::error::AllSourceError::InternalError(
1105 "Compaction not enabled (no Parquet storage)".to_string(),
1106 )
1107 })?;
1108
1109 tracing::info!("📦 Manual compaction triggered via API");
1110
1111 let result = compaction_manager.compact_now()?;
1112
1113 Ok(Json(result))
1114}
1115
1116pub async fn compaction_stats(State(store): State<SharedStore>) -> Result<Json<serde_json::Value>> {
1118 let compaction_manager = store.compaction_manager().ok_or_else(|| {
1119 crate::error::AllSourceError::InternalError(
1120 "Compaction not enabled (no Parquet storage)".to_string(),
1121 )
1122 })?;
1123
1124 let stats = compaction_manager.stats();
1125 let config = compaction_manager.config();
1126
1127 Ok(Json(serde_json::json!({
1128 "stats": stats,
1129 "config": {
1130 "min_files_to_compact": config.min_files_to_compact,
1131 "target_file_size": config.target_file_size,
1132 "max_file_size": config.max_file_size,
1133 "small_file_threshold": config.small_file_threshold,
1134 "compaction_interval_seconds": config.compaction_interval_seconds,
1135 "auto_compact": config.auto_compact,
1136 "strategy": config.strategy
1137 }
1138 })))
1139}
1140
1141pub async fn register_schema(
1143 State(store): State<SharedStore>,
1144 Json(req): Json<RegisterSchemaRequest>,
1145) -> Result<Json<RegisterSchemaResponse>> {
1146 let schema_registry = store.schema_registry();
1147
1148 let response =
1149 schema_registry.register_schema(req.subject, req.schema, req.description, req.tags)?;
1150
1151 tracing::info!(
1152 "📋 Schema registered: v{} for '{}'",
1153 response.version,
1154 response.subject
1155 );
1156
1157 Ok(Json(response))
1158}
1159
1160#[derive(Deserialize)]
1162pub struct GetSchemaParams {
1163 version: Option<u32>,
1164}
1165
1166pub async fn get_schema(
1167 State(store): State<SharedStore>,
1168 Path(subject): Path<String>,
1169 Query(params): Query<GetSchemaParams>,
1170) -> Result<Json<serde_json::Value>> {
1171 let schema_registry = store.schema_registry();
1172
1173 let schema = schema_registry.get_schema(&subject, params.version)?;
1174
1175 tracing::debug!("Retrieved schema v{} for '{}'", schema.version, subject);
1176
1177 Ok(Json(serde_json::json!({
1178 "id": schema.id,
1179 "subject": schema.subject,
1180 "version": schema.version,
1181 "schema": schema.schema,
1182 "created_at": schema.created_at,
1183 "description": schema.description,
1184 "tags": schema.tags
1185 })))
1186}
1187
1188pub async fn list_schema_versions(
1190 State(store): State<SharedStore>,
1191 Path(subject): Path<String>,
1192) -> Result<Json<serde_json::Value>> {
1193 let schema_registry = store.schema_registry();
1194
1195 let versions = schema_registry.list_versions(&subject)?;
1196
1197 Ok(Json(serde_json::json!({
1198 "subject": subject,
1199 "versions": versions
1200 })))
1201}
1202
1203pub async fn list_subjects(State(store): State<SharedStore>) -> Json<serde_json::Value> {
1205 let schema_registry = store.schema_registry();
1206
1207 let subjects = schema_registry.list_subjects();
1208
1209 Json(serde_json::json!({
1210 "subjects": subjects,
1211 "total": subjects.len()
1212 }))
1213}
1214
1215pub async fn validate_event_schema(
1217 State(store): State<SharedStore>,
1218 Json(req): Json<ValidateEventRequest>,
1219) -> Result<Json<ValidateEventResponse>> {
1220 let schema_registry = store.schema_registry();
1221
1222 let response = schema_registry.validate(&req.subject, req.version, &req.payload)?;
1223
1224 if response.valid {
1225 tracing::debug!(
1226 "✅ Event validated against schema '{}' v{}",
1227 req.subject,
1228 response.schema_version
1229 );
1230 } else {
1231 tracing::warn!(
1232 "❌ Event validation failed for '{}': {:?}",
1233 req.subject,
1234 response.errors
1235 );
1236 }
1237
1238 Ok(Json(response))
1239}
1240
1241#[derive(Deserialize)]
1243pub struct SetCompatibilityRequest {
1244 compatibility: CompatibilityMode,
1245}
1246
1247pub async fn set_compatibility_mode(
1248 State(store): State<SharedStore>,
1249 Path(subject): Path<String>,
1250 Json(req): Json<SetCompatibilityRequest>,
1251) -> Json<serde_json::Value> {
1252 let schema_registry = store.schema_registry();
1253
1254 schema_registry.set_compatibility_mode(subject.clone(), req.compatibility);
1255
1256 tracing::info!(
1257 "🔧 Set compatibility mode for '{}' to {:?}",
1258 subject,
1259 req.compatibility
1260 );
1261
1262 Json(serde_json::json!({
1263 "subject": subject,
1264 "compatibility": req.compatibility
1265 }))
1266}
1267
1268pub async fn start_replay(
1270 State(store): State<SharedStore>,
1271 Json(req): Json<StartReplayRequest>,
1272) -> Result<Json<StartReplayResponse>> {
1273 let replay_manager = store.replay_manager();
1274
1275 let response = replay_manager.start_replay(store, req)?;
1276
1277 tracing::info!(
1278 "🔄 Started replay {} with {} events",
1279 response.replay_id,
1280 response.total_events
1281 );
1282
1283 Ok(Json(response))
1284}
1285
1286pub async fn get_replay_progress(
1288 State(store): State<SharedStore>,
1289 Path(replay_id): Path<uuid::Uuid>,
1290) -> Result<Json<ReplayProgress>> {
1291 let replay_manager = store.replay_manager();
1292
1293 let progress = replay_manager.get_progress(replay_id)?;
1294
1295 Ok(Json(progress))
1296}
1297
1298pub async fn list_replays(State(store): State<SharedStore>) -> Json<serde_json::Value> {
1300 let replay_manager = store.replay_manager();
1301
1302 let replays = replay_manager.list_replays();
1303
1304 Json(serde_json::json!({
1305 "replays": replays,
1306 "total": replays.len()
1307 }))
1308}
1309
1310pub async fn cancel_replay(
1312 State(store): State<SharedStore>,
1313 Path(replay_id): Path<uuid::Uuid>,
1314) -> Result<Json<serde_json::Value>> {
1315 let replay_manager = store.replay_manager();
1316
1317 replay_manager.cancel_replay(replay_id)?;
1318
1319 tracing::info!("🛑 Cancelled replay {}", replay_id);
1320
1321 Ok(Json(serde_json::json!({
1322 "replay_id": replay_id,
1323 "status": "cancelled"
1324 })))
1325}
1326
1327pub async fn delete_replay(
1329 State(store): State<SharedStore>,
1330 Path(replay_id): Path<uuid::Uuid>,
1331) -> Result<Json<serde_json::Value>> {
1332 let replay_manager = store.replay_manager();
1333
1334 let deleted = replay_manager.delete_replay(replay_id)?;
1335
1336 if deleted {
1337 tracing::info!("🗑️ Deleted replay {}", replay_id);
1338 }
1339
1340 Ok(Json(serde_json::json!({
1341 "replay_id": replay_id,
1342 "deleted": deleted
1343 })))
1344}
1345
1346pub async fn register_pipeline(
1348 State(store): State<SharedStore>,
1349 Json(config): Json<PipelineConfig>,
1350) -> Result<Json<serde_json::Value>> {
1351 let pipeline_manager = store.pipeline_manager();
1352
1353 let pipeline_id = pipeline_manager.register(config.clone());
1354
1355 tracing::info!(
1356 "🔀 Pipeline registered: {} (name: {})",
1357 pipeline_id,
1358 config.name
1359 );
1360
1361 Ok(Json(serde_json::json!({
1362 "pipeline_id": pipeline_id,
1363 "name": config.name,
1364 "enabled": config.enabled
1365 })))
1366}
1367
1368pub async fn list_pipelines(State(store): State<SharedStore>) -> Json<serde_json::Value> {
1370 let pipeline_manager = store.pipeline_manager();
1371
1372 let pipelines = pipeline_manager.list();
1373
1374 tracing::debug!("Listed {} pipelines", pipelines.len());
1375
1376 Json(serde_json::json!({
1377 "pipelines": pipelines,
1378 "total": pipelines.len()
1379 }))
1380}
1381
1382pub async fn get_pipeline(
1384 State(store): State<SharedStore>,
1385 Path(pipeline_id): Path<uuid::Uuid>,
1386) -> Result<Json<PipelineConfig>> {
1387 let pipeline_manager = store.pipeline_manager();
1388
1389 let pipeline = pipeline_manager.get(pipeline_id).ok_or_else(|| {
1390 crate::error::AllSourceError::ValidationError(format!("Pipeline not found: {pipeline_id}"))
1391 })?;
1392
1393 Ok(Json(pipeline.config().clone()))
1394}
1395
1396pub async fn remove_pipeline(
1398 State(store): State<SharedStore>,
1399 Path(pipeline_id): Path<uuid::Uuid>,
1400) -> Result<Json<serde_json::Value>> {
1401 let pipeline_manager = store.pipeline_manager();
1402
1403 let removed = pipeline_manager.remove(pipeline_id);
1404
1405 if removed {
1406 tracing::info!("🗑️ Removed pipeline {}", pipeline_id);
1407 }
1408
1409 Ok(Json(serde_json::json!({
1410 "pipeline_id": pipeline_id,
1411 "removed": removed
1412 })))
1413}
1414
1415pub async fn all_pipeline_stats(State(store): State<SharedStore>) -> Json<serde_json::Value> {
1417 let pipeline_manager = store.pipeline_manager();
1418
1419 let stats = pipeline_manager.all_stats();
1420
1421 Json(serde_json::json!({
1422 "stats": stats,
1423 "total": stats.len()
1424 }))
1425}
1426
1427pub async fn get_pipeline_stats(
1429 State(store): State<SharedStore>,
1430 Path(pipeline_id): Path<uuid::Uuid>,
1431) -> Result<Json<PipelineStats>> {
1432 let pipeline_manager = store.pipeline_manager();
1433
1434 let pipeline = pipeline_manager.get(pipeline_id).ok_or_else(|| {
1435 crate::error::AllSourceError::ValidationError(format!("Pipeline not found: {pipeline_id}"))
1436 })?;
1437
1438 Ok(Json(pipeline.stats()))
1439}
1440
1441pub async fn reset_pipeline(
1443 State(store): State<SharedStore>,
1444 Path(pipeline_id): Path<uuid::Uuid>,
1445) -> Result<Json<serde_json::Value>> {
1446 let pipeline_manager = store.pipeline_manager();
1447
1448 let pipeline = pipeline_manager.get(pipeline_id).ok_or_else(|| {
1449 crate::error::AllSourceError::ValidationError(format!("Pipeline not found: {pipeline_id}"))
1450 })?;
1451
1452 pipeline.reset();
1453
1454 tracing::info!("🔄 Reset pipeline {}", pipeline_id);
1455
1456 Ok(Json(serde_json::json!({
1457 "pipeline_id": pipeline_id,
1458 "reset": true
1459 })))
1460}
1461
1462pub async fn get_event_by_id(
1468 State(store): State<SharedStore>,
1469 Path(event_id): Path<uuid::Uuid>,
1470) -> Result<Json<serde_json::Value>> {
1471 let event = store.get_event_by_id(&event_id)?.ok_or_else(|| {
1472 crate::error::AllSourceError::EntityNotFound(format!("Event '{event_id}' not found"))
1473 })?;
1474
1475 let dto = EventDto::from(&event);
1476
1477 tracing::debug!("Event retrieved by ID: {}", event_id);
1478
1479 Ok(Json(serde_json::json!({
1480 "event": dto,
1481 "found": true
1482 })))
1483}
1484
1485pub async fn list_projections(State(store): State<SharedStore>) -> Json<serde_json::Value> {
1491 let projection_manager = store.projection_manager();
1492 let status_map = store.projection_status();
1493
1494 let projections: Vec<serde_json::Value> = projection_manager
1495 .list_projections()
1496 .iter()
1497 .map(|(name, projection)| {
1498 let status = status_map
1499 .get(name)
1500 .map_or_else(|| "running".to_string(), |s| s.value().clone());
1501 serde_json::json!({
1502 "name": name,
1503 "type": format!("{:?}", projection.name()),
1504 "status": status,
1505 })
1506 })
1507 .collect();
1508
1509 tracing::debug!("Listed {} projections", projections.len());
1510
1511 Json(serde_json::json!({
1512 "projections": projections,
1513 "total": projections.len()
1514 }))
1515}
1516
1517pub async fn get_projection(
1519 State(store): State<SharedStore>,
1520 Path(name): Path<String>,
1521) -> Result<Json<serde_json::Value>> {
1522 let projection_manager = store.projection_manager();
1523
1524 let projection = projection_manager.get_projection(&name).ok_or_else(|| {
1525 crate::error::AllSourceError::EntityNotFound(format!("Projection '{name}' not found"))
1526 })?;
1527
1528 Ok(Json(serde_json::json!({
1529 "name": projection.name(),
1530 "found": true
1531 })))
1532}
1533
1534pub async fn get_projection_state(
1547 State(store): State<SharedStore>,
1548 Path((name, entity_id)): Path<(String, String)>,
1549) -> Result<Json<serde_json::Value>> {
1550 let state = store
1551 .projection_manager()
1552 .get_projection(&name)
1553 .and_then(|p| p.get_state(&entity_id))
1554 .or_else(|| {
1555 store
1556 .projection_state_cache()
1557 .get(&format!("{name}:{entity_id}"))
1558 .map(|entry| entry.value().clone())
1559 });
1560
1561 tracing::debug!("Projection state retrieved: {} / {}", name, entity_id);
1562
1563 Ok(Json(serde_json::json!({
1564 "projection": name,
1565 "entity_id": entity_id,
1566 "state": state,
1567 "found": state.is_some()
1568 })))
1569}
1570
1571pub async fn delete_projection(
1576 State(store): State<SharedStore>,
1577 Path(name): Path<String>,
1578) -> Result<Json<serde_json::Value>> {
1579 let projection_manager = store.projection_manager();
1580
1581 let projection = projection_manager.get_projection(&name).ok_or_else(|| {
1582 crate::error::AllSourceError::EntityNotFound(format!("Projection '{name}' not found"))
1583 })?;
1584
1585 projection.clear();
1586
1587 let cache = store.projection_state_cache();
1589 let prefix = format!("{name}:");
1590 let keys_to_remove: Vec<String> = cache
1591 .iter()
1592 .filter(|entry| entry.key().starts_with(&prefix))
1593 .map(|entry| entry.key().clone())
1594 .collect();
1595 for key in keys_to_remove {
1596 cache.remove(&key);
1597 }
1598
1599 tracing::info!("Projection deleted (cleared): {}", name);
1600
1601 Ok(Json(serde_json::json!({
1602 "projection": name,
1603 "deleted": true
1604 })))
1605}
1606
1607pub async fn get_projection_state_summary(
1616 State(store): State<SharedStore>,
1617 Path(name): Path<String>,
1618) -> Result<Json<serde_json::Value>> {
1619 let cache = store.projection_state_cache();
1620 let prefix = format!("{name}:");
1621 let states: Vec<serde_json::Value> = cache
1622 .iter()
1623 .filter(|entry| entry.key().starts_with(&prefix))
1624 .map(|entry| {
1625 let entity_id = entry.key().strip_prefix(&prefix).unwrap_or(entry.key());
1626 serde_json::json!({
1627 "entity_id": entity_id,
1628 "state": entry.value().clone()
1629 })
1630 })
1631 .collect();
1632
1633 let total = states.len();
1634
1635 tracing::debug!("Projection state summary: {} ({} entities)", name, total);
1636
1637 Ok(Json(serde_json::json!({
1638 "projection": name,
1639 "states": states,
1640 "total": total
1641 })))
1642}
1643
1644pub async fn reset_projection(
1648 State(store): State<SharedStore>,
1649 Path(name): Path<String>,
1650) -> Result<Json<serde_json::Value>> {
1651 let reprocessed = store.reset_projection(&name)?;
1652
1653 tracing::info!(
1654 "Projection reset: {} ({} events reprocessed)",
1655 name,
1656 reprocessed
1657 );
1658
1659 Ok(Json(serde_json::json!({
1660 "projection": name,
1661 "reset": true,
1662 "events_reprocessed": reprocessed
1663 })))
1664}
1665
1666pub async fn pause_projection(
1670 State(store): State<SharedStore>,
1671 Path(name): Path<String>,
1672) -> Result<Json<serde_json::Value>> {
1673 let projection_manager = store.projection_manager();
1674
1675 let _projection = projection_manager.get_projection(&name).ok_or_else(|| {
1677 crate::error::AllSourceError::EntityNotFound(format!("Projection '{name}' not found"))
1678 })?;
1679
1680 store
1681 .projection_status()
1682 .insert(name.clone(), "paused".to_string());
1683
1684 tracing::info!("Projection paused: {}", name);
1685
1686 Ok(Json(serde_json::json!({
1687 "projection": name,
1688 "status": "paused"
1689 })))
1690}
1691
1692pub async fn start_projection(
1696 State(store): State<SharedStore>,
1697 Path(name): Path<String>,
1698) -> Result<Json<serde_json::Value>> {
1699 let projection_manager = store.projection_manager();
1700
1701 let _projection = projection_manager.get_projection(&name).ok_or_else(|| {
1703 crate::error::AllSourceError::EntityNotFound(format!("Projection '{name}' not found"))
1704 })?;
1705
1706 store
1707 .projection_status()
1708 .insert(name.clone(), "running".to_string());
1709
1710 tracing::info!("Projection started: {}", name);
1711
1712 Ok(Json(serde_json::json!({
1713 "projection": name,
1714 "status": "running"
1715 })))
1716}
1717
1718#[derive(Debug, Deserialize)]
1720pub struct SaveProjectionStateRequest {
1721 pub state: serde_json::Value,
1722}
1723
1724pub async fn save_projection_state(
1729 State(store): State<SharedStore>,
1730 Path((name, entity_id)): Path<(String, String)>,
1731 Json(req): Json<SaveProjectionStateRequest>,
1732) -> Result<Json<serde_json::Value>> {
1733 let projection_cache = store.projection_state_cache();
1734
1735 projection_cache.insert(format!("{name}:{entity_id}"), req.state.clone());
1737
1738 tracing::info!("Projection state saved: {} / {}", name, entity_id);
1739
1740 Ok(Json(serde_json::json!({
1741 "projection": name,
1742 "entity_id": entity_id,
1743 "saved": true
1744 })))
1745}
1746
1747#[derive(Debug, Deserialize)]
1751pub struct BulkGetStateRequest {
1752 pub entity_ids: Vec<String>,
1753}
1754
1755#[derive(Debug, Deserialize)]
1759pub struct BulkSaveStateRequest {
1760 pub states: Vec<BulkSaveStateItem>,
1761}
1762
1763#[derive(Debug, Deserialize)]
1764pub struct BulkSaveStateItem {
1765 pub entity_id: String,
1766 pub state: serde_json::Value,
1767}
1768
1769pub async fn bulk_get_projection_states(
1770 State(store): State<SharedStore>,
1771 Path(name): Path<String>,
1772 Json(req): Json<BulkGetStateRequest>,
1773) -> Result<Json<serde_json::Value>> {
1774 let projection = store.projection_manager().get_projection(&name);
1778 let cache = store.projection_state_cache();
1779
1780 let states: Vec<serde_json::Value> = req
1781 .entity_ids
1782 .iter()
1783 .map(|entity_id| {
1784 let state = projection
1785 .as_ref()
1786 .and_then(|p| p.get_state(entity_id))
1787 .or_else(|| {
1788 cache
1789 .get(&format!("{name}:{entity_id}"))
1790 .map(|entry| entry.value().clone())
1791 });
1792 serde_json::json!({
1793 "entity_id": entity_id,
1794 "state": state,
1795 "found": state.is_some()
1796 })
1797 })
1798 .collect();
1799
1800 tracing::debug!(
1801 "Bulk projection state retrieved: {} entities from {}",
1802 states.len(),
1803 name
1804 );
1805
1806 Ok(Json(serde_json::json!({
1807 "projection": name,
1808 "states": states,
1809 "total": states.len()
1810 })))
1811}
1812
1813pub async fn bulk_save_projection_states(
1818 State(store): State<SharedStore>,
1819 Path(name): Path<String>,
1820 Json(req): Json<BulkSaveStateRequest>,
1821) -> Result<Json<serde_json::Value>> {
1822 let projection_cache = store.projection_state_cache();
1823
1824 let mut saved_count = 0;
1825 for item in &req.states {
1826 projection_cache.insert(format!("{name}:{}", item.entity_id), item.state.clone());
1827 saved_count += 1;
1828 }
1829
1830 tracing::info!(
1831 "Bulk projection state saved: {} entities for {}",
1832 saved_count,
1833 name
1834 );
1835
1836 Ok(Json(serde_json::json!({
1837 "projection": name,
1838 "saved": saved_count,
1839 "total": req.states.len()
1840 })))
1841}
1842
1843#[derive(Debug, Deserialize)]
1849pub struct ListWebhooksParams {
1850 pub tenant_id: Option<String>,
1851}
1852
1853pub async fn register_webhook(
1855 State(store): State<SharedStore>,
1856 Json(req): Json<RegisterWebhookRequest>,
1857) -> Json<serde_json::Value> {
1858 let registry = store.webhook_registry();
1859 let webhook = registry.register(req);
1860
1861 tracing::info!("Webhook registered: {} -> {}", webhook.id, webhook.url);
1862
1863 Json(serde_json::json!({
1864 "webhook": webhook,
1865 "created": true
1866 }))
1867}
1868
1869pub async fn list_webhooks(
1871 State(store): State<SharedStore>,
1872 Query(params): Query<ListWebhooksParams>,
1873) -> Json<serde_json::Value> {
1874 let registry = store.webhook_registry();
1875
1876 let webhooks = if let Some(tenant_id) = params.tenant_id {
1877 registry.list_by_tenant(&tenant_id)
1878 } else {
1879 vec![]
1881 };
1882
1883 let total = webhooks.len();
1884
1885 Json(serde_json::json!({
1886 "webhooks": webhooks,
1887 "total": total
1888 }))
1889}
1890
1891pub async fn get_webhook(
1893 State(store): State<SharedStore>,
1894 Path(webhook_id): Path<uuid::Uuid>,
1895) -> Result<Json<serde_json::Value>> {
1896 let registry = store.webhook_registry();
1897
1898 let webhook = registry.get(webhook_id).ok_or_else(|| {
1899 crate::error::AllSourceError::EntityNotFound(format!("Webhook '{webhook_id}' not found"))
1900 })?;
1901
1902 Ok(Json(serde_json::json!({
1903 "webhook": webhook,
1904 "found": true
1905 })))
1906}
1907
1908pub async fn update_webhook(
1910 State(store): State<SharedStore>,
1911 Path(webhook_id): Path<uuid::Uuid>,
1912 Json(req): Json<UpdateWebhookRequest>,
1913) -> Result<Json<serde_json::Value>> {
1914 let registry = store.webhook_registry();
1915
1916 let webhook = registry.update(webhook_id, req).ok_or_else(|| {
1917 crate::error::AllSourceError::EntityNotFound(format!("Webhook '{webhook_id}' not found"))
1918 })?;
1919
1920 tracing::info!("Webhook updated: {}", webhook_id);
1921
1922 Ok(Json(serde_json::json!({
1923 "webhook": webhook,
1924 "updated": true
1925 })))
1926}
1927
1928pub async fn delete_webhook(
1930 State(store): State<SharedStore>,
1931 Path(webhook_id): Path<uuid::Uuid>,
1932) -> Result<Json<serde_json::Value>> {
1933 let registry = store.webhook_registry();
1934
1935 let webhook = registry.delete(webhook_id).ok_or_else(|| {
1936 crate::error::AllSourceError::EntityNotFound(format!("Webhook '{webhook_id}' not found"))
1937 })?;
1938
1939 tracing::info!("Webhook deleted: {} ({})", webhook_id, webhook.url);
1940
1941 Ok(Json(serde_json::json!({
1942 "webhook_id": webhook_id,
1943 "deleted": true
1944 })))
1945}
1946
1947#[derive(Debug, Deserialize)]
1949pub struct ListDeliveriesParams {
1950 pub limit: Option<usize>,
1951}
1952
1953pub async fn list_webhook_deliveries(
1955 State(store): State<SharedStore>,
1956 Path(webhook_id): Path<uuid::Uuid>,
1957 Query(params): Query<ListDeliveriesParams>,
1958) -> Result<Json<serde_json::Value>> {
1959 let registry = store.webhook_registry();
1960
1961 registry.get(webhook_id).ok_or_else(|| {
1963 crate::error::AllSourceError::EntityNotFound(format!("Webhook '{webhook_id}' not found"))
1964 })?;
1965
1966 let limit = params.limit.unwrap_or(50);
1967 let deliveries = registry.get_deliveries(webhook_id, limit);
1968 let total = deliveries.len();
1969
1970 Ok(Json(serde_json::json!({
1971 "webhook_id": webhook_id,
1972 "deliveries": deliveries,
1973 "total": total
1974 })))
1975}
1976
1977#[cfg(feature = "analytics")]
1983pub async fn eventql_query(
1984 State(store): State<SharedStore>,
1985 Json(req): Json<crate::infrastructure::query::eventql::EventQLRequest>,
1986) -> Result<Json<serde_json::Value>> {
1987 let events = store.snapshot_events();
1988 match crate::infrastructure::query::eventql::execute_eventql(&events, &req).await {
1989 Ok(response) => Ok(Json(serde_json::json!({
1990 "columns": response.columns,
1991 "rows": response.rows,
1992 "row_count": response.row_count,
1993 }))),
1994 Err(e) => Err(crate::error::AllSourceError::InvalidQuery(e)),
1995 }
1996}
1997
1998pub async fn graphql_query(
2000 State(store): State<SharedStore>,
2001 Json(req): Json<GraphQLRequest>,
2002) -> Json<serde_json::Value> {
2003 let fields = match crate::infrastructure::query::graphql::parse_query(&req.query) {
2004 Ok(f) => f,
2005 Err(e) => {
2006 return Json(
2007 serde_json::to_value(GraphQLResponse {
2008 data: None,
2009 errors: vec![GraphQLError { message: e }],
2010 })
2011 .unwrap(),
2012 );
2013 }
2014 };
2015
2016 let mut data = serde_json::Map::new();
2017 let mut errors = Vec::new();
2018
2019 for field in &fields {
2020 match field.name.as_str() {
2021 "events" => {
2022 let request = crate::application::dto::QueryEventsRequest {
2023 entity_id: field.arguments.get("entity_id").cloned(),
2024 event_type: field.arguments.get("event_type").cloned(),
2025 tenant_id: field.arguments.get("tenant_id").cloned(),
2026 limit: field.arguments.get("limit").and_then(|l| l.parse().ok()),
2027 as_of: None,
2028 since: None,
2029 until: None,
2030 event_type_prefix: None,
2031 payload_filter: None,
2032 };
2033 match store.query(&request) {
2034 Ok(events) => {
2035 let json_events: Vec<serde_json::Value> = events
2036 .iter()
2037 .map(|e| {
2038 crate::infrastructure::query::graphql::event_to_json(
2039 e,
2040 &field.fields,
2041 )
2042 })
2043 .collect();
2044 data.insert("events".to_string(), serde_json::Value::Array(json_events));
2045 }
2046 Err(e) => errors.push(GraphQLError {
2047 message: format!("events query failed: {e}"),
2048 }),
2049 }
2050 }
2051 "event" => {
2052 if let Some(id_str) = field.arguments.get("id") {
2053 if let Ok(id) = uuid::Uuid::parse_str(id_str) {
2054 match store.get_event_by_id(&id) {
2055 Ok(Some(event)) => {
2056 data.insert(
2057 "event".to_string(),
2058 crate::infrastructure::query::graphql::event_to_json(
2059 &event,
2060 &field.fields,
2061 ),
2062 );
2063 }
2064 Ok(None) => {
2065 data.insert("event".to_string(), serde_json::Value::Null);
2066 }
2067 Err(e) => errors.push(GraphQLError {
2068 message: format!("event lookup failed: {e}"),
2069 }),
2070 }
2071 } else {
2072 errors.push(GraphQLError {
2073 message: format!("Invalid UUID: {id_str}"),
2074 });
2075 }
2076 } else {
2077 errors.push(GraphQLError {
2078 message: "event query requires 'id' argument".to_string(),
2079 });
2080 }
2081 }
2082 "projections" => {
2083 let pm = store.projection_manager();
2084 let names: Vec<serde_json::Value> = pm
2085 .list_projections()
2086 .iter()
2087 .map(|(name, _)| serde_json::Value::String(name.clone()))
2088 .collect();
2089 data.insert("projections".to_string(), serde_json::Value::Array(names));
2090 }
2091 "stats" => {
2092 let stats = store.stats();
2093 data.insert(
2094 "stats".to_string(),
2095 serde_json::json!({
2096 "total_events": stats.total_events,
2097 "total_entities": stats.total_entities,
2098 "total_event_types": stats.total_event_types,
2099 }),
2100 );
2101 }
2102 "__schema" => {
2103 data.insert(
2104 "__schema".to_string(),
2105 crate::infrastructure::query::graphql::introspection_schema(),
2106 );
2107 }
2108 other => {
2109 errors.push(GraphQLError {
2110 message: format!("Unknown field: {other}"),
2111 });
2112 }
2113 }
2114 }
2115
2116 Json(
2117 serde_json::to_value(GraphQLResponse {
2118 data: Some(serde_json::Value::Object(data)),
2119 errors,
2120 })
2121 .unwrap(),
2122 )
2123}
2124
2125pub async fn geo_query(
2127 State(store): State<SharedStore>,
2128 Json(req): Json<GeoQueryRequest>,
2129) -> Json<serde_json::Value> {
2130 let events = store.snapshot_events();
2131 let geo_index = store.geo_index();
2132 let results =
2133 crate::infrastructure::query::geospatial::execute_geo_query(&events, &geo_index, &req);
2134 let total = results.len();
2135 Json(serde_json::json!({
2136 "results": results,
2137 "total": total,
2138 }))
2139}
2140
2141pub async fn geo_stats(State(store): State<SharedStore>) -> Json<serde_json::Value> {
2143 let stats = store.geo_index().stats();
2144 Json(serde_json::json!(stats))
2145}
2146
2147pub async fn exactly_once_stats(State(store): State<SharedStore>) -> Json<serde_json::Value> {
2149 let stats = store.exactly_once().stats();
2150 Json(serde_json::json!(stats))
2151}
2152
2153pub async fn schema_evolution_history(
2155 State(store): State<SharedStore>,
2156 Path(event_type): Path<String>,
2157) -> Json<serde_json::Value> {
2158 let mgr = store.schema_evolution();
2159 let history = mgr.get_history(&event_type);
2160 let version = mgr.get_version(&event_type);
2161 Json(serde_json::json!({
2162 "event_type": event_type,
2163 "current_version": version,
2164 "history": history,
2165 }))
2166}
2167
2168pub async fn schema_evolution_schema(
2170 State(store): State<SharedStore>,
2171 Path(event_type): Path<String>,
2172) -> Json<serde_json::Value> {
2173 let mgr = store.schema_evolution();
2174 if let Some(schema) = mgr.get_schema(&event_type) {
2175 let json_schema = crate::application::services::schema_evolution::to_json_schema(&schema);
2176 Json(serde_json::json!({
2177 "event_type": event_type,
2178 "version": mgr.get_version(&event_type),
2179 "inferred_schema": schema,
2180 "json_schema": json_schema,
2181 }))
2182 } else {
2183 Json(serde_json::json!({
2184 "event_type": event_type,
2185 "error": "No schema inferred for this event type"
2186 }))
2187 }
2188}
2189
2190pub async fn schema_evolution_stats(State(store): State<SharedStore>) -> Json<serde_json::Value> {
2192 let stats = store.schema_evolution().stats();
2193 let event_types = store.schema_evolution().list_event_types();
2194 Json(serde_json::json!({
2195 "stats": stats,
2196 "tracked_event_types": event_types,
2197 }))
2198}
2199
2200#[cfg(feature = "embedded-sync")]
2206pub async fn sync_pull_handler(
2207 State(state): State<AppState>,
2208 Json(request): Json<crate::embedded::sync_types::SyncPullRequest>,
2209) -> Result<Json<crate::embedded::sync_types::SyncPullResponse>> {
2210 use crate::infrastructure::cluster::{crdt::ReplicatedEvent, hlc::HlcTimestamp};
2211
2212 let store = &state.store;
2213
2214 let since = request
2217 .version_vector
2218 .values()
2219 .map(|ts| ts.physical_ms)
2220 .min()
2221 .and_then(|ms| chrono::DateTime::from_timestamp_millis(ms as i64));
2222
2223 let events = store.query(&crate::application::dto::QueryEventsRequest {
2224 entity_id: None,
2225 event_type: None,
2226 tenant_id: None,
2227 as_of: None,
2228 since,
2229 until: None,
2230 limit: None,
2231 event_type_prefix: None,
2232 payload_filter: None,
2233 })?;
2234
2235 let mut replicated = Vec::with_capacity(events.len());
2237 let mut last_ms = 0u64;
2238 let mut logical = 0u32;
2239
2240 for event in &events {
2241 let event_ms = event.timestamp().timestamp_millis() as u64;
2242 if event_ms == last_ms {
2243 logical += 1;
2244 } else {
2245 last_ms = event_ms;
2246 logical = 0;
2247 }
2248
2249 replicated.push(ReplicatedEvent {
2250 event_id: event.id().to_string(),
2251 hlc_timestamp: HlcTimestamp::new(event_ms, logical, 0),
2252 origin_region: "server".to_string(),
2253 event_data: serde_json::json!({
2254 "event_type": event.event_type_str(),
2255 "entity_id": event.entity_id_str(),
2256 "tenant_id": event.tenant_id_str(),
2257 "payload": event.payload,
2258 "metadata": event.metadata,
2259 }),
2260 });
2261 }
2262
2263 Ok(Json(crate::embedded::sync_types::SyncPullResponse {
2264 events: replicated,
2265 version_vector: std::collections::BTreeMap::new(),
2266 }))
2267}
2268
2269#[cfg(feature = "embedded-sync")]
2271pub async fn sync_push_handler(
2272 State(state): State<AppState>,
2273 Json(request): Json<crate::embedded::sync_types::SyncPushRequest>,
2274) -> Result<Json<crate::embedded::sync_types::SyncPushResponse>> {
2275 let store = &state.store;
2276
2277 let mut accepted = 0usize;
2278 let mut skipped = 0usize;
2279
2280 for rep_event in &request.events {
2281 let event_data = &rep_event.event_data;
2282 let event_type = event_data
2283 .get("event_type")
2284 .and_then(|v| v.as_str())
2285 .unwrap_or("unknown")
2286 .to_string();
2287 let entity_id = event_data
2288 .get("entity_id")
2289 .and_then(|v| v.as_str())
2290 .unwrap_or("unknown")
2291 .to_string();
2292 let tenant_id = event_data
2293 .get("tenant_id")
2294 .and_then(|v| v.as_str())
2295 .unwrap_or("default")
2296 .to_string();
2297 let payload = event_data
2298 .get("payload")
2299 .cloned()
2300 .unwrap_or(serde_json::json!({}));
2301 let metadata = event_data.get("metadata").cloned();
2302
2303 match Event::from_strings(event_type, entity_id, tenant_id, payload, metadata) {
2304 Ok(domain_event) => {
2305 store.ingest(&domain_event)?;
2306 accepted += 1;
2307 }
2308 Err(_) => {
2309 skipped += 1;
2310 }
2311 }
2312 }
2313
2314 Ok(Json(crate::embedded::sync_types::SyncPushResponse {
2315 accepted,
2316 skipped,
2317 version_vector: std::collections::BTreeMap::new(),
2318 }))
2319}
2320
2321pub async fn register_consumer(
2327 State(store): State<SharedStore>,
2328 Json(req): Json<RegisterConsumerRequest>,
2329) -> Result<Json<ConsumerResponse>> {
2330 let consumer = store
2331 .consumer_registry()
2332 .register(&req.consumer_id, &req.event_type_filters);
2333
2334 Ok(Json(ConsumerResponse {
2335 consumer_id: consumer.consumer_id,
2336 event_type_filters: consumer.event_type_filters,
2337 cursor_position: consumer.cursor_position,
2338 }))
2339}
2340
2341pub async fn get_consumer(
2343 State(store): State<SharedStore>,
2344 Path(consumer_id): Path<String>,
2345) -> Result<Json<ConsumerResponse>> {
2346 let consumer = store.consumer_registry().get_or_create(&consumer_id);
2347
2348 Ok(Json(ConsumerResponse {
2349 consumer_id: consumer.consumer_id,
2350 event_type_filters: consumer.event_type_filters,
2351 cursor_position: consumer.cursor_position,
2352 }))
2353}
2354
2355#[derive(Debug, Deserialize)]
2357pub struct ConsumerPollQuery {
2358 pub limit: Option<usize>,
2359}
2360
2361pub async fn poll_consumer_events(
2362 State(store): State<SharedStore>,
2363 Path(consumer_id): Path<String>,
2364 Query(query): Query<ConsumerPollQuery>,
2365) -> Result<Json<ConsumerEventsResponse>> {
2366 let consumer = store.consumer_registry().get_or_create(&consumer_id);
2367 let offset = consumer.cursor_position.unwrap_or(0);
2368 let limit = query.limit.unwrap_or(100);
2369
2370 let events = store.events_after_offset(offset, &consumer.event_type_filters, limit);
2371 let count = events.len();
2372
2373 let consumer_events: Vec<ConsumerEventDto> = events
2374 .into_iter()
2375 .map(|(position, event)| ConsumerEventDto {
2376 position,
2377 event: EventDto::from(&event),
2378 })
2379 .collect();
2380
2381 Ok(Json(ConsumerEventsResponse {
2382 events: consumer_events,
2383 count,
2384 }))
2385}
2386
2387pub async fn ack_consumer(
2389 State(store): State<SharedStore>,
2390 Path(consumer_id): Path<String>,
2391 Json(req): Json<AckRequest>,
2392) -> Result<Json<serde_json::Value>> {
2393 let max_offset = store.total_events() as u64;
2394
2395 store
2396 .consumer_registry()
2397 .ack(&consumer_id, req.position, max_offset)
2398 .map_err(crate::error::AllSourceError::InvalidInput)?;
2399
2400 Ok(Json(serde_json::json!({
2401 "status": "ok",
2402 "consumer_id": consumer_id,
2403 "position": req.position,
2404 })))
2405}
2406
2407#[cfg(test)]
2408mod tests {
2409 use super::*;
2410 use crate::{domain::entities::Event, store::EventStore};
2411
2412 fn create_test_store() -> Arc<EventStore> {
2413 Arc::new(EventStore::new())
2414 }
2415
2416 fn create_test_event(entity_id: &str, event_type: &str) -> Event {
2417 Event::from_strings(
2418 event_type.to_string(),
2419 entity_id.to_string(),
2420 "test-stream".to_string(),
2421 serde_json::json!({
2422 "name": "Test",
2423 "value": 42
2424 }),
2425 None,
2426 )
2427 .unwrap()
2428 }
2429
2430 #[tokio::test]
2431 async fn test_query_events_has_more_and_total_count() {
2432 let store = create_test_store();
2433
2434 for i in 0..50 {
2436 store
2437 .ingest(&create_test_event(&format!("entity-{i}"), "user.created"))
2438 .unwrap();
2439 }
2440
2441 let req = QueryEventsRequest {
2443 entity_id: None,
2444 event_type: None,
2445 tenant_id: None,
2446 as_of: None,
2447 since: None,
2448 until: None,
2449 limit: Some(10),
2450 event_type_prefix: None,
2451 payload_filter: None,
2452 };
2453
2454 let requested_limit = req.limit;
2455 let unlimited_req = QueryEventsRequest {
2456 limit: None,
2457 ..QueryEventsRequest {
2458 entity_id: req.entity_id,
2459 event_type: req.event_type,
2460 tenant_id: req.tenant_id,
2461 as_of: req.as_of,
2462 since: req.since,
2463 until: req.until,
2464 limit: None,
2465 event_type_prefix: req.event_type_prefix,
2466 payload_filter: req.payload_filter,
2467 }
2468 };
2469 let all_events = store.query(&unlimited_req).unwrap();
2470 let total_count = all_events.len();
2471 let limited_events: Vec<Event> = if let Some(limit) = requested_limit {
2472 all_events.into_iter().take(limit).collect()
2473 } else {
2474 all_events
2475 };
2476 let count = limited_events.len();
2477 let has_more = count < total_count;
2478
2479 assert_eq!(count, 10);
2480 assert_eq!(total_count, 50);
2481 assert!(has_more);
2482 }
2483
2484 #[tokio::test]
2485 async fn test_query_events_no_more_results() {
2486 let store = create_test_store();
2487
2488 for i in 0..5 {
2490 store
2491 .ingest(&create_test_event(&format!("entity-{i}"), "user.created"))
2492 .unwrap();
2493 }
2494
2495 let all_events = store
2497 .query(&QueryEventsRequest {
2498 entity_id: None,
2499 event_type: None,
2500 tenant_id: None,
2501 as_of: None,
2502 since: None,
2503 until: None,
2504 limit: None,
2505 event_type_prefix: None,
2506 payload_filter: None,
2507 })
2508 .unwrap();
2509 let total_count = all_events.len();
2510 let limited_events: Vec<Event> = all_events.into_iter().take(100).collect();
2511 let count = limited_events.len();
2512 let has_more = count < total_count;
2513
2514 assert_eq!(count, 5);
2515 assert_eq!(total_count, 5);
2516 assert!(!has_more);
2517 }
2518
2519 #[tokio::test]
2523 async fn test_query_events_order_desc_returns_latest() {
2524 let store = create_test_store();
2525
2526 let base = chrono::Utc::now();
2529 for i in 0..3i64 {
2530 let mut event = create_test_event("org-1", "auth.org.updated");
2531 event.timestamp = base + chrono::Duration::seconds(i);
2532 event.version = i + 1;
2533 store.ingest(&event).unwrap();
2534 }
2535
2536 let req = QueryEventsRequest {
2537 entity_id: Some("org-1".to_string()),
2538 ..Default::default()
2539 };
2540 let ascending = store.query(&req).unwrap();
2541 assert_eq!(ascending.len(), 3);
2542 assert!(ascending[0].timestamp < ascending[1].timestamp);
2544 assert!(ascending[1].timestamp < ascending[2].timestamp);
2545 let oldest_ts = ascending[0].timestamp;
2546 let newest_ts = ascending[2].timestamp;
2547
2548 let mut descending = ascending.clone();
2550 descending.reverse();
2551 let latest: Vec<Event> = descending.into_iter().take(1).collect();
2552 assert_eq!(latest.len(), 1);
2553 assert_eq!(
2554 latest[0].timestamp, newest_ts,
2555 "order=desc&limit=1 must yield the newest event"
2556 );
2557
2558 let oldest: Vec<Event> = ascending.into_iter().take(1).collect();
2560 assert_eq!(oldest[0].timestamp, oldest_ts);
2561 }
2562
2563 #[tokio::test]
2564 async fn test_list_entities_by_type_prefix() {
2565 let store = create_test_store();
2566
2567 store
2569 .ingest(&create_test_event("idx-1", "index.created"))
2570 .unwrap();
2571 store
2572 .ingest(&create_test_event("idx-1", "index.updated"))
2573 .unwrap();
2574 store
2575 .ingest(&create_test_event("idx-2", "index.created"))
2576 .unwrap();
2577 store
2578 .ingest(&create_test_event("idx-3", "index.created"))
2579 .unwrap();
2580 store
2582 .ingest(&create_test_event("trade-1", "trade.created"))
2583 .unwrap();
2584 store
2585 .ingest(&create_test_event("trade-2", "trade.created"))
2586 .unwrap();
2587
2588 let req = ListEntitiesRequest {
2590 event_type_prefix: Some("index.".to_string()),
2591 ..Default::default()
2592 };
2593 let query_req = QueryEventsRequest {
2594 entity_id: None,
2595 event_type: None,
2596 tenant_id: None,
2597 as_of: None,
2598 since: None,
2599 until: None,
2600 limit: None,
2601 event_type_prefix: req.event_type_prefix,
2602 payload_filter: req.payload_filter,
2603 };
2604 let events = store.query(&query_req).unwrap();
2605
2606 let mut entity_map: std::collections::HashMap<String, Vec<&Event>> =
2608 std::collections::HashMap::new();
2609 for event in &events {
2610 entity_map
2611 .entry(event.entity_id().to_string())
2612 .or_default()
2613 .push(event);
2614 }
2615
2616 assert_eq!(entity_map.len(), 3); assert_eq!(entity_map["idx-1"].len(), 2); assert_eq!(entity_map["idx-2"].len(), 1);
2619 assert_eq!(entity_map["idx-3"].len(), 1);
2620 }
2621
2622 #[tokio::test]
2625 async fn test_list_entities_order_and_pagination() {
2626 let store = create_test_store();
2627
2628 let base = chrono::Utc::now();
2630 for (i, eid) in ["org-a", "org-b", "org-c"].iter().enumerate() {
2631 let mut event = create_test_event(eid, "auth.org.created");
2632 event.timestamp = base + chrono::Duration::seconds(i as i64);
2633 store.ingest(&event).unwrap();
2634 }
2635 let prefix = || Some("auth.org.".to_string());
2636
2637 let desc = list_entities(
2639 State(store.clone()),
2640 Query(ListEntitiesRequest {
2641 event_type_prefix: prefix(),
2642 ..Default::default()
2643 }),
2644 )
2645 .await
2646 .unwrap();
2647 let desc_ids: Vec<&str> = desc
2648 .0
2649 .entities
2650 .iter()
2651 .map(|e| e.entity_id.as_str())
2652 .collect();
2653 assert_eq!(desc_ids, ["org-c", "org-b", "org-a"]);
2654
2655 let asc = list_entities(
2657 State(store.clone()),
2658 Query(ListEntitiesRequest {
2659 event_type_prefix: prefix(),
2660 order: Some("asc".to_string()),
2661 ..Default::default()
2662 }),
2663 )
2664 .await
2665 .unwrap();
2666 let asc_ids: Vec<&str> = asc
2667 .0
2668 .entities
2669 .iter()
2670 .map(|e| e.entity_id.as_str())
2671 .collect();
2672 assert_eq!(asc_ids, ["org-a", "org-b", "org-c"]);
2673
2674 let page2 = list_entities(
2676 State(store.clone()),
2677 Query(ListEntitiesRequest {
2678 event_type_prefix: prefix(),
2679 order: Some("asc".to_string()),
2680 limit: Some(1),
2681 offset: Some(1),
2682 ..Default::default()
2683 }),
2684 )
2685 .await
2686 .unwrap();
2687 assert_eq!(page2.0.entities.len(), 1);
2688 assert_eq!(page2.0.entities[0].entity_id, "org-b");
2689 assert_eq!(page2.0.total, 3);
2690 assert!(page2.0.has_more);
2691
2692 let err = list_entities(
2694 State(store.clone()),
2695 Query(ListEntitiesRequest {
2696 event_type_prefix: prefix(),
2697 order: Some("sideways".to_string()),
2698 ..Default::default()
2699 }),
2700 )
2701 .await;
2702 assert!(err.is_err(), "invalid order value must be rejected");
2703 }
2704
2705 fn create_test_event_with_payload(
2706 entity_id: &str,
2707 event_type: &str,
2708 payload: serde_json::Value,
2709 ) -> Event {
2710 Event::from_strings(
2711 event_type.to_string(),
2712 entity_id.to_string(),
2713 "test-stream".to_string(),
2714 payload,
2715 None,
2716 )
2717 .unwrap()
2718 }
2719
2720 #[tokio::test]
2721 async fn test_detect_duplicates_by_payload_fields() {
2722 let store = create_test_store();
2723
2724 store
2726 .ingest(&create_test_event_with_payload(
2727 "idx-1",
2728 "index.created",
2729 serde_json::json!({"name": "S&P 500", "user_id": "alice"}),
2730 ))
2731 .unwrap();
2732 store
2733 .ingest(&create_test_event_with_payload(
2734 "idx-2",
2735 "index.created",
2736 serde_json::json!({"name": "S&P 500", "user_id": "bob"}),
2737 ))
2738 .unwrap();
2739 store
2740 .ingest(&create_test_event_with_payload(
2741 "idx-3",
2742 "index.created",
2743 serde_json::json!({"name": "NASDAQ", "user_id": "alice"}),
2744 ))
2745 .unwrap();
2746 store
2747 .ingest(&create_test_event_with_payload(
2748 "idx-4",
2749 "index.created",
2750 serde_json::json!({"name": "NASDAQ", "user_id": "carol"}),
2751 ))
2752 .unwrap();
2753 store
2754 .ingest(&create_test_event_with_payload(
2755 "idx-5",
2756 "index.created",
2757 serde_json::json!({"name": "DAX", "user_id": "dave"}),
2758 ))
2759 .unwrap();
2760
2761 let query_req = QueryEventsRequest {
2763 entity_id: None,
2764 event_type: None,
2765 tenant_id: None,
2766 as_of: None,
2767 since: None,
2768 until: None,
2769 limit: None,
2770 event_type_prefix: Some("index.".to_string()),
2771 payload_filter: None,
2772 };
2773 let events = store.query(&query_req).unwrap();
2774
2775 let group_by_fields = vec!["name"];
2777 let mut entity_latest: std::collections::HashMap<String, &Event> =
2778 std::collections::HashMap::new();
2779 for event in &events {
2780 let eid = event.entity_id().to_string();
2781 entity_latest
2782 .entry(eid)
2783 .and_modify(|existing| {
2784 if event.timestamp() > existing.timestamp() {
2785 *existing = event;
2786 }
2787 })
2788 .or_insert(event);
2789 }
2790
2791 let mut groups: std::collections::HashMap<String, Vec<String>> =
2792 std::collections::HashMap::new();
2793 for (entity_id, event) in &entity_latest {
2794 let payload = event.payload();
2795 let mut key_parts = serde_json::Map::new();
2796 for field in &group_by_fields {
2797 let value = payload
2798 .get(*field)
2799 .cloned()
2800 .unwrap_or(serde_json::Value::Null);
2801 key_parts.insert((*field).to_string(), value);
2802 }
2803 let key_str = serde_json::to_string(&key_parts).unwrap_or_default();
2804 groups.entry(key_str).or_default().push(entity_id.clone());
2805 }
2806
2807 let duplicate_groups: Vec<_> = groups
2808 .into_iter()
2809 .filter(|(_, ids)| ids.len() > 1)
2810 .collect();
2811
2812 assert_eq!(duplicate_groups.len(), 2); for (_, ids) in &duplicate_groups {
2814 assert_eq!(ids.len(), 2);
2815 }
2816 }
2817
2818 #[tokio::test]
2819 async fn test_detect_duplicates_no_duplicates() {
2820 let store = create_test_store();
2821
2822 store
2824 .ingest(&create_test_event_with_payload(
2825 "idx-1",
2826 "index.created",
2827 serde_json::json!({"name": "A"}),
2828 ))
2829 .unwrap();
2830 store
2831 .ingest(&create_test_event_with_payload(
2832 "idx-2",
2833 "index.created",
2834 serde_json::json!({"name": "B"}),
2835 ))
2836 .unwrap();
2837
2838 let query_req = QueryEventsRequest {
2839 entity_id: None,
2840 event_type: None,
2841 tenant_id: None,
2842 as_of: None,
2843 since: None,
2844 until: None,
2845 limit: None,
2846 event_type_prefix: Some("index.".to_string()),
2847 payload_filter: None,
2848 };
2849 let events = store.query(&query_req).unwrap();
2850
2851 let mut entity_latest: std::collections::HashMap<String, &Event> =
2852 std::collections::HashMap::new();
2853 for event in &events {
2854 entity_latest
2855 .entry(event.entity_id().to_string())
2856 .or_insert(event);
2857 }
2858
2859 let mut groups: std::collections::HashMap<String, Vec<String>> =
2860 std::collections::HashMap::new();
2861 for (entity_id, event) in &entity_latest {
2862 let key_str =
2863 serde_json::to_string(&serde_json::json!({"name": event.payload().get("name")}))
2864 .unwrap();
2865 groups.entry(key_str).or_default().push(entity_id.clone());
2866 }
2867
2868 let duplicate_groups: Vec<_> = groups
2869 .into_iter()
2870 .filter(|(_, ids)| ids.len() > 1)
2871 .collect();
2872
2873 assert_eq!(duplicate_groups.len(), 0); }
2875
2876 #[tokio::test]
2877 async fn test_detect_duplicates_multi_field_group_by() {
2878 let store = create_test_store();
2879
2880 store
2882 .ingest(&create_test_event_with_payload(
2883 "idx-1",
2884 "index.created",
2885 serde_json::json!({"name": "S&P 500", "user_id": "alice"}),
2886 ))
2887 .unwrap();
2888 store
2889 .ingest(&create_test_event_with_payload(
2890 "idx-2",
2891 "index.created",
2892 serde_json::json!({"name": "S&P 500", "user_id": "alice"}),
2893 ))
2894 .unwrap();
2895 store
2897 .ingest(&create_test_event_with_payload(
2898 "idx-3",
2899 "index.created",
2900 serde_json::json!({"name": "S&P 500", "user_id": "bob"}),
2901 ))
2902 .unwrap();
2903
2904 let query_req = QueryEventsRequest {
2905 entity_id: None,
2906 event_type: None,
2907 tenant_id: None,
2908 as_of: None,
2909 since: None,
2910 until: None,
2911 limit: None,
2912 event_type_prefix: Some("index.".to_string()),
2913 payload_filter: None,
2914 };
2915 let events = store.query(&query_req).unwrap();
2916
2917 let group_by_fields = vec!["name", "user_id"];
2918 let mut entity_latest: std::collections::HashMap<String, &Event> =
2919 std::collections::HashMap::new();
2920 for event in &events {
2921 entity_latest
2922 .entry(event.entity_id().to_string())
2923 .and_modify(|existing| {
2924 if event.timestamp() > existing.timestamp() {
2925 *existing = event;
2926 }
2927 })
2928 .or_insert(event);
2929 }
2930
2931 let mut groups: std::collections::HashMap<String, Vec<String>> =
2932 std::collections::HashMap::new();
2933 for (entity_id, event) in &entity_latest {
2934 let payload = event.payload();
2935 let mut key_parts = serde_json::Map::new();
2936 for field in &group_by_fields {
2937 let value = payload
2938 .get(*field)
2939 .cloned()
2940 .unwrap_or(serde_json::Value::Null);
2941 key_parts.insert((*field).to_string(), value);
2942 }
2943 let key_str = serde_json::to_string(&key_parts).unwrap_or_default();
2944 groups.entry(key_str).or_default().push(entity_id.clone());
2945 }
2946
2947 let duplicate_groups: Vec<_> = groups
2948 .into_iter()
2949 .filter(|(_, ids)| ids.len() > 1)
2950 .collect();
2951
2952 assert_eq!(duplicate_groups.len(), 1);
2954 let (_, ref ids) = duplicate_groups[0];
2955 assert_eq!(ids.len(), 2);
2956 let mut sorted_ids = ids.clone();
2957 sorted_ids.sort();
2958 assert_eq!(sorted_ids, vec!["idx-1", "idx-2"]);
2959 }
2960
2961 #[tokio::test]
2962 async fn test_projection_state_cache() {
2963 let store = create_test_store();
2964
2965 let cache = store.projection_state_cache();
2967 cache.insert(
2968 "entity_snapshots:user-123".to_string(),
2969 serde_json::json!({"name": "Test User", "age": 30}),
2970 );
2971
2972 let state = cache.get("entity_snapshots:user-123");
2974 assert!(state.is_some());
2975 let state = state.unwrap();
2976 assert_eq!(state["name"], "Test User");
2977 assert_eq!(state["age"], 30);
2978 }
2979
2980 #[tokio::test]
2981 async fn test_projection_manager_list_projections() {
2982 let store = create_test_store();
2983
2984 let projection_manager = store.projection_manager();
2986 let projections = projection_manager.list_projections();
2987
2988 assert!(projections.len() >= 2);
2990
2991 let names: Vec<&str> = projections.iter().map(|(name, _)| name.as_str()).collect();
2992 assert!(names.contains(&"entity_snapshots"));
2993 assert!(names.contains(&"event_counters"));
2994 }
2995
2996 #[tokio::test]
2997 async fn test_projection_state_after_event_ingestion() {
2998 let store = create_test_store();
2999
3000 let event = create_test_event("user-456", "user.created");
3002 store.ingest(&event).unwrap();
3003
3004 let projection_manager = store.projection_manager();
3006 let snapshot_projection = projection_manager
3007 .get_projection("entity_snapshots")
3008 .unwrap();
3009
3010 let state = snapshot_projection.get_state("user-456");
3011 assert!(state.is_some());
3012 let state = state.unwrap();
3013 assert_eq!(state["name"], "Test");
3014 assert_eq!(state["value"], 42);
3015 }
3016
3017 #[tokio::test]
3018 async fn test_projection_state_cache_multiple_entities() {
3019 let store = create_test_store();
3020 let cache = store.projection_state_cache();
3021
3022 for i in 0..10 {
3024 cache.insert(
3025 format!("entity_snapshots:entity-{i}"),
3026 serde_json::json!({"id": i, "status": "active"}),
3027 );
3028 }
3029
3030 assert_eq!(cache.len(), 10);
3032
3033 for i in 0..10 {
3035 let key = format!("entity_snapshots:entity-{i}");
3036 let state = cache.get(&key);
3037 assert!(state.is_some());
3038 assert_eq!(state.unwrap()["id"], i);
3039 }
3040 }
3041
3042 #[tokio::test]
3043 async fn test_projection_state_update() {
3044 let store = create_test_store();
3045 let cache = store.projection_state_cache();
3046
3047 cache.insert(
3049 "entity_snapshots:user-789".to_string(),
3050 serde_json::json!({"balance": 100}),
3051 );
3052
3053 cache.insert(
3055 "entity_snapshots:user-789".to_string(),
3056 serde_json::json!({"balance": 150}),
3057 );
3058
3059 let state = cache.get("entity_snapshots:user-789").unwrap();
3061 assert_eq!(state["balance"], 150);
3062 }
3063
3064 #[tokio::test]
3065 async fn test_event_counter_projection() {
3066 let store = create_test_store();
3067
3068 store
3070 .ingest(&create_test_event("user-1", "user.created"))
3071 .unwrap();
3072 store
3073 .ingest(&create_test_event("user-2", "user.created"))
3074 .unwrap();
3075 store
3076 .ingest(&create_test_event("user-1", "user.updated"))
3077 .unwrap();
3078
3079 let projection_manager = store.projection_manager();
3081 let counter_projection = projection_manager.get_projection("event_counters").unwrap();
3082
3083 let created_state = counter_projection.get_state("user.created");
3085 assert!(created_state.is_some());
3086 assert_eq!(created_state.unwrap()["count"], 2);
3087
3088 let updated_state = counter_projection.get_state("user.updated");
3089 assert!(updated_state.is_some());
3090 assert_eq!(updated_state.unwrap()["count"], 1);
3091 }
3092
3093 #[tokio::test]
3094 async fn test_projection_state_cache_key_format() {
3095 let store = create_test_store();
3096 let cache = store.projection_state_cache();
3097
3098 let key = "orders:order-12345".to_string();
3100 cache.insert(key.clone(), serde_json::json!({"total": 99.99}));
3101
3102 let state = cache.get(&key).unwrap();
3103 assert_eq!(state["total"], 99.99);
3104 }
3105
3106 #[tokio::test]
3107 async fn test_projection_state_cache_removal() {
3108 let store = create_test_store();
3109 let cache = store.projection_state_cache();
3110
3111 cache.insert(
3113 "test:entity-1".to_string(),
3114 serde_json::json!({"data": "value"}),
3115 );
3116 assert_eq!(cache.len(), 1);
3117
3118 cache.remove("test:entity-1");
3119 assert_eq!(cache.len(), 0);
3120 assert!(cache.get("test:entity-1").is_none());
3121 }
3122
3123 #[tokio::test]
3124 async fn test_get_nonexistent_projection() {
3125 let store = create_test_store();
3126 let projection_manager = store.projection_manager();
3127
3128 let projection = projection_manager.get_projection("nonexistent_projection");
3130 assert!(projection.is_none());
3131 }
3132
3133 #[tokio::test]
3134 async fn test_get_nonexistent_entity_state() {
3135 let store = create_test_store();
3136 let projection_manager = store.projection_manager();
3137
3138 let snapshot_projection = projection_manager
3140 .get_projection("entity_snapshots")
3141 .unwrap();
3142 let state = snapshot_projection.get_state("nonexistent-entity-xyz");
3143 assert!(state.is_none());
3144 }
3145
3146 #[tokio::test]
3147 async fn test_projection_state_cache_concurrent_access() {
3148 let store = create_test_store();
3149 let cache = store.projection_state_cache();
3150
3151 let handles: Vec<_> = (0..10)
3153 .map(|i| {
3154 let cache_clone = cache.clone();
3155 tokio::spawn(async move {
3156 cache_clone.insert(
3157 format!("concurrent:entity-{i}"),
3158 serde_json::json!({"thread": i}),
3159 );
3160 })
3161 })
3162 .collect();
3163
3164 for handle in handles {
3165 handle.await.unwrap();
3166 }
3167
3168 assert_eq!(cache.len(), 10);
3170 }
3171
3172 #[tokio::test]
3173 async fn test_projection_state_large_payload() {
3174 let store = create_test_store();
3175 let cache = store.projection_state_cache();
3176
3177 let large_array: Vec<serde_json::Value> = (0..1000)
3179 .map(|i| serde_json::json!({"item": i, "description": "test item with some padding data to increase size"}))
3180 .collect();
3181
3182 cache.insert(
3183 "large:entity-1".to_string(),
3184 serde_json::json!({"items": large_array}),
3185 );
3186
3187 let state = cache.get("large:entity-1").unwrap();
3188 let items = state["items"].as_array().unwrap();
3189 assert_eq!(items.len(), 1000);
3190 }
3191
3192 #[tokio::test]
3193 async fn test_projection_state_complex_json() {
3194 let store = create_test_store();
3195 let cache = store.projection_state_cache();
3196
3197 let complex_state = serde_json::json!({
3199 "user": {
3200 "id": "user-123",
3201 "profile": {
3202 "name": "John Doe",
3203 "email": "john@example.com",
3204 "settings": {
3205 "theme": "dark",
3206 "notifications": true
3207 }
3208 },
3209 "roles": ["admin", "user"],
3210 "metadata": {
3211 "created_at": "2025-01-01T00:00:00Z",
3212 "last_login": null
3213 }
3214 }
3215 });
3216
3217 cache.insert("complex:user-123".to_string(), complex_state);
3218
3219 let state = cache.get("complex:user-123").unwrap();
3220 assert_eq!(state["user"]["profile"]["name"], "John Doe");
3221 assert_eq!(state["user"]["roles"][0], "admin");
3222 assert!(state["user"]["metadata"]["last_login"].is_null());
3223 }
3224
3225 #[tokio::test]
3226 async fn test_projection_state_cache_iteration() {
3227 let store = create_test_store();
3228 let cache = store.projection_state_cache();
3229
3230 for i in 0..5 {
3232 cache.insert(format!("iter:entity-{i}"), serde_json::json!({"index": i}));
3233 }
3234
3235 let entries: Vec<_> = cache.iter().map(|entry| entry.key().clone()).collect();
3237 assert_eq!(entries.len(), 5);
3238 }
3239
3240 #[tokio::test]
3241 async fn test_projection_manager_get_entity_snapshots() {
3242 let store = create_test_store();
3243 let projection_manager = store.projection_manager();
3244
3245 let projection = projection_manager.get_projection("entity_snapshots");
3247 assert!(projection.is_some());
3248 assert_eq!(projection.unwrap().name(), "entity_snapshots");
3249 }
3250
3251 #[tokio::test]
3252 async fn test_projection_manager_get_event_counters() {
3253 let store = create_test_store();
3254 let projection_manager = store.projection_manager();
3255
3256 let projection = projection_manager.get_projection("event_counters");
3258 assert!(projection.is_some());
3259 assert_eq!(projection.unwrap().name(), "event_counters");
3260 }
3261
3262 #[tokio::test]
3263 async fn test_projection_state_cache_overwrite() {
3264 let store = create_test_store();
3265 let cache = store.projection_state_cache();
3266
3267 cache.insert(
3269 "overwrite:entity-1".to_string(),
3270 serde_json::json!({"version": 1}),
3271 );
3272
3273 cache.insert(
3275 "overwrite:entity-1".to_string(),
3276 serde_json::json!({"version": 2}),
3277 );
3278
3279 cache.insert(
3281 "overwrite:entity-1".to_string(),
3282 serde_json::json!({"version": 3}),
3283 );
3284
3285 let state = cache.get("overwrite:entity-1").unwrap();
3286 assert_eq!(state["version"], 3);
3287
3288 assert_eq!(cache.len(), 1);
3290 }
3291
3292 #[tokio::test]
3293 async fn test_projection_state_multiple_projections() {
3294 let store = create_test_store();
3295 let cache = store.projection_state_cache();
3296
3297 cache.insert(
3299 "entity_snapshots:user-1".to_string(),
3300 serde_json::json!({"name": "Alice"}),
3301 );
3302 cache.insert(
3303 "event_counters:user.created".to_string(),
3304 serde_json::json!({"count": 5}),
3305 );
3306 cache.insert(
3307 "custom_projection:order-1".to_string(),
3308 serde_json::json!({"total": 150.0}),
3309 );
3310
3311 assert_eq!(
3313 cache.get("entity_snapshots:user-1").unwrap()["name"],
3314 "Alice"
3315 );
3316 assert_eq!(
3317 cache.get("event_counters:user.created").unwrap()["count"],
3318 5
3319 );
3320 assert_eq!(
3321 cache.get("custom_projection:order-1").unwrap()["total"],
3322 150.0
3323 );
3324 }
3325
3326 #[tokio::test]
3327 async fn test_bulk_projection_state_access() {
3328 let store = create_test_store();
3329
3330 for i in 0..5 {
3332 let event = create_test_event(&format!("bulk-user-{i}"), "user.created");
3333 store.ingest(&event).unwrap();
3334 }
3335
3336 let projection_manager = store.projection_manager();
3338 let snapshot_projection = projection_manager
3339 .get_projection("entity_snapshots")
3340 .unwrap();
3341
3342 for i in 0..5 {
3344 let state = snapshot_projection.get_state(&format!("bulk-user-{i}"));
3345 assert!(state.is_some(), "Entity bulk-user-{i} should have state");
3346 }
3347 }
3348
3349 #[tokio::test]
3350 async fn test_bulk_save_projection_states() {
3351 let store = create_test_store();
3352 let cache = store.projection_state_cache();
3353
3354 let states = vec![
3356 BulkSaveStateItem {
3357 entity_id: "bulk-entity-1".to_string(),
3358 state: serde_json::json!({"name": "Entity 1", "value": 100}),
3359 },
3360 BulkSaveStateItem {
3361 entity_id: "bulk-entity-2".to_string(),
3362 state: serde_json::json!({"name": "Entity 2", "value": 200}),
3363 },
3364 BulkSaveStateItem {
3365 entity_id: "bulk-entity-3".to_string(),
3366 state: serde_json::json!({"name": "Entity 3", "value": 300}),
3367 },
3368 ];
3369
3370 let projection_name = "test_projection";
3371
3372 for item in &states {
3374 cache.insert(
3375 format!("{projection_name}:{}", item.entity_id),
3376 item.state.clone(),
3377 );
3378 }
3379
3380 assert_eq!(cache.len(), 3);
3382
3383 let state1 = cache.get("test_projection:bulk-entity-1").unwrap();
3384 assert_eq!(state1["name"], "Entity 1");
3385 assert_eq!(state1["value"], 100);
3386
3387 let state2 = cache.get("test_projection:bulk-entity-2").unwrap();
3388 assert_eq!(state2["name"], "Entity 2");
3389 assert_eq!(state2["value"], 200);
3390
3391 let state3 = cache.get("test_projection:bulk-entity-3").unwrap();
3392 assert_eq!(state3["name"], "Entity 3");
3393 assert_eq!(state3["value"], 300);
3394 }
3395
3396 #[tokio::test]
3397 async fn test_bulk_save_empty_states() {
3398 let store = create_test_store();
3399 let cache = store.projection_state_cache();
3400
3401 cache.clear();
3403
3404 let states: Vec<BulkSaveStateItem> = vec![];
3406 assert_eq!(states.len(), 0);
3407
3408 assert_eq!(cache.len(), 0);
3410 }
3411
3412 #[tokio::test]
3413 async fn test_bulk_save_overwrites_existing() {
3414 let store = create_test_store();
3415 let cache = store.projection_state_cache();
3416
3417 cache.insert(
3419 "test:entity-1".to_string(),
3420 serde_json::json!({"version": 1, "data": "initial"}),
3421 );
3422
3423 let new_state = serde_json::json!({"version": 2, "data": "updated"});
3425 cache.insert("test:entity-1".to_string(), new_state);
3426
3427 let state = cache.get("test:entity-1").unwrap();
3429 assert_eq!(state["version"], 2);
3430 assert_eq!(state["data"], "updated");
3431 }
3432
3433 #[tokio::test]
3434 async fn test_bulk_save_high_volume() {
3435 let store = create_test_store();
3436 let cache = store.projection_state_cache();
3437
3438 for i in 0..1000 {
3440 cache.insert(
3441 format!("volume_test:entity-{i}"),
3442 serde_json::json!({"index": i, "status": "active"}),
3443 );
3444 }
3445
3446 assert_eq!(cache.len(), 1000);
3448
3449 assert_eq!(cache.get("volume_test:entity-0").unwrap()["index"], 0);
3451 assert_eq!(cache.get("volume_test:entity-500").unwrap()["index"], 500);
3452 assert_eq!(cache.get("volume_test:entity-999").unwrap()["index"], 999);
3453 }
3454
3455 #[tokio::test]
3456 async fn test_bulk_save_different_projections() {
3457 let store = create_test_store();
3458 let cache = store.projection_state_cache();
3459
3460 let projections = ["entity_snapshots", "event_counters", "custom_analytics"];
3462
3463 for proj in &projections {
3464 for i in 0..5 {
3465 cache.insert(
3466 format!("{proj}:entity-{i}"),
3467 serde_json::json!({"projection": proj, "id": i}),
3468 );
3469 }
3470 }
3471
3472 assert_eq!(cache.len(), 15);
3474
3475 for proj in &projections {
3477 let state = cache.get(&format!("{proj}:entity-0")).unwrap();
3478 assert_eq!(state["projection"], *proj);
3479 }
3480 }
3481
3482 #[tokio::test]
3493 async fn get_projection_state_falls_back_to_cache_when_unregistered() {
3494 let store = create_test_store();
3495 store.projection_state_cache().insert(
3496 "assets:BTC".to_string(),
3497 serde_json::json!({"symbol": "BTC", "altname": "Bitcoin"}),
3498 );
3499
3500 let resp = get_projection_state(
3501 State(Arc::clone(&store)),
3502 Path(("assets".to_string(), "BTC".to_string())),
3503 )
3504 .await
3505 .expect("should not error when projection is not registered");
3506
3507 assert_eq!(resp.0["found"], serde_json::Value::Bool(true));
3508 assert_eq!(resp.0["state"]["symbol"], "BTC");
3509 assert_eq!(resp.0["state"]["altname"], "Bitcoin");
3510 }
3511
3512 #[tokio::test]
3513 async fn get_projection_state_returns_not_found_when_absent_everywhere() {
3514 let store = create_test_store();
3515
3516 let resp = get_projection_state(
3517 State(Arc::clone(&store)),
3518 Path(("assets".to_string(), "UNKNOWN".to_string())),
3519 )
3520 .await
3521 .unwrap();
3522
3523 assert_eq!(resp.0["found"], serde_json::Value::Bool(false));
3524 assert_eq!(resp.0["state"], serde_json::Value::Null);
3525 }
3526
3527 #[tokio::test]
3528 async fn get_projection_state_registered_wins_over_cache() {
3529 let store = create_test_store();
3530
3531 let event = create_test_event("user-777", "user.created");
3533 store.ingest(&event).unwrap();
3534
3535 store.projection_state_cache().insert(
3537 "entity_snapshots:user-777".to_string(),
3538 serde_json::json!({"stolen": "value"}),
3539 );
3540
3541 let resp = get_projection_state(
3542 State(Arc::clone(&store)),
3543 Path(("entity_snapshots".to_string(), "user-777".to_string())),
3544 )
3545 .await
3546 .unwrap();
3547
3548 assert_eq!(resp.0["found"], serde_json::Value::Bool(true));
3551 assert!(
3552 resp.0["state"].get("stolen").is_none(),
3553 "cache entry must not shadow registered projection state: got {:?}",
3554 resp.0["state"]
3555 );
3556 }
3557
3558 #[tokio::test]
3559 async fn get_projection_state_summary_returns_cache_without_registration() {
3560 let store = create_test_store();
3561 let cache = store.projection_state_cache();
3562 cache.insert("assets:BTC".into(), serde_json::json!({"symbol": "BTC"}));
3563 cache.insert("assets:ETH".into(), serde_json::json!({"symbol": "ETH"}));
3564 cache.insert("trades:t-1".into(), serde_json::json!({"x": 1}));
3566
3567 let resp =
3568 get_projection_state_summary(State(Arc::clone(&store)), Path("assets".to_string()))
3569 .await
3570 .unwrap();
3571
3572 assert_eq!(resp.0["total"], 2);
3573 let states = resp.0["states"].as_array().unwrap();
3574 let entity_ids: Vec<&str> = states
3575 .iter()
3576 .map(|s| s["entity_id"].as_str().unwrap())
3577 .collect();
3578 assert!(entity_ids.contains(&"BTC"));
3579 assert!(entity_ids.contains(&"ETH"));
3580 }
3581
3582 #[tokio::test]
3583 async fn bulk_get_projection_states_falls_back_to_cache() {
3584 let store = create_test_store();
3585 let cache = store.projection_state_cache();
3586 cache.insert("assets:BTC".into(), serde_json::json!({"symbol": "BTC"}));
3587 cache.insert("assets:ETH".into(), serde_json::json!({"symbol": "ETH"}));
3588
3589 let req = BulkGetStateRequest {
3590 entity_ids: vec!["BTC".into(), "ETH".into(), "MISSING".into()],
3591 };
3592
3593 let resp = bulk_get_projection_states(
3594 State(Arc::clone(&store)),
3595 Path("assets".to_string()),
3596 Json(req),
3597 )
3598 .await
3599 .unwrap();
3600
3601 assert_eq!(resp.0["total"], 3);
3602 let states = resp.0["states"].as_array().unwrap();
3603 let by_id: std::collections::HashMap<&str, &serde_json::Value> = states
3604 .iter()
3605 .map(|s| (s["entity_id"].as_str().unwrap(), s))
3606 .collect();
3607
3608 assert_eq!(by_id["BTC"]["found"], serde_json::Value::Bool(true));
3609 assert_eq!(by_id["BTC"]["state"]["symbol"], "BTC");
3610 assert_eq!(by_id["ETH"]["found"], serde_json::Value::Bool(true));
3611 assert_eq!(by_id["MISSING"]["found"], serde_json::Value::Bool(false));
3612 }
3613}