diff --git a/clients/javascript/lib/gen_types/CreateEventGroupRequestBody.ts b/clients/javascript/lib/gen_types/CreateEventGroupRequestBody.ts new file mode 100644 index 00000000..c1271e12 --- /dev/null +++ b/clients/javascript/lib/gen_types/CreateEventGroupRequestBody.ts @@ -0,0 +1,22 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ID } from './ID' + +/** + * Request body for creating an event + */ +export type CreateEventGroupRequestBody = { + /** + * UUID of the calendar where the event group will be created + */ + calendarId: ID + /** + * Optional parent event ID + * This is useful for external applications that need to link Nittei's events to a wider data model (e.g. a project, an order, etc.) + */ + parentId?: string + /** + * Optional external event ID + * This is useful for external applications that need to link Nittei's events to their own data models + */ + externalId?: string +} diff --git a/clients/javascript/lib/gen_types/EventGroup.ts b/clients/javascript/lib/gen_types/EventGroup.ts index 243939fd..7b47529c 100644 --- a/clients/javascript/lib/gen_types/EventGroup.ts +++ b/clients/javascript/lib/gen_types/EventGroup.ts @@ -5,11 +5,34 @@ import type { ID } from './ID' * Group of calendar events */ export type EventGroup = { + /** + * Unique ID + */ id: ID + /** + * Calendar ID to which the group belongs + */ calendarId: ID + /** + * User ID + */ userId: ID + /** + * Account ID + */ accountId: ID + /** + * Parent ID - this is an ID external to the system + * It allows to link groups of events together to an outside entity + */ parentId: string | null + /** + * External ID - this is an ID external to the system + * It allows to link a group of events to an outside entity + */ externalId: string | null + /** + * List of event IDs in the group + */ eventIds: Array } diff --git a/clients/javascript/lib/gen_types/EventGroupDTO.ts b/clients/javascript/lib/gen_types/EventGroupDTO.ts new file mode 100644 index 00000000..b836c4d5 --- /dev/null +++ b/clients/javascript/lib/gen_types/EventGroupDTO.ts @@ -0,0 +1,28 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ID } from './ID' + +/** + * Calendar event object + */ +export type EventGroupDTO = { + /** + * UUID of the event + */ + id: ID + /** + * Optional parent event ID + */ + parentId: string | null + /** + * Optional external ID + */ + externalId: string | null + /** + * UUID of the calendar + */ + calendarId: ID + /** + * UUID of the user + */ + userId: ID +} diff --git a/clients/javascript/lib/gen_types/EventGroupResponse.ts b/clients/javascript/lib/gen_types/EventGroupResponse.ts new file mode 100644 index 00000000..0f466f7f --- /dev/null +++ b/clients/javascript/lib/gen_types/EventGroupResponse.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EventGroupDTO } from './EventGroupDTO' + +/** + * Calendar event response object + */ +export type EventGroupResponse = { + /** + * Calendar event retrieved + */ + eventGroup: EventGroupDTO +} diff --git a/clients/javascript/lib/gen_types/UpdateEventGroupRequestBody.ts b/clients/javascript/lib/gen_types/UpdateEventGroupRequestBody.ts new file mode 100644 index 00000000..45d9b472 --- /dev/null +++ b/clients/javascript/lib/gen_types/UpdateEventGroupRequestBody.ts @@ -0,0 +1,18 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Request body for updating an event + */ +export type UpdateEventGroupRequestBody = { + /** + * Optional parent event ID + * This is useful for external applications that need to link Nittei's events to a wider data model (e.g. a project, an order, etc.) + */ + parentId?: string + /** + * Optional external event ID + * This is useful for external applications that need to link Nittei's events to their own data models + * Default is None + */ + externalId?: string +} diff --git a/clients/javascript/lib/gen_types/index.ts b/clients/javascript/lib/gen_types/index.ts index 9b582b02..3fbab310 100644 --- a/clients/javascript/lib/gen_types/index.ts +++ b/clients/javascript/lib/gen_types/index.ts @@ -22,12 +22,15 @@ export * from './CalendarSettingsDTO' export * from './CreateAccountRequestBody' export * from './CreateAccountResponseBody' export * from './CreateCalendarRequestBody' +export * from './CreateEventGroupRequestBody' export * from './CreateEventRequestBody' export * from './CreateServiceEventIntendRequestBody' export * from './CreateServiceRequestBody' export * from './CreateUserRequestBody' export * from './DateTimeQuery' export * from './EventGroup' +export * from './EventGroupDTO' +export * from './EventGroupResponse' export * from './EventInstance' export * from './EventWithInstancesDTO' export * from './GetCalendarEventsAPIResponse' @@ -88,6 +91,7 @@ export * from './TimePlan' export * from './TimeSpan' export * from './UpdateCalendarRequestBody' export * from './UpdateCalendarSettings' +export * from './UpdateEventGroupRequestBody' export * from './UpdateEventRequestBody' export * from './UpdateServiceRequestBody' export * from './UpdateServiceUserRequestBody' diff --git a/crates/api/src/event_group/create_event_group.rs b/crates/api/src/event_group/create_event_group.rs new file mode 100644 index 00000000..56923580 --- /dev/null +++ b/crates/api/src/event_group/create_event_group.rs @@ -0,0 +1,175 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use nittei_api_structs::create_event_group::*; +use nittei_domain::{event_group::EventGroup, User, ID}; +use nittei_infra::NitteiContext; + +use crate::{ + error::NitteiError, + shared::{ + auth::{account_can_modify_user, protect_account_route}, + usecase::{execute, UseCase}, + }, +}; + +pub async fn create_event_group_admin_controller( + http_req: HttpRequest, + path_params: web::Path, + body: actix_web_validator::Json, + ctx: web::Data, +) -> Result { + let account = protect_account_route(&http_req, &ctx).await?; + let user = account_can_modify_user(&account, &path_params.user_id, &ctx).await?; + + let body = body.0; + let usecase = CreateEventGroupUseCase { + parent_id: body.parent_id, + external_id: body.external_id, + user, + calendar_id: body.calendar_id, + }; + + execute(usecase, &ctx) + .await + .map(|group| HttpResponse::Created().json(APIResponse::new(group))) + .map_err(NitteiError::from) +} + +#[derive(Debug, Default)] +pub struct CreateEventGroupUseCase { + pub calendar_id: ID, + pub user: User, + pub parent_id: Option, + pub external_id: Option, +} + +#[derive(Debug, PartialEq)] +pub enum UseCaseError { + NotFound(ID), + StorageError, +} + +impl From for NitteiError { + fn from(e: UseCaseError) -> Self { + match e { + UseCaseError::NotFound(calendar_id) => Self::NotFound(format!( + "The calendar with id: {}, was not found.", + calendar_id + )), + UseCaseError::StorageError => Self::InternalError, + } + } +} + +impl From for UseCaseError { + fn from(_: anyhow::Error) -> Self { + UseCaseError::StorageError + } +} + +#[async_trait::async_trait(?Send)] +impl UseCase for CreateEventGroupUseCase { + type Response = EventGroup; + + type Error = UseCaseError; + + const NAME: &'static str = "CreateEvent"; + + async fn execute(&mut self, ctx: &NitteiContext) -> Result { + let calendar = ctx + .repos + .calendars + .find(&self.calendar_id) + .await + .map_err(|_| UseCaseError::StorageError)?; + let calendar = match calendar { + Some(calendar) if calendar.user_id == self.user.id => calendar, + _ => return Err(UseCaseError::NotFound(self.calendar_id.clone())), + }; + + let g = EventGroup { + id: Default::default(), + parent_id: self.parent_id.clone(), + external_id: self.external_id.clone(), + calendar_id: calendar.id.clone(), + user_id: self.user.id.clone(), + account_id: self.user.account_id.clone(), + event_ids: vec![], + }; + + ctx.repos.event_groups.insert(&g).await?; + + Ok(g) + } +} + +#[cfg(test)] +mod test { + use nittei_domain::{Account, Calendar, User}; + use nittei_infra::setup_context; + + use super::*; + + struct TestContext { + ctx: NitteiContext, + calendar: Calendar, + user: User, + } + + async fn setup() -> TestContext { + let ctx = setup_context().await.unwrap(); + let account = Account::default(); + ctx.repos.accounts.insert(&account).await.unwrap(); + let user = User::new(account.id.clone(), None); + ctx.repos.users.insert(&user).await.unwrap(); + let calendar = Calendar::new(&user.id, &account.id, None, None); + ctx.repos.calendars.insert(&calendar).await.unwrap(); + + TestContext { + user, + calendar, + ctx, + } + } + + #[actix_web::main] + #[test] + async fn creates_event_group() { + let TestContext { + ctx, + calendar, + user, + } = setup().await; + + let mut usecase = CreateEventGroupUseCase { + calendar_id: calendar.id.clone(), + user, + ..Default::default() + }; + + let res = usecase.execute(&ctx).await; + + assert!(res.is_ok()); + } + + #[actix_web::main] + #[test] + async fn rejects_invalid_calendar_id() { + let TestContext { + ctx, + calendar: _, + user, + } = setup().await; + + let mut usecase = CreateEventGroupUseCase { + user, + ..Default::default() + }; + + let res = usecase.execute(&ctx).await; + assert!(res.is_err()); + assert_eq!( + res.unwrap_err(), + UseCaseError::NotFound(usecase.calendar_id) + ); + } +} diff --git a/crates/api/src/event_group/mod.rs b/crates/api/src/event_group/mod.rs new file mode 100644 index 00000000..de0fd4c9 --- /dev/null +++ b/crates/api/src/event_group/mod.rs @@ -0,0 +1,13 @@ +mod create_event_group; + +use actix_web::web; +use create_event_group::create_event_group_admin_controller; + +// Configure the routes for the event_group module +pub fn configure_routes(cfg: &mut web::ServiceConfig) { + // Create an event for a user (admin route) + cfg.route( + "/user/{user_id}/event_group", + web::post().to(create_event_group_admin_controller), + ); +} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index d452c480..99384cd6 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -2,6 +2,7 @@ mod account; mod calendar; mod error; mod event; +mod event_group; mod http_logger; mod job_schedulers; mod schedule; @@ -41,6 +42,7 @@ pub fn configure_server_api(cfg: &mut web::ServiceConfig) { account::configure_routes(cfg); calendar::configure_routes(cfg); event::configure_routes(cfg); + event_group::configure_routes(cfg); schedule::configure_routes(cfg); service::configure_routes(cfg); status::configure_routes(cfg); diff --git a/crates/api_structs/src/event_group/api.rs b/crates/api_structs/src/event_group/api.rs new file mode 100644 index 00000000..f33a1116 --- /dev/null +++ b/crates/api_structs/src/event_group/api.rs @@ -0,0 +1,124 @@ +use nittei_domain::{event_group::EventGroup, ID}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use validator::Validate; + +use super::dtos::EventGroupDTO; + +/// Calendar event response object +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct EventGroupResponse { + /// Calendar event retrieved + pub event_group: EventGroupDTO, +} + +impl EventGroupResponse { + pub fn new(event_group: EventGroup) -> Self { + Self { + event_group: EventGroupDTO::new(event_group), + } + } +} + +pub mod create_event_group { + + use super::*; + + #[derive(Serialize, Deserialize)] + pub struct PathParams { + pub user_id: ID, + } + + /// Request body for creating an event + #[derive(Serialize, Deserialize, Validate, TS)] + #[serde(rename_all = "camelCase")] + #[ts(export, rename = "CreateEventGroupRequestBody")] + pub struct RequestBody { + /// UUID of the calendar where the event group will be created + pub calendar_id: ID, + + /// Optional parent event ID + /// This is useful for external applications that need to link Nittei's events to a wider data model (e.g. a project, an order, etc.) + #[serde(default)] + #[ts(optional)] + #[validate(length(min = 1))] + pub parent_id: Option, + + /// Optional external event ID + /// This is useful for external applications that need to link Nittei's events to their own data models + #[serde(default)] + #[ts(optional)] + #[validate(length(min = 1))] + pub external_id: Option, + } + + pub type APIResponse = EventGroupResponse; +} + +pub mod delete_event_group { + use super::*; + + #[derive(Deserialize)] + pub struct PathParams { + pub event_group_id: ID, + } + + pub type APIResponse = EventGroupResponse; +} + +pub mod get_event_group { + use super::*; + + #[derive(Deserialize)] + pub struct PathParams { + pub event_group_id: ID, + } + + pub type APIResponse = EventGroupResponse; +} + +pub mod get_event_group_by_external_id { + use super::*; + + #[derive(Deserialize)] + pub struct PathParams { + pub external_id: String, + } + + pub type APIResponse = EventGroupResponse; +} + +pub mod update_event_group { + + use super::*; + + /// Request body for updating an event + #[derive(Deserialize, Serialize, Validate, TS)] + #[serde(rename_all = "camelCase")] + #[ts(export, rename = "UpdateEventGroupRequestBody")] + pub struct RequestBody { + /// Optional parent event ID + /// This is useful for external applications that need to link Nittei's events to a wider data model (e.g. a project, an order, etc.) + #[serde(default)] + #[ts(optional)] + #[validate(length(min = 1))] + pub parent_id: Option, + + /// Optional external event ID + /// This is useful for external applications that need to link Nittei's events to their own data models + /// Default is None + #[serde(default)] + #[ts(optional)] + #[validate(length(min = 1))] + pub external_id: Option, + } + + #[derive(Deserialize)] + pub struct PathParams { + pub event_group_id: ID, + } + + pub type APIResponse = EventGroupResponse; +} diff --git a/crates/api_structs/src/event_group/dtos.rs b/crates/api_structs/src/event_group/dtos.rs new file mode 100644 index 00000000..84d224a6 --- /dev/null +++ b/crates/api_structs/src/event_group/dtos.rs @@ -0,0 +1,37 @@ +use nittei_domain::{event_group::EventGroup, ID}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +/// Calendar event object +#[derive(Debug, Deserialize, Serialize, Clone, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct EventGroupDTO { + /// UUID of the event + pub id: ID, + + /// Optional parent event ID + pub parent_id: Option, + + /// Optional external ID + pub external_id: Option, + + /// UUID of the calendar + pub calendar_id: ID, + + /// UUID of the user + pub user_id: ID, +} + +impl EventGroupDTO { + /// Create a new EventGroupDTO from an EventGroup + pub fn new(event_group: EventGroup) -> Self { + Self { + id: event_group.id, + parent_id: event_group.parent_id, + external_id: event_group.external_id, + calendar_id: event_group.calendar_id, + user_id: event_group.user_id, + } + } +} diff --git a/crates/api_structs/src/event_group/mod.rs b/crates/api_structs/src/event_group/mod.rs new file mode 100644 index 00000000..1b658d6d --- /dev/null +++ b/crates/api_structs/src/event_group/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod api; +pub(crate) mod dtos; diff --git a/crates/api_structs/src/lib.rs b/crates/api_structs/src/lib.rs index 6bd60b59..bd7b2549 100644 --- a/crates/api_structs/src/lib.rs +++ b/crates/api_structs/src/lib.rs @@ -1,6 +1,7 @@ mod account; mod calendar; mod event; +mod event_group; mod helpers; mod schedule; mod service; @@ -12,6 +13,7 @@ pub mod dtos { account::dtos::*, calendar::dtos::*, event::dtos::*, + event_group::dtos::*, schedule::dtos::*, service::dtos::*, user::dtos::*, @@ -21,6 +23,7 @@ pub use crate::{ account::api::*, calendar::api::*, event::api::*, + event_group::api::*, schedule::api::*, service::api::*, status::api::*, diff --git a/crates/domain/src/event_group.rs b/crates/domain/src/event_group.rs index bde71e5b..05e7c9ae 100644 --- a/crates/domain/src/event_group.rs +++ b/crates/domain/src/event_group.rs @@ -8,11 +8,26 @@ use crate::ID; #[serde(rename_all = "camelCase")] #[ts(export)] pub struct EventGroup { + /// Unique ID pub id: ID, + + /// Calendar ID to which the group belongs pub calendar_id: ID, + + /// User ID pub user_id: ID, + + /// Account ID pub account_id: ID, + + /// Parent ID - this is an ID external to the system + /// It allows to link groups of events together to an outside entity pub parent_id: Option, + + /// External ID - this is an ID external to the system + /// It allows to link a group of events to an outside entity pub external_id: Option, + + /// List of event IDs in the group pub event_ids: Vec, }