diff --git a/backend/src/api/model/realm/mod.rs b/backend/src/api/model/realm/mod.rs index 1d3a3307d..2e28a1b2c 100644 --- a/backend/src/api/model/realm/mod.rs +++ b/backend/src/api/model/realm/mod.rs @@ -20,7 +20,8 @@ use super::block::{Block, BlockValue, PlaylistBlock, SeriesBlock, VideoBlock}; mod mutations; pub(crate) use mutations::{ - ChildIndex, NewRealm, RemovedRealm, UpdateRealm, UpdatedPermissions, UpdatedRealmName, RealmSpecifier, + ChildIndex, NewRealm, RemovedRealm, UpdateRealm, UpdatedPermissions, + UpdatedRealmName, RealmSpecifier, RealmLineageComponent, CreateRealmLineageOutcome, }; diff --git a/backend/src/api/model/realm/mutations.rs b/backend/src/api/model/realm/mutations.rs index 39508106e..a32bf62a7 100644 --- a/backend/src/api/model/realm/mutations.rs +++ b/backend/src/api/model/realm/mutations.rs @@ -394,8 +394,19 @@ pub(crate) struct RealmSpecifier { pub(crate) path_segment: String, } +#[derive(Clone, juniper::GraphQLInputObject)] +pub(crate) struct RealmLineageComponent { + pub(crate) name: String, + pub(crate) path_segment: String, +} + #[derive(juniper::GraphQLObject)] #[graphql(Context = Context)] pub(crate) struct RemovedRealm { parent: Option, } + +#[derive(juniper::GraphQLObject)] +pub struct CreateRealmLineageOutcome { + pub num_created: i32, +} diff --git a/backend/src/api/mutation.rs b/backend/src/api/mutation.rs index 33bf2abac..fd926f934 100644 --- a/backend/src/api/mutation.rs +++ b/backend/src/api/mutation.rs @@ -1,7 +1,7 @@ use juniper::graphql_object; use crate::{ - api::model::event::RemovedEvent, + api::{err::map_db_err, model::event::RemovedEvent}, auth::AuthContext, }; use super::{ @@ -21,6 +21,8 @@ use super::{ UpdatedRealmName, UpdateRealm, RealmSpecifier, + RealmLineageComponent, + CreateRealmLineageOutcome, }, block::{ BlockValue, @@ -232,6 +234,47 @@ impl Mutation { BlockValue::remove(id, context).await } + /// Basically `mkdir -p` for realms: makes sure the given realm lineage + /// exists, creating the missing realms. Existing realms are *not* updated. + /// Each realm in the given list is the sub-realm of the previous item in + /// the list. The first item is sub-realm of the root realm. + async fn create_realm_lineage( + realms: Vec, + context: &Context, + ) -> ApiResult { + if context.auth != AuthContext::TrustedExternal { + return Err(not_authorized!("only trusted external applications can use this mutation")); + } + + if realms.len() == 0 { + return Ok(CreateRealmLineageOutcome { num_created: 0 }); + } + + if context.config.general.reserved_paths().any(|r| realms[0].path_segment == r) { + return Err(invalid_input!(key = "realm.path-is-reserved", "path is reserved and cannot be used")); + } + + let mut parent_path = String::new(); + let mut num_created = 0; + for realm in realms { + let sql = "\ + insert into realms (parent, name, path_segment) \ + values ((select id from realms where full_path = $1), $2, $3) \ + on conflict do nothing"; + let res = context.db.execute(sql, &[&parent_path, &realm.name, &realm.path_segment]) + .await; + let affected = map_db_err!(res, { + if constraint == "valid_path" => invalid_input!("path invalid"), + })?; + num_created += affected as i32; + + parent_path.push('/'); + parent_path.push_str(&realm.path_segment); + } + + Ok(CreateRealmLineageOutcome { num_created }) + } + /// Atomically mount a series into an (empty) realm. /// Creates all the necessary realms on the path to the target /// and adds a block with the given series at the leaf. diff --git a/frontend/src/schema.graphql b/frontend/src/schema.graphql index 34f78c80a..422a33395 100644 --- a/frontend/src/schema.graphql +++ b/frontend/src/schema.graphql @@ -229,6 +229,10 @@ input NewVideoBlock { showLink: Boolean! } +type CreateRealmLineageOutcome { + numCreated: Int! +} + "A `Block`: a UI element that belongs to a realm." interface Block { id: ID! @@ -586,6 +590,13 @@ type Mutation { updateVideoBlock(id: ID!, set: UpdateVideoBlock!): Block! "Remove a block from a realm." removeBlock(id: ID!): RemovedBlock! + """ + Basically `mkdir -p` for realms: makes sure the given realm lineage + exists, creating the missing realms. Existing realms are *not* updated. + Each realm in the given list is the sub-realm of the previous item in + the list. The first item is sub-realm of the root realm. + """ + createRealmLineage(realms: [RealmLineageComponent!]!): CreateRealmLineageOutcome! """ Atomically mount a series into an (empty) realm. Creates all the necessary realms on the path to the target @@ -765,6 +776,11 @@ enum ItemType { REALM } +input RealmLineageComponent { + name: String! + pathSegment: String! +} + enum EventSortColumn { TITLE CREATED