Skip to content
Merged
12 changes: 9 additions & 3 deletions crates/cli/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,19 @@ impl Action for UploadCommand {
.into_diagnostic()
.context("failed to send upload request")?;

let json_resp = resp
.json::<serde_json::Value>()
let text_resp = resp
.text()
.await
.into_diagnostic()
.context("failed to read response body")?;

tracing::debug!(body = text_resp, "got upload response");

let json_resp: serde_json::Value = serde_json::from_str(&text_resp)
.into_diagnostic()
.context("failed to deserialize response body as JSON")?;

tracing::debug!(body = ?json_resp, "got upload response");
tracing::debug!(body = ?json_resp, "parsed upload response");

Ok(())
}
Expand Down
21 changes: 14 additions & 7 deletions crates/models/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ pub struct Store {

impl Store {
/// Generates the value of the unique [`Store`] index
/// `name`.
pub fn unique_index_name(&self) -> Vec<EitherSlug> {
vec![self.name.clone().into_inner().into()]
/// `name_by_org`.
pub fn unique_index_name_by_org(&self) -> EitherSlug {
LaxSlug::new(format!(
"{org_id}-{name}",
org_id = self.org,
name = self.name
))
.into()
}
}

Expand Down Expand Up @@ -62,14 +67,14 @@ impl From<Store> for PvStore {
/// The unique index selector for [`Store`].
#[derive(Debug, Clone, Copy)]
pub enum StoreUniqueIndexSelector {
/// The `name` index.
Name,
/// The `name-by-org` index.
NameByOrg,
}

impl fmt::Display for StoreUniqueIndexSelector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StoreUniqueIndexSelector::Name => write!(f, "name"),
StoreUniqueIndexSelector::NameByOrg => write!(f, "name-by-org"),
}
}
}
Expand Down Expand Up @@ -107,7 +112,9 @@ impl Model for Store {
const UNIQUE_INDICES: &'static [(
Self::UniqueIndexSelector,
SlugFieldGetter<Self>,
)] = &[(StoreUniqueIndexSelector::Name, Store::unique_index_name)];
)] = &[(StoreUniqueIndexSelector::NameByOrg, |s| {
vec![Store::unique_index_name_by_org(s)]
})];

fn id(&self) -> dvf::RecordId<Self> { self.id }
}
23 changes: 22 additions & 1 deletion crates/prime-domain/src/create.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use db::CreateModelError;
use models::{
Cache, Org,
Cache, Org, StorageCredentials, Store, StoreConfiguration,
dvf::{EntityName, RecordId, Visibility},
};

Expand All @@ -25,4 +25,25 @@ impl PrimeDomainService {
.await
.map(|c| c.id)
}

/// Creates a [`Store`].
pub async fn create_store(
&self,
org: RecordId<Org>,
name: EntityName,
credentials: StorageCredentials,
config: StoreConfiguration,
) -> Result<RecordId<Store>, CreateModelError> {
self
.store_repo
.create_model(Store {
id: RecordId::new(),
org,
credentials,
config,
name,
})
.await
.map(|s| s.id)
}
}
22 changes: 20 additions & 2 deletions crates/prime-domain/src/fetch_by_name.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use db::FetchModelByIndexError;
use models::{Cache, CacheUniqueIndexSelector, dvf::EntityName};
use db::{FetchModelByIndexError, kv::LaxSlug};
use models::{
Cache, CacheUniqueIndexSelector, Org, Store, StoreUniqueIndexSelector,
dvf::{EntityName, RecordId},
};

use crate::PrimeDomainService;

Expand All @@ -17,4 +20,19 @@ impl PrimeDomainService {
)
.await
}

/// Fetches a [`Store`] by its org and name.
pub async fn fetch_store_by_org_and_name(
&self,
org: RecordId<Org>,
store_name: EntityName,
) -> Result<Option<Store>, FetchModelByIndexError> {
self
.store_repo
.fetch_model_by_unique_index(
StoreUniqueIndexSelector::NameByOrg,
LaxSlug::new(format!("{org}-{store_name}")).into(),
)
.await
}
}
1 change: 1 addition & 0 deletions crates/prime-domain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod fetch_by_name;
mod fetch_by_org;
mod migrate;
pub mod narinfo;
mod search_by_user;
pub mod upload;

pub use belt;
Expand Down
60 changes: 60 additions & 0 deletions crates/prime-domain/src/search_by_user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use db::{FetchModelByIndexError, FetchModelError, kv::LaxSlug};
use models::{
Store, StoreUniqueIndexSelector, User,
dvf::{EntityName, RecordId},
};

use crate::PrimeDomainService;

#[derive(Debug, thiserror::Error)]
pub enum SearchByUserError {
/// Indicates that the user does not exist.
#[error("Failed to find user: {0}")]
MissingUser(RecordId<User>),
/// Indicates that a database error occurred.
#[error("Failed to fetch users by index")]
FetchError(#[from] FetchModelError),
/// Indicates that a database error occurred.
#[error("Failed to fetch users by index")]
FetchByIndexError(#[from] FetchModelByIndexError),
}

impl PrimeDomainService {
/// Find all stores with the given name across all a user's orgs.
pub async fn search_stores_by_name_and_user(
&self,
user_id: RecordId<User>,
store_name: EntityName,
) -> Result<Vec<Store>, SearchByUserError> {
// fetch the user
let user = self
.fetch_user_by_id(user_id)
.await?
.ok_or(SearchByUserError::MissingUser(user_id))?;

// the user's orgs
let user_orgs = user.iter_orgs();

// find the store with the given name in each org
let mut stores = Vec::new();
for org in user_orgs {
// calculate the `NameByOrg` index
let index_value = LaxSlug::new(format!("{org}-{store_name}"));
// find the associated store
let store = self
.store_repo
.fetch_model_by_unique_index(
StoreUniqueIndexSelector::NameByOrg,
index_value.into(),
)
.await?;

// if a store with that name exists, push it
if let Some(store) = store {
stores.push(store);
}
}

Ok(stores)
}
}
52 changes: 38 additions & 14 deletions crates/prime-domain/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ use belt::Belt;
use miette::{Context, IntoDiagnostic, miette};
use models::{
Cache, CacheUniqueIndexSelector, Digest, Entry, EntryUniqueIndexSelector,
NarAuthenticityData, NarDeriverData, NarStorageData, StorePath,
StoreUniqueIndexSelector, User,
NarAuthenticityData, NarDeriverData, NarStorageData, Org, StorePath, User,
dvf::{CompressionStatus, EitherSlug, EntityName, RecordId},
model::Model,
};
use serde::{Deserialize, Serialize};

use crate::PrimeDomainService;
use crate::{PrimeDomainService, search_by_user::SearchByUserError};

/// The request struct for the [`upload`](PrimeDomainService::upload) fn.
#[derive(Debug)]
Expand Down Expand Up @@ -51,6 +50,12 @@ pub enum UploadError {
/// The target store was not found.
#[error("The target store was not found: \"{0}\"")]
TargetStoreNotFound(EntityName),
/// Multiple stores were found with the given name in different organizations.
#[error(
"The target store name \"{1}\" is ambiguous: multiple results found in \
orgs {0:?}"
)]
TargetStoreAmbiguous(Vec<RecordId<Org>>, EntityName),
/// An entry with that path already exists in the target store.
#[error("An entry with that path already exists in the target store: {0}")]
DuplicateEntryInStore(RecordId<Entry>),
Expand Down Expand Up @@ -98,18 +103,37 @@ impl PrimeDomainService {
.ok_or(miette!("authenticated user not found"))
.map_err(UploadError::InternalError)?;

// find the given store
let target_store = self
.store_repo
.fetch_model_by_unique_index(
StoreUniqueIndexSelector::Name,
EitherSlug::Strict(req.target_store.clone().into_inner()),
)
// find the stores the user could be referring to
let possible_stores = self
.search_stores_by_name_and_user(user.id, req.target_store.clone())
.await
.into_diagnostic()
.context("failed to search for target store")
.map_err(UploadError::InternalError)?
.ok_or(UploadError::TargetStoreNotFound(req.target_store))?;
.map_err(|e| match e {
SearchByUserError::MissingUser(u) => {
unreachable!("user {u} was already fetched")
}
SearchByUserError::FetchError(e) => UploadError::InternalError(
Err::<(), _>(e)
.into_diagnostic()
.context("failed to search for stores by user")
.unwrap_err(),
),
SearchByUserError::FetchByIndexError(e) => UploadError::InternalError(
Err::<(), _>(e)
.into_diagnostic()
.context("failed to search for stores by user")
.unwrap_err(),
),
})?;

// make sure there's only one
let target_store = match possible_stores.len() {
0 => Err(UploadError::TargetStoreNotFound(req.target_store)),
1 => Ok(possible_stores.first().unwrap().clone()),
_ => Err(UploadError::TargetStoreAmbiguous(
possible_stores.iter().map(|s| s.org).collect(),
req.target_store,
)),
}?;

// org is assigned by the store
let org = target_store.org;
Expand Down
14 changes: 14 additions & 0 deletions crates/site-app/src/components/create_button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,17 @@ pub fn CreateCacheButton(text: &'static str) -> impl IntoView {
</a>
}
}

#[component]
pub fn CreateStoreButton(text: &'static str) -> impl IntoView {
let org_hook = OrgHook::new_requested();
let base_url = org_hook.base_url();
let href =
Signal::derive(move || format!("{base}/create_store", base = base_url()));

view! {
<a href=href class="btn btn-primary-subtle">
{ text }
</a>
}
}
6 changes: 3 additions & 3 deletions crates/site-app/src/components/input_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ pub fn InputField(
const WARN_HINT_CLASS: &str = "text-warn-11 text-sm";

view! {
<div class=OUTER_WRAPPER_CLASS>
<label class=LABEL_CLASS for=id>{ label_text }</label>
<label for=id class=OUTER_WRAPPER_CLASS>
<p class=LABEL_CLASS>{ label_text }</p>
<div class=INPUT_WRAPPER_CLASS>
{ move || before.map(|i| i.into_any()).unwrap_or(().into_any()) }
<input
Expand All @@ -76,6 +76,6 @@ pub fn InputField(
<p class=WARN_HINT_CLASS>{ e }</p>
})}
</div>
</div>
</label>
}
}
6 changes: 3 additions & 3 deletions crates/site-app/src/components/navbar/org_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ use crate::{

#[island]
pub(super) fn OrgSelectorPopover(user: AuthUser) -> impl IntoView {
const CONTAINER_CLASS: &str = "hover:bg-base-3 active:bg-base-4 \
const CONTAINER_CLASS: &str = "transition hover:bg-base-3 active:bg-base-4 \
cursor-pointer px-2 py-1 rounded flex \
flex-col gap leading-none items-end gap-0.5";
flex-col gap leading-none items-end gap-0";

let active_org_hook = OrgHook::new_active();
let active_org_descriptor = active_org_hook.descriptor();
Expand Down Expand Up @@ -44,7 +44,7 @@ pub(super) fn OrgSelectorPopover(user: AuthUser) -> impl IntoView {
view! {
<div class="relative">
<div class=CONTAINER_CLASS on:click=toggle>
<span class="text-base-12">{ user.name.to_string() }</span>
<span class="text-base-12 text-sm">{ user.name.to_string() }</span>
<div class="flex flex-row items-center gap-0.5">
<span class="text-sm">
<Suspense fallback=|| "[loading]">
Expand Down
1 change: 1 addition & 0 deletions crates/site-app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub fn App() -> impl IntoView {
<Route path=path!("/org/:org/dash") view=protect_by_org(DashboardPage) />
<Route path=path!("/org/:org/entry/:entry") view=protect_by_org(EntryPage) />
<Route path=path!("/org/:org/create_cache") view=protect_by_org(CreateCachePage) />
<Route path=path!("/org/:org/create_store") view=protect_by_org(CreateStorePage) />
<Route path=path!("/auth/signup") view=SignupPage/>
<Route path=path!("/auth/login") view=LoginPage/>
<Route path=path!("/auth/logout") view=LogoutPage/>
Expand Down
5 changes: 3 additions & 2 deletions crates/site-app/src/pages.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod create_cache;
mod create_store;
mod dashboard;
mod entry;
mod homepage;
Expand All @@ -9,6 +10,6 @@ mod signup;
mod unauthorized;

pub use self::{
create_cache::*, dashboard::*, entry::*, homepage::*, login::*, logout::*,
protected::*, signup::*, unauthorized::*,
create_cache::*, create_store::*, dashboard::*, entry::*, homepage::*,
login::*, logout::*, protected::*, signup::*, unauthorized::*,
};
Loading