feat: Add DatasetToDatasetGroup model and update schema

- Introduced a new `DatasetToDatasetGroup` struct to represent the relationship between datasets and dataset groups, including fields for timestamps and optional deletion.
- Updated the database schema to include `updated_at` and `deleted_at` fields for the `datasets_to_dataset_groups` table, enhancing data tracking capabilities.
- Refactored the routing in `mod.rs` to include a nested router for assets, improving the organization of dataset group routes.
This commit is contained in:
dal 2025-01-21 11:53:47 -07:00
parent 395b1773e0
commit 71c234aa4b
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
12 changed files with 702 additions and 10 deletions

View File

@ -0,0 +1,4 @@
-- This file should undo anything in `up.sql`
ALTER TABLE datasets_to_dataset_groups
DROP COLUMN deleted_at,
DROP COLUMN updated_at;

View File

@ -0,0 +1,4 @@
-- Your SQL goes here
ALTER TABLE datasets_to_dataset_groups
ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ADD COLUMN deleted_at TIMESTAMPTZ;

View File

@ -558,3 +558,15 @@ pub struct DatasetGroupPermission {
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>, pub deleted_at: Option<DateTime<Utc>>,
} }
#[derive(Queryable, Insertable, Associations, Debug)]
#[diesel(belongs_to(Dataset, foreign_key = dataset_id))]
#[diesel(belongs_to(DatasetGroup, foreign_key = dataset_group_id))]
#[diesel(table_name = datasets_to_dataset_groups)]
pub struct DatasetToDatasetGroup {
pub dataset_id: Uuid,
pub dataset_group_id: Uuid,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
}

View File

@ -260,6 +260,8 @@ diesel::table! {
dataset_id -> Uuid, dataset_id -> Uuid,
dataset_group_id -> Uuid, dataset_group_id -> Uuid,
created_at -> Timestamptz, created_at -> Timestamptz,
updated_at -> Timestamptz,
deleted_at -> Nullable<Timestamptz>,
} }
} }

View File

@ -0,0 +1,86 @@
use anyhow::Result;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::Extension;
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use serde::Serialize;
use uuid::Uuid;
use crate::database::lib::get_pg_pool;
use crate::database::models::User;
use crate::database::schema::{datasets, datasets_to_dataset_groups};
use crate::routes::rest::ApiResponse;
use crate::utils::security::checks::is_user_workspace_admin_or_data_admin;
use crate::utils::user::user_info::get_user_organization_id;
/// Represents dataset information with its assignment status to a dataset group
#[derive(Debug, Serialize)]
pub struct DatasetInfo {
pub id: Uuid,
pub name: String,
pub assigned: bool,
}
/// List datasets that can be associated with a dataset group
/// Returns datasets with their current assignment status to the specified dataset group
pub async fn list_datasets(
Extension(user): Extension<User>,
Path(dataset_group_id): Path<Uuid>,
) -> Result<ApiResponse<Vec<DatasetInfo>>, (StatusCode, &'static str)> {
let datasets = match list_datasets_handler(user, dataset_group_id).await {
Ok(datasets) => datasets,
Err(e) => {
tracing::error!("Error listing datasets for dataset group: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Error listing datasets for dataset group",
));
}
};
Ok(ApiResponse::JsonData(datasets))
}
async fn list_datasets_handler(user: User, dataset_group_id: Uuid) -> Result<Vec<DatasetInfo>> {
let mut conn = get_pg_pool().get().await?;
let organization_id = get_user_organization_id(&user.id).await?;
if !is_user_workspace_admin_or_data_admin(&user, &organization_id).await? {
return Err(anyhow::anyhow!(
"User is not authorized to list datasets for dataset group"
));
}
// Query datasets and their assignment status to the dataset group
let datasets = datasets::table
.left_join(
datasets_to_dataset_groups::table.on(
datasets_to_dataset_groups::dataset_id
.eq(datasets::id)
.and(datasets_to_dataset_groups::dataset_group_id.eq(dataset_group_id))
.and(datasets_to_dataset_groups::deleted_at.is_null()),
),
)
.select((
datasets::id,
datasets::name,
diesel::dsl::sql::<diesel::sql_types::Bool>(
"datasets_to_dataset_groups.dataset_id IS NOT NULL",
),
))
.filter(datasets::organization_id.eq(organization_id))
.filter(datasets::deleted_at.is_null())
.order_by(datasets::created_at.desc())
.load::<(Uuid, String, bool)>(&mut *conn)
.await?;
Ok(datasets
.into_iter()
.map(|(id, name, assigned)| DatasetInfo {
id,
name,
assigned,
})
.collect())
}

View File

@ -0,0 +1,112 @@
use anyhow::Result;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::Extension;
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use serde::Serialize;
use uuid::Uuid;
use crate::database::enums::IdentityType;
use crate::database::lib::get_pg_pool;
use crate::database::models::User;
use crate::database::schema::{
dataset_groups_permissions, permission_groups, permission_groups_to_identities,
};
use crate::routes::rest::ApiResponse;
use crate::utils::security::checks::is_user_workspace_admin_or_data_admin;
use crate::utils::user::user_info::get_user_organization_id;
/// Represents permission group information with its assignment status to a dataset group
#[derive(Debug, Serialize)]
pub struct PermissionGroupInfo {
pub id: Uuid,
pub name: String,
pub user_count: i64,
pub assigned: bool,
}
/// List permission groups that can be associated with a dataset group
/// Returns permission groups with their current assignment status to the specified dataset group
/// and the count of users in each permission group
pub async fn list_permission_groups(
Extension(user): Extension<User>,
Path(dataset_group_id): Path<Uuid>,
) -> Result<ApiResponse<Vec<PermissionGroupInfo>>, (StatusCode, &'static str)> {
let permission_groups = match list_permission_groups_handler(user, dataset_group_id).await {
Ok(groups) => groups,
Err(e) => {
tracing::error!("Error listing permission groups for dataset group: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Error listing permission groups for dataset group",
));
}
};
Ok(ApiResponse::JsonData(permission_groups))
}
async fn list_permission_groups_handler(
user: User,
dataset_group_id: Uuid,
) -> Result<Vec<PermissionGroupInfo>> {
let mut conn = get_pg_pool().get().await?;
let organization_id = get_user_organization_id(&user.id).await?;
if !is_user_workspace_admin_or_data_admin(&user, &organization_id).await? {
return Err(anyhow::anyhow!(
"User is not authorized to list permission groups for dataset group"
));
}
// Query permission groups with their user count and assignment status
let groups = permission_groups::table
.left_join(
dataset_groups_permissions::table.on(
dataset_groups_permissions::permission_id
.eq(permission_groups::id)
.and(dataset_groups_permissions::dataset_group_id.eq(dataset_group_id))
.and(dataset_groups_permissions::permission_type.eq("permission_group"))
.and(dataset_groups_permissions::deleted_at.is_null()),
),
)
.left_join(
permission_groups_to_identities::table.on(
permission_groups_to_identities::permission_group_id
.eq(permission_groups::id)
.and(permission_groups_to_identities::identity_type.eq(IdentityType::User))
.and(permission_groups_to_identities::deleted_at.is_null()),
),
)
.select((
permission_groups::id,
permission_groups::name,
diesel::dsl::sql::<diesel::sql_types::BigInt>(
"COALESCE(COUNT(DISTINCT permission_groups_to_identities.identity_id), 0)",
),
diesel::dsl::sql::<diesel::sql_types::Bool>(
"dataset_groups_permissions.id IS NOT NULL",
),
))
.group_by((
permission_groups::id,
permission_groups::name,
dataset_groups_permissions::id,
))
.filter(permission_groups::organization_id.eq(organization_id))
.filter(permission_groups::deleted_at.is_null())
.order_by(permission_groups::created_at.desc())
.load::<(Uuid, String, i64, bool)>(&mut *conn)
.await?;
Ok(groups
.into_iter()
.map(|(id, name, user_count, assigned)| PermissionGroupInfo {
id,
name,
user_count,
assigned,
})
.collect())
}

View File

@ -0,0 +1,94 @@
use anyhow::Result;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::Extension;
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use serde::Serialize;
use uuid::Uuid;
use crate::database::lib::get_pg_pool;
use crate::database::models::User;
use crate::database::schema::{dataset_groups_permissions, users, users_to_organizations};
use crate::routes::rest::ApiResponse;
use crate::utils::security::checks::is_user_workspace_admin_or_data_admin;
use crate::utils::user::user_info::get_user_organization_id;
/// Represents user information with their assignment status to a dataset group
#[derive(Debug, Serialize)]
pub struct UserInfo {
pub id: Uuid,
pub name: String,
pub email: String,
pub assigned: bool,
}
/// List users that can be associated with a dataset group
/// Returns users with their current assignment status to the specified dataset group
pub async fn list_users(
Extension(user): Extension<User>,
Path(dataset_group_id): Path<Uuid>,
) -> Result<ApiResponse<Vec<UserInfo>>, (StatusCode, &'static str)> {
let users = match list_users_handler(user, dataset_group_id).await {
Ok(users) => users,
Err(e) => {
tracing::error!("Error listing users for dataset group: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Error listing users for dataset group",
));
}
};
Ok(ApiResponse::JsonData(users))
}
async fn list_users_handler(user: User, dataset_group_id: Uuid) -> Result<Vec<UserInfo>> {
let mut conn = get_pg_pool().get().await?;
let organization_id = get_user_organization_id(&user.id).await?;
if !is_user_workspace_admin_or_data_admin(&user, &organization_id).await? {
return Err(anyhow::anyhow!(
"User is not authorized to list users for dataset group"
));
}
// Query users and their assignment status to the dataset group
let users = users::table
.left_join(
dataset_groups_permissions::table.on(
dataset_groups_permissions::permission_id
.eq(users::id)
.and(dataset_groups_permissions::dataset_group_id.eq(dataset_group_id))
.and(dataset_groups_permissions::permission_type.eq("user"))
.and(dataset_groups_permissions::deleted_at.is_null()),
),
)
.inner_join(
users_to_organizations::table.on(users_to_organizations::user_id
.eq(users::id)
.and(users_to_organizations::organization_id.eq(organization_id))
.and(users_to_organizations::deleted_at.is_null())),
)
.select((
users::id,
users::name.nullable(),
users::email,
diesel::dsl::sql::<diesel::sql_types::Bool>(
"dataset_groups_permissions.id IS NOT NULL",
),
))
.order_by(users::created_at.desc())
.load::<(Uuid, Option<String>, String, bool)>(&mut *conn)
.await?;
Ok(users
.into_iter()
.map(|(id, name, email, assigned)| UserInfo {
id,
name: name.unwrap_or("".to_string()),
email,
assigned,
})
.collect())
}

View File

@ -0,0 +1,25 @@
use axum::{routing::get, Router};
mod list_datasets;
mod list_permission_groups;
mod list_users;
mod put_datasets;
mod put_permission_groups;
mod put_users;
pub use list_datasets::list_datasets;
pub use list_permission_groups::list_permission_groups;
pub use list_users::list_users;
pub use put_datasets::put_datasets;
pub use put_permission_groups::put_permission_groups;
pub use put_users::put_users;
pub fn router() -> Router {
Router::new()
.route("/datasets", get(list_datasets).put(put_datasets))
.route(
"/permission_groups",
get(list_permission_groups).put(put_permission_groups),
)
.route("/users", get(list_users).put(put_users))
}

View File

@ -0,0 +1,113 @@
use anyhow::Result;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::{Extension, Json};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize};
use tokio::spawn;
use uuid::Uuid;
use crate::database::lib::get_pg_pool;
use crate::database::models::{DatasetToDatasetGroup, User};
use crate::database::schema::datasets_to_dataset_groups;
use crate::routes::rest::ApiResponse;
use crate::utils::security::checks::is_user_workspace_admin_or_data_admin;
use crate::utils::user::user_info::get_user_organization_id;
#[derive(Debug, Serialize, Deserialize)]
pub struct DatasetAssignment {
pub id: Uuid,
pub assigned: bool,
}
/// Update dataset assignments for a dataset group
/// Accepts a list of dataset assignments to add or remove from the dataset group
pub async fn put_datasets(
Extension(user): Extension<User>,
Path(dataset_group_id): Path<Uuid>,
Json(assignments): Json<Vec<DatasetAssignment>>,
) -> Result<ApiResponse<()>, (StatusCode, &'static str)> {
match put_datasets_handler(user, dataset_group_id, assignments).await {
Ok(_) => Ok(ApiResponse::NoContent),
Err(e) => {
tracing::error!("Error assigning datasets to dataset group: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Error assigning datasets to dataset group",
));
}
}
}
async fn put_datasets_handler(
user: User,
dataset_group_id: Uuid,
assignments: Vec<DatasetAssignment>,
) -> Result<()> {
let organization_id = get_user_organization_id(&user.id).await?;
if !is_user_workspace_admin_or_data_admin(&user, &organization_id).await? {
return Err(anyhow::anyhow!(
"User is not authorized to assign datasets to dataset group"
));
}
let (to_assign, to_unassign): (Vec<_>, Vec<_>) = assignments.into_iter().partition(|a| a.assigned);
let assign_handle = {
let dataset_group_id = dataset_group_id;
spawn(async move {
if !to_assign.is_empty() {
let mut conn = get_pg_pool().get().await?;
let values: Vec<_> = to_assign
.into_iter()
.map(|dataset| DatasetToDatasetGroup {
dataset_id: dataset.id,
dataset_group_id,
deleted_at: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
})
.collect();
diesel::insert_into(datasets_to_dataset_groups::table)
.values(&values)
.on_conflict((
datasets_to_dataset_groups::dataset_id,
datasets_to_dataset_groups::dataset_group_id,
))
.do_update()
.set(datasets_to_dataset_groups::deleted_at.eq(None::<chrono::DateTime<chrono::Utc>>))
.execute(&mut *conn)
.await?;
}
Ok::<_, anyhow::Error>(())
})
};
let unassign_handle = {
let dataset_group_id = dataset_group_id;
spawn(async move {
if !to_unassign.is_empty() {
let mut conn = get_pg_pool().get().await?;
diesel::update(datasets_to_dataset_groups::table)
.filter(
datasets_to_dataset_groups::dataset_id
.eq_any(to_unassign.iter().map(|a| a.id))
.and(datasets_to_dataset_groups::dataset_group_id.eq(dataset_group_id)),
)
.set(datasets_to_dataset_groups::deleted_at.eq(chrono::Utc::now()))
.execute(&mut *conn)
.await?;
}
Ok::<_, anyhow::Error>(())
})
};
let (assign_result, unassign_result) = tokio::try_join!(assign_handle, unassign_handle)?;
assign_result?;
unassign_result?;
Ok(())
}

View File

@ -0,0 +1,120 @@
use anyhow::Result;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::{Extension, Json};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize};
use tokio::spawn;
use uuid::Uuid;
use crate::database::lib::get_pg_pool;
use crate::database::models::{DatasetGroupPermission, User};
use crate::database::schema::dataset_groups_permissions;
use crate::routes::rest::ApiResponse;
use crate::utils::security::checks::is_user_workspace_admin_or_data_admin;
use crate::utils::user::user_info::get_user_organization_id;
#[derive(Debug, Serialize, Deserialize)]
pub struct PermissionGroupAssignment {
pub id: Uuid,
pub assigned: bool,
}
/// Update permission group assignments for a dataset group
/// Accepts a list of permission group assignments to add or remove from the dataset group
pub async fn put_permission_groups(
Extension(user): Extension<User>,
Path(dataset_group_id): Path<Uuid>,
Json(assignments): Json<Vec<PermissionGroupAssignment>>,
) -> Result<ApiResponse<()>, (StatusCode, &'static str)> {
match put_permission_groups_handler(user, dataset_group_id, assignments).await {
Ok(_) => Ok(ApiResponse::NoContent),
Err(e) => {
tracing::error!("Error assigning permission groups to dataset group: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Error assigning permission groups to dataset group",
));
}
}
}
async fn put_permission_groups_handler(
user: User,
dataset_group_id: Uuid,
assignments: Vec<PermissionGroupAssignment>,
) -> Result<()> {
let organization_id = get_user_organization_id(&user.id).await?;
if !is_user_workspace_admin_or_data_admin(&user, &organization_id).await? {
return Err(anyhow::anyhow!(
"User is not authorized to assign permission groups to dataset group"
));
}
let (to_assign, to_unassign): (Vec<_>, Vec<_>) = assignments.into_iter().partition(|a| a.assigned);
let assign_handle = {
let dataset_group_id = dataset_group_id;
spawn(async move {
if !to_assign.is_empty() {
let mut conn = get_pg_pool().get().await?;
let values: Vec<_> = to_assign
.into_iter()
.map(|group| DatasetGroupPermission {
id: Uuid::new_v4(),
dataset_group_id,
permission_id: group.id,
permission_type: "permission_group".to_string(),
organization_id,
deleted_at: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
})
.collect();
diesel::insert_into(dataset_groups_permissions::table)
.values(&values)
.on_conflict((
dataset_groups_permissions::dataset_group_id,
dataset_groups_permissions::permission_id,
dataset_groups_permissions::permission_type,
))
.do_update()
.set(dataset_groups_permissions::deleted_at.eq(None::<chrono::DateTime<chrono::Utc>>))
.execute(&mut *conn)
.await?;
}
Ok::<_, anyhow::Error>(())
})
};
let unassign_handle = {
let dataset_group_id = dataset_group_id;
spawn(async move {
if !to_unassign.is_empty() {
let mut conn = get_pg_pool().get().await?;
diesel::update(dataset_groups_permissions::table)
.filter(
dataset_groups_permissions::dataset_group_id
.eq(dataset_group_id)
.and(dataset_groups_permissions::permission_id.eq_any(
to_unassign.iter().map(|a| a.id),
))
.and(dataset_groups_permissions::permission_type.eq("permission_group")),
)
.set(dataset_groups_permissions::deleted_at.eq(chrono::Utc::now()))
.execute(&mut *conn)
.await?;
}
Ok::<_, anyhow::Error>(())
})
};
let (assign_result, unassign_result) = tokio::try_join!(assign_handle, unassign_handle)?;
assign_result?;
unassign_result?;
Ok(())
}

View File

@ -0,0 +1,120 @@
use anyhow::Result;
use axum::extract::Path;
use axum::http::StatusCode;
use axum::{Extension, Json};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize};
use tokio::spawn;
use uuid::Uuid;
use crate::database::lib::get_pg_pool;
use crate::database::models::{DatasetGroupPermission, User};
use crate::database::schema::dataset_groups_permissions;
use crate::routes::rest::ApiResponse;
use crate::utils::security::checks::is_user_workspace_admin_or_data_admin;
use crate::utils::user::user_info::get_user_organization_id;
#[derive(Debug, Serialize, Deserialize)]
pub struct UserAssignment {
pub id: Uuid,
pub assigned: bool,
}
/// Update user assignments for a dataset group
/// Accepts a list of user assignments to add or remove from the dataset group
pub async fn put_users(
Extension(user): Extension<User>,
Path(dataset_group_id): Path<Uuid>,
Json(assignments): Json<Vec<UserAssignment>>,
) -> Result<ApiResponse<()>, (StatusCode, &'static str)> {
match put_users_handler(user, dataset_group_id, assignments).await {
Ok(_) => Ok(ApiResponse::NoContent),
Err(e) => {
tracing::error!("Error assigning users to dataset group: {:?}", e);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Error assigning users to dataset group",
));
}
}
}
async fn put_users_handler(
user: User,
dataset_group_id: Uuid,
assignments: Vec<UserAssignment>,
) -> Result<()> {
let organization_id = get_user_organization_id(&user.id).await?;
if !is_user_workspace_admin_or_data_admin(&user, &organization_id).await? {
return Err(anyhow::anyhow!(
"User is not authorized to assign users to dataset group"
));
}
let (to_assign, to_unassign): (Vec<_>, Vec<_>) = assignments.into_iter().partition(|a| a.assigned);
let assign_handle = {
let dataset_group_id = dataset_group_id;
spawn(async move {
if !to_assign.is_empty() {
let mut conn = get_pg_pool().get().await?;
let values: Vec<_> = to_assign
.into_iter()
.map(|user_assignment| DatasetGroupPermission {
id: Uuid::new_v4(),
dataset_group_id,
permission_id: user_assignment.id,
permission_type: "user".to_string(),
organization_id,
deleted_at: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
})
.collect();
diesel::insert_into(dataset_groups_permissions::table)
.values(&values)
.on_conflict((
dataset_groups_permissions::dataset_group_id,
dataset_groups_permissions::permission_id,
dataset_groups_permissions::permission_type,
))
.do_update()
.set(dataset_groups_permissions::deleted_at.eq(None::<chrono::DateTime<chrono::Utc>>))
.execute(&mut *conn)
.await?;
}
Ok::<_, anyhow::Error>(())
})
};
let unassign_handle = {
let dataset_group_id = dataset_group_id;
spawn(async move {
if !to_unassign.is_empty() {
let mut conn = get_pg_pool().get().await?;
diesel::update(dataset_groups_permissions::table)
.filter(
dataset_groups_permissions::dataset_group_id
.eq(dataset_group_id)
.and(dataset_groups_permissions::permission_id.eq_any(
to_unassign.iter().map(|a| a.id),
))
.and(dataset_groups_permissions::permission_type.eq("user")),
)
.set(dataset_groups_permissions::deleted_at.eq(chrono::Utc::now()))
.execute(&mut *conn)
.await?;
}
Ok::<_, anyhow::Error>(())
})
};
let (assign_result, unassign_result) = tokio::try_join!(assign_handle, unassign_handle)?;
assign_result?;
unassign_result?;
Ok(())
}

View File

@ -1,8 +1,9 @@
pub mod delete_dataset_group; mod assets;
pub mod get_dataset_group; mod delete_dataset_group;
pub mod list_dataset_groups; mod get_dataset_group;
pub mod post_dataset_group; mod list_dataset_groups;
pub mod put_dataset_group; mod post_dataset_group;
mod put_dataset_group;
use axum::{ use axum::{
middleware, middleware,
@ -13,10 +14,8 @@ use axum::{
use crate::buster_middleware::auth::auth; use crate::buster_middleware::auth::auth;
use self::{ use self::{
delete_dataset_group::delete_dataset_group, delete_dataset_group::delete_dataset_group, get_dataset_group::get_dataset_group,
get_dataset_group::get_dataset_group, list_dataset_groups::list_dataset_groups, post_dataset_group::post_dataset_group,
list_dataset_groups::list_dataset_groups,
post_dataset_group::post_dataset_group,
put_dataset_group::put_dataset_group, put_dataset_group::put_dataset_group,
}; };
@ -27,5 +26,6 @@ pub fn router() -> Router {
.route("/:dataset_group_id", get(get_dataset_group)) .route("/:dataset_group_id", get(get_dataset_group))
.route("/:dataset_group_id", delete(delete_dataset_group)) .route("/:dataset_group_id", delete(delete_dataset_group))
.route("/", put(put_dataset_group)) .route("/", put(put_dataset_group))
.nest("/:dataset_group_id", assets::router())
.route_layer(middleware::from_fn(auth)) .route_layer(middleware::from_fn(auth))
} }