From 9798996cccc9be23efe356961d42adb1b2e95b26 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Wed, 12 Jun 2024 13:50:32 +0200 Subject: [PATCH] Add `createRealmLineage` mutation to API This is supposed to make mounting series easier from external scripts. The existing `mountSeries` API is (a) doing a lot of different things and (b) requires the caller to distinguish between what realms already exist and which still need to be created. This new API is idempotent and more convenient. With this, it would be possible to simplify `mountSeries`, but we can't simply break that API since the Admin UI is using it. So we keep it for now. We might add an alternative API in the future. --- backend/src/api/model/realm/mod.rs | 3 +- backend/src/api/model/realm/mutations.rs | 11 ++++++ backend/src/api/mutation.rs | 45 +++++++++++++++++++++++- frontend/src/schema.graphql | 16 +++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/backend/src/api/model/realm/mod.rs b/backend/src/api/model/realm/mod.rs index c5b17fc79..49790ace4 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, 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 ca7bf4e3b..340e81d01 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, @@ -209,6 +211,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 e7fe065ea..9391240f8 100644 --- a/frontend/src/schema.graphql +++ b/frontend/src/schema.graphql @@ -216,6 +216,10 @@ input NewVideoBlock { showLink: Boolean! } +type CreateRealmLineageOutcome { + numCreated: Int! +} + "A `Block`: a UI element that belongs to a realm." interface Block { id: ID! @@ -519,6 +523,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 @@ -677,6 +688,11 @@ enum ItemType { REALM } +input RealmLineageComponent { + name: String! + pathSegment: String! +} + enum EventSortColumn { TITLE CREATED