Skip to content

Commit

Permalink
feat: add route for searching events
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeDecMeetsMore committed Nov 12, 2024
1 parent 038304f commit 01bd375
Show file tree
Hide file tree
Showing 14 changed files with 400 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

6 changes: 6 additions & 0 deletions crates/api/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod get_event_instances;
mod get_events_by_calendars;
mod get_events_by_meta;
pub mod get_upcoming_reminders;
mod search_events;
mod subscribers;
pub mod sync_event_reminders;
mod update_event;
Expand All @@ -16,6 +17,7 @@ use delete_event::{delete_event_admin_controller, delete_event_controller};
use get_event::{get_event_admin_controller, get_event_controller};
use get_event_instances::{get_event_instances_admin_controller, get_event_instances_controller};
use get_events_by_meta::get_events_by_meta_controller;
use search_events::search_events_controller;
use update_event::{update_event_admin_controller, update_event_controller};

// Configure the routes for the event module
Expand All @@ -37,6 +39,10 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
// Get events by metadata
cfg.route("/events/meta", web::get().to(get_events_by_meta_controller));

// Search events
// /!\ This is a POST route
cfg.route("/events/search", web::post().to(search_events_controller));

// Get a specific event by external id
cfg.route(
"/user/events/external_id/{external_id}",
Expand Down
159 changes: 159 additions & 0 deletions crates/api/src/event/search_events.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
use actix_web::{web, HttpRequest, HttpResponse};
use nittei_api_structs::{dtos::CalendarEventDTO, search_events::*};
use nittei_domain::{DateTimeQuery, IDQuery, ID};
use nittei_infra::{NitteiContext, SearchEventsParams};

use crate::{
error::NitteiError,
shared::{
auth::protect_account_route,
usecase::{execute, UseCase},
},
};

pub async fn search_events_controller(
http_req: HttpRequest,
body: actix_web_validator::Json<RequestBody>,
ctx: web::Data<NitteiContext>,
) -> Result<HttpResponse, NitteiError> {
let account = protect_account_route(&http_req, &ctx).await?;

let body = body.0;
let usecase = SearchEventsUseCase {
account_id: account.id,
user_id: body.user_id,
calendar_ids: body.calendar_ids,
parent_id: body.parent_id,
start_time: body.start_time,
end_time: body.end_time,
status: body.status,
updated_at: body.updated_at,
metadata: body.metadata,
};

execute(usecase, &ctx)
.await
.map(|events| HttpResponse::Ok().json(APIResponse::new(events.events)))
.map_err(NitteiError::from)
}

#[derive(Debug)]
pub struct SearchEventsUseCase {
/// Account ID
pub account_id: ID,

/// User ID
pub user_id: ID,

/// Optional list of calendar UUIDs
/// If not provided, all calendars will be used
pub calendar_ids: Option<Vec<ID>>,

/// Optional query on parent ID
pub parent_id: Option<IDQuery>,

/// Optional query on start time - "lower than or equal", or "great than or equal" (UTC)
pub start_time: Option<DateTimeQuery>,

/// Optional query on end time - "lower than or equal", or "great than or equal" (UTC)
pub end_time: Option<DateTimeQuery>,

/// Optional list of event status
pub status: Option<Vec<String>>,

/// Optioanl query on updated at - "lower than or equal", or "great than or equal" (UTC)
pub updated_at: Option<DateTimeQuery>,

/// Optional list of metadata key-value pairs
pub metadata: Option<serde_json::Value>,
}

#[derive(Debug)]
pub struct UseCaseResponse {
pub events: Vec<CalendarEventDTO>,
}

#[derive(Debug)]
pub enum UseCaseError {
InternalError,
BadRequest(String),
NotFound(String, String),
}

impl From<UseCaseError> for NitteiError {
fn from(e: UseCaseError) -> Self {
match e {
UseCaseError::InternalError => Self::InternalError,
UseCaseError::BadRequest(msg) => Self::BadClientData(msg),
UseCaseError::NotFound(entity, event_id) => Self::NotFound(format!(
"The {} with id: {}, was not found.",
entity, event_id
)),
}
}
}

#[async_trait::async_trait(?Send)]
impl UseCase for SearchEventsUseCase {
type Response = UseCaseResponse;

type Error = UseCaseError;

const NAME: &'static str = "SearchEvents";

async fn execute(&mut self, ctx: &NitteiContext) -> Result<UseCaseResponse, UseCaseError> {
if let Some(calendar_ids) = &self.calendar_ids {
if calendar_ids.is_empty() {
return Err(UseCaseError::BadRequest(
"calendar_ids cannot be empty".into(),
));
}

let calendars = ctx
.repos
.calendars
.find_multiple(calendar_ids.iter().collect())
.await
.map_err(|_| UseCaseError::InternalError)?;

// Check that all calendars exist and belong to the same account
if calendars.is_empty()
|| calendars.len() != calendar_ids.len()
|| !calendars
.iter()
.all(|cal| cal.account_id == self.account_id)
{
return Err(UseCaseError::NotFound(
"Calendars not found".to_string(),
calendar_ids
.iter()
.map(|c| c.to_string())
.collect::<Vec<String>>()
.join(","),
));
}
}

let res = ctx
.repos
.events
.search_events(SearchEventsParams {
user_id: self.user_id.clone(),
calendar_ids: self.calendar_ids.clone(),
parent_id: self.parent_id.clone(),
start_time: self.start_time.clone(),
end_time: self.end_time.clone(),
status: self.status.clone(),
updated_at: self.updated_at.clone(),
metadata: self.metadata.clone(),
})
.await;

match res {
Ok(events) => Ok(UseCaseResponse {
events: events.into_iter().map(CalendarEventDTO::new).collect(),
}),
Err(_) => Err(UseCaseError::InternalError),
}
}
}
53 changes: 53 additions & 0 deletions crates/api_structs/src/event/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,59 @@ pub mod get_events_by_calendars {
}
}

pub mod search_events {
use nittei_domain::{DateTimeQuery, IDQuery};

use super::*;

/// Query parameters for searching events
#[derive(Deserialize, Serialize, Validate, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, rename = "SearchEventsRequestBody")]
pub struct RequestBody {
/// User ID
pub user_id: ID,

/// Optional list of calendar UUIDs
/// If not provided, all calendars will be used
#[validate(length(min = 1))]
pub calendar_ids: Option<Vec<ID>>,

/// Optional query on parent ID
pub parent_id: Option<IDQuery>,

/// Optional query on start time - "lower than or equal", or "great than or equal" (UTC)
pub start_time: Option<DateTimeQuery>,

/// Optional query on end time - "lower than or equal", or "great than or equal" (UTC)
pub end_time: Option<DateTimeQuery>,

/// Optional list of event status
pub status: Option<Vec<String>>,

/// Optioanl query on updated at - "lower than or equal", or "great than or equal" (UTC)
pub updated_at: Option<DateTimeQuery>,

/// Optional list of metadata key-value pairs
pub metadata: Option<serde_json::Value>,
}

/// API response for getting events by calendars
#[derive(Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, rename = "SearchEventsAPIResponse")]
pub struct APIResponse {
/// List of calendar events retrieved
pub events: Vec<CalendarEventDTO>,
}

impl APIResponse {
pub fn new(events: Vec<CalendarEventDTO>) -> Self {
Self { events }
}
}
}

pub mod get_events_by_meta {
use super::*;

Expand Down
1 change: 1 addition & 0 deletions crates/domain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ ts-rs = { git = "https://github.com/Aleph-Alpha/ts-rs.git", rev = "4aea173270f5d
"chrono-impl",
"serde-json-impl",
] }
validator = { version = "0.18", features = ["derive"] }
2 changes: 2 additions & 0 deletions crates/domain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ pub use service::{
TimePlan,
};
pub use shared::{
datetime_query::DateTimeQuery,
entity::{Entity, ID},
id_query::IDQuery,
metadata::{Meta, Metadata},
recurrence::{RRuleFrequency, RRuleOptions, WeekDayRecurrence},
};
Expand Down
18 changes: 18 additions & 0 deletions crates/domain/src/shared/datetime_query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use validator::Validate;

/// Query parameters for searching on a date time
#[derive(Deserialize, Serialize, TS, Debug, Validate, Clone)]
#[serde(rename_all = "camelCase")]
#[ts(export, rename = "DateTimeQuery")]
pub struct DateTimeQuery {
/// Optional "greater than or equal" query (UTC)
#[ts(type = "Date")]
pub gte: Option<DateTime<Utc>>,

/// Optional "less than or equal" query (UTC)
#[ts(type = "Date")]
pub lte: Option<DateTime<Utc>>,
}
17 changes: 17 additions & 0 deletions crates/domain/src/shared/id_query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use validator::Validate;

use crate::ID;

/// Query parameters for searching on an ID
#[derive(Deserialize, Serialize, TS, Debug, Validate, Clone)]
#[serde(rename_all = "camelCase")]
#[ts(export, rename = "IdQuery")]
pub struct IDQuery {
/// Optional ID (equality test)
pub eq: Option<ID>,
/// Optional bool (existence test)
/// If "eq" is provided, this field is ignored
pub exists: Option<bool>,
}
2 changes: 2 additions & 0 deletions crates/domain/src/shared/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub mod datetime_query;
pub mod entity;
pub mod id_query;
pub mod metadata;
pub mod recurrence;
pub mod weekday;
7 changes: 6 additions & 1 deletion crates/infra/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ use std::sync::Arc;

pub use config::Config;
use repos::Repos;
pub use repos::{BusyCalendarIdentifier, ExternalBusyCalendarIdentifier, MetadataFindQuery};
pub use repos::{
BusyCalendarIdentifier,
ExternalBusyCalendarIdentifier,
MetadataFindQuery,
SearchEventsParams,
};
pub use services::*;
use sqlx::postgres::PgPoolOptions;
pub use system::ISys;
Expand Down
17 changes: 16 additions & 1 deletion crates/infra/src/repos/event/calendar_event/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
mod postgres;

use chrono::{DateTime, Utc};
use nittei_domain::{CalendarEvent, TimeSpan, ID};
use nittei_domain::{CalendarEvent, DateTimeQuery, IDQuery, TimeSpan, ID};
pub use postgres::PostgresEventRepo;

use crate::repos::shared::query_structs::MetadataFindQuery;
Expand All @@ -12,6 +12,17 @@ pub struct MostRecentCreatedServiceEvents {
pub created: Option<i64>,
}

pub struct SearchEventsParams {
pub user_id: ID,
pub calendar_ids: Option<Vec<ID>>,
pub parent_id: Option<IDQuery>,
pub start_time: Option<DateTimeQuery>,
pub end_time: Option<DateTimeQuery>,
pub status: Option<Vec<String>>,
pub updated_at: Option<DateTimeQuery>,
pub metadata: Option<serde_json::Value>,
}

#[async_trait::async_trait]
pub trait IEventRepo: Send + Sync {
async fn insert(&self, e: &CalendarEvent) -> anyhow::Result<()>;
Expand All @@ -29,6 +40,10 @@ pub trait IEventRepo: Send + Sync {
calendar_ids: Vec<ID>,
timespan: &TimeSpan,
) -> anyhow::Result<Vec<CalendarEvent>>;
async fn search_events(
&self,
search_events_params: SearchEventsParams,
) -> anyhow::Result<Vec<CalendarEvent>>;
async fn find_most_recently_created_service_events(
&self,
service_id: &ID,
Expand Down
Loading

0 comments on commit 01bd375

Please sign in to comment.