From 5082831630fddfd16b362999658d5f45ec45fd63 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:17:59 +0000 Subject: [PATCH 1/5] Add consistent ordering to individual_permissions queries - Add .order_by(users::email) to individual_permissions_query in dashboard handler - Add .order_by(users::email) to individual_permissions_query in metrics handler - Add .order_by(users::email) to individual_permissions_query in collections handler - Ensures consistent alphabetical ordering by email across all endpoints Fixes BUS-1477 Co-Authored-By: nate@buster.so --- apps/api/libs/handlers/src/collections/get_collection_handler.rs | 1 + apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs | 1 + apps/api/libs/handlers/src/metrics/get_metric_handler.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/apps/api/libs/handlers/src/collections/get_collection_handler.rs b/apps/api/libs/handlers/src/collections/get_collection_handler.rs index f8382661e..2a281543b 100644 --- a/apps/api/libs/handlers/src/collections/get_collection_handler.rs +++ b/apps/api/libs/handlers/src/collections/get_collection_handler.rs @@ -133,6 +133,7 @@ pub async fn get_collection_handler( users::name, users::avatar_url, )) + .order_by(users::email) .load::(&mut conn) .await; diff --git a/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs b/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs index c83508a1d..498366a35 100644 --- a/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs +++ b/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs @@ -346,6 +346,7 @@ pub async fn get_dashboard_handler( .filter(asset_permissions::identity_type.eq(IdentityType::User)) .filter(asset_permissions::deleted_at.is_null()) .select((asset_permissions::role, users::email, users::name, users::avatar_url)) + .order_by(users::email) .load::(&mut conn) .await; diff --git a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs index 40b4d75e1..05e3c211b 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs @@ -399,6 +399,7 @@ pub async fn get_metric_handler( .filter(asset_permissions::identity_type.eq(IdentityType::User)) .filter(asset_permissions::deleted_at.is_null()) .select((asset_permissions::role, users::email, users::name, users::avatar_url)) + .order_by(users::email) .load::(&mut conn) .await; From 98926c51095573c8f4a9daa562c0fd7509bba3b9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:28:24 +0000 Subject: [PATCH 2/5] Fix lint formatting issues in env-utils package - Add trailing newlines to tsconfig.json and biome.json - Resolves CI lint failures in Build, Lint & Test step Related to BUS-1477 Co-Authored-By: nate@buster.so --- packages/env-utils/biome.json | 2 +- packages/env-utils/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/env-utils/biome.json b/packages/env-utils/biome.json index ce45f6045..b242fa54d 100644 --- a/packages/env-utils/biome.json +++ b/packages/env-utils/biome.json @@ -4,4 +4,4 @@ "files": { "include": ["src/**/*"] } -} \ No newline at end of file +} diff --git a/packages/env-utils/tsconfig.json b/packages/env-utils/tsconfig.json index e0c5ef87a..becffbbee 100644 --- a/packages/env-utils/tsconfig.json +++ b/packages/env-utils/tsconfig.json @@ -7,4 +7,4 @@ }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] -} \ No newline at end of file +} From 1820dd6842ec26451485361b3936d52a4a28ae41 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:41:40 +0000 Subject: [PATCH 3/5] Implement consistent email sorting for individual_permissions arrays - Backend: Add itertools imports and case-insensitive email sorting to collections, dashboards, and metrics handlers - Frontend: Add email sorting to React Query mutation onMutate callbacks for share/unshare operations - Ensures consistent alphabetical ordering by email across API responses and UI state - Addresses BUS-1477 requirements for predictable individual_permissions ordering Co-Authored-By: nate@buster.so --- .../handlers/src/collections/get_collection_handler.rs | 7 +++++-- .../handlers/src/dashboards/get_dashboard_handler.rs | 7 +++++-- apps/api/libs/handlers/src/metrics/get_metric_handler.rs | 7 +++++-- .../web/src/api/buster_rest/collections/queryRequests.ts | 9 +++++---- apps/web/src/api/buster_rest/dashboards/queryRequests.ts | 9 +++++---- .../api/buster_rest/metrics/updateMetricQueryRequests.ts | 9 +++++---- 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/apps/api/libs/handlers/src/collections/get_collection_handler.rs b/apps/api/libs/handlers/src/collections/get_collection_handler.rs index 2a281543b..8fbc1a805 100644 --- a/apps/api/libs/handlers/src/collections/get_collection_handler.rs +++ b/apps/api/libs/handlers/src/collections/get_collection_handler.rs @@ -11,6 +11,7 @@ use database::{ }; use diesel::{ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl, Queryable}; use diesel_async::RunQueryDsl; +use itertools::Itertools; use middleware::AuthenticatedUser; use sharing::{check_permission_access, compute_effective_permission}; use tracing; @@ -133,7 +134,6 @@ pub async fn get_collection_handler( users::name, users::avatar_url, )) - .order_by(users::email) .load::(&mut conn) .await; @@ -157,7 +157,10 @@ pub async fn get_collection_handler( name: p.name, avatar_url: p.avatar_url, }) - .collect::>(), + .collect::>() + .into_iter() + .sorted_by(|a, b| a.email.to_lowercase().cmp(&b.email.to_lowercase())) + .collect(), ) } } diff --git a/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs b/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs index 498366a35..ce184d626 100644 --- a/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs +++ b/apps/api/libs/handlers/src/dashboards/get_dashboard_handler.rs @@ -5,6 +5,7 @@ use chrono::{DateTime, Utc}; use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl, Queryable, Selectable}; use diesel_async::RunQueryDsl; use futures::future::join_all; +use itertools::Itertools; use middleware::AuthenticatedUser; use serde_json::Value; use serde_yaml; @@ -346,7 +347,6 @@ pub async fn get_dashboard_handler( .filter(asset_permissions::identity_type.eq(IdentityType::User)) .filter(asset_permissions::deleted_at.is_null()) .select((asset_permissions::role, users::email, users::name, users::avatar_url)) - .order_by(users::email) .load::(&mut conn) .await; @@ -391,7 +391,10 @@ pub async fn get_dashboard_handler( name: p.name, avatar_url: p.avatar_url, }) - .collect::>(), + .collect::>() + .into_iter() + .sorted_by(|a, b| a.email.to_lowercase().cmp(&b.email.to_lowercase())) + .collect(), ) } } diff --git a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs index 05e3c211b..0e27cf3ee 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, Result}; use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl, Queryable}; use diesel_async::RunQueryDsl; use futures::future::join; +use itertools::Itertools; use middleware::AuthenticatedUser; use serde_yaml; use sharing::asset_access_checks::check_metric_collection_access; @@ -399,7 +400,6 @@ pub async fn get_metric_handler( .filter(asset_permissions::identity_type.eq(IdentityType::User)) .filter(asset_permissions::deleted_at.is_null()) .select((asset_permissions::role, users::email, users::name, users::avatar_url)) - .order_by(users::email) .load::(&mut conn) .await; @@ -465,7 +465,10 @@ pub async fn get_metric_handler( name: p.name, avatar_url: p.avatar_url, }) - .collect::>(), + .collect::>() + .into_iter() + .sorted_by(|a, b| a.email.to_lowercase().cmp(&b.email.to_lowercase())) + .collect(), ) } } diff --git a/apps/web/src/api/buster_rest/collections/queryRequests.ts b/apps/web/src/api/buster_rest/collections/queryRequests.ts index 7d9763943..0e5655549 100644 --- a/apps/web/src/api/buster_rest/collections/queryRequests.ts +++ b/apps/web/src/api/buster_rest/collections/queryRequests.ts @@ -182,7 +182,7 @@ export const useShareCollection = () => { draft.individual_permissions = [ ...params.map((p) => ({ ...p })), ...(draft.individual_permissions || []) - ]; + ].sort((a, b) => a.email.localeCompare(b.email)); }); }); }, @@ -208,7 +208,8 @@ export const useUnshareCollection = () => { if (!previousData) return previousData; return create(previousData, (draft: BusterCollection) => { draft.individual_permissions = - draft.individual_permissions?.filter((t) => !variables.data.includes(t.email)) || []; + (draft.individual_permissions?.filter((t) => !variables.data.includes(t.email)) || []) + .sort((a, b) => a.email.localeCompare(b.email)); }); }); }, @@ -231,11 +232,11 @@ export const useUpdateCollectionShare = () => { if (!previousData) return previousData; return create(previousData, (draft) => { draft.individual_permissions = - draft.individual_permissions?.map((t) => { + (draft.individual_permissions?.map((t) => { const found = params.users?.find((v) => v.email === t.email); if (found) return { ...t, ...found }; return t; - }) || []; + }) || []).sort((a, b) => a.email.localeCompare(b.email)); if (params.publicly_accessible !== undefined) { draft.publicly_accessible = params.publicly_accessible; diff --git a/apps/web/src/api/buster_rest/dashboards/queryRequests.ts b/apps/web/src/api/buster_rest/dashboards/queryRequests.ts index 4d2efdcf0..23640490e 100644 --- a/apps/web/src/api/buster_rest/dashboards/queryRequests.ts +++ b/apps/web/src/api/buster_rest/dashboards/queryRequests.ts @@ -368,7 +368,7 @@ export const useShareDashboard = () => { avatar_url: p.avatar_url || null })), ...(draft.individual_permissions || []) - ]; + ].sort((a, b) => a.email.localeCompare(b.email)); }); }); }, @@ -398,7 +398,8 @@ export const useUnshareDashboard = () => { if (!previousData) return previousData; return create(previousData, (draft) => { draft.individual_permissions = - draft.individual_permissions?.filter((t) => !variables.data.includes(t.email)) || []; + (draft.individual_permissions?.filter((t) => !variables.data.includes(t.email)) || []) + .sort((a, b) => a.email.localeCompare(b.email)); }); }); }, @@ -419,11 +420,11 @@ export const useUpdateDashboardShare = () => { if (!previousData) return previousData; return create(previousData, (draft) => { draft.individual_permissions = - draft.individual_permissions?.map((t) => { + (draft.individual_permissions?.map((t) => { const found = params.users?.find((v) => v.email === t.email); if (found) return { ...t, ...found }; return t; - }) || []; + }) || []).sort((a, b) => a.email.localeCompare(b.email)); if (params.publicly_accessible !== undefined) { draft.publicly_accessible = params.publicly_accessible; diff --git a/apps/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts b/apps/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts index b0ea68177..5bd233cfd 100644 --- a/apps/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts +++ b/apps/web/src/api/buster_rest/metrics/updateMetricQueryRequests.ts @@ -233,7 +233,7 @@ export const useShareMetric = () => { avatar_url: p.avatar_url || null })), ...(draft.individual_permissions || []) - ]; + ].sort((a, b) => a.email.localeCompare(b.email)); }); }); }, @@ -264,7 +264,8 @@ export const useUnshareMetric = () => { if (!previousData) return previousData; return create(previousData, (draft: BusterMetric) => { draft.individual_permissions = - draft.individual_permissions?.filter((t) => !variables.data.includes(t.email)) || []; + (draft.individual_permissions?.filter((t) => !variables.data.includes(t.email)) || []) + .sort((a, b) => a.email.localeCompare(b.email)); }); }); }, @@ -297,11 +298,11 @@ export const useUpdateMetricShare = () => { if (!previousData) return previousData; return create(previousData, (draft: BusterMetric) => { draft.individual_permissions = - draft.individual_permissions?.map((t) => { + (draft.individual_permissions?.map((t) => { const found = variables.params.users?.find((v) => v.email === t.email); if (found) return { ...t, ...found }; return t; - }) || []; + }) || []).sort((a, b) => a.email.localeCompare(b.email)); if (variables.params.publicly_accessible !== undefined) { draft.publicly_accessible = variables.params.publicly_accessible; From 6e626c2d2bc5b9d271a20da902d1bba27563e359 Mon Sep 17 00:00:00 2001 From: dal Date: Tue, 22 Jul 2025 08:20:45 -0600 Subject: [PATCH 4/5] added in the itertools --- apps/api/Cargo.toml | 1 + apps/api/libs/handlers/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/api/Cargo.toml b/apps/api/Cargo.toml index 35211c857..b52fea08c 100644 --- a/apps/api/Cargo.toml +++ b/apps/api/Cargo.toml @@ -47,6 +47,7 @@ mockito = "1.2.0" mockall = "0.12.1" bb8-redis = "0.18.0" indexmap = { version = "2.2.6", features = ["serde"] } +itertools = "0.14" once_cell = "1.20.2" rustls = { version = "0.23", features = ["ring"] } rustls-native-certs = "0.8" diff --git a/apps/api/libs/handlers/Cargo.toml b/apps/api/libs/handlers/Cargo.toml index 8b59a7f59..8726b17c7 100644 --- a/apps/api/libs/handlers/Cargo.toml +++ b/apps/api/libs/handlers/Cargo.toml @@ -37,6 +37,7 @@ semantic_layer = { path = "../semantic_layer" } # Add any handler-specific dependencies here dashmap = "5.5.3" +itertools = { workspace = true } # Add stored_values dependency stored_values = { path = "../stored_values" } From 0c034d0926b7324cd633adf3a7f58fe8e6c13092 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Tue, 22 Jul 2025 09:06:57 -0600 Subject: [PATCH 5/5] fix small linting error --- apps/trigger/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/trigger/package.json b/apps/trigger/package.json index bfa465c42..cb364bfeb 100644 --- a/apps/trigger/package.json +++ b/apps/trigger/package.json @@ -38,4 +38,4 @@ "devDependencies": { "@trigger.dev/build": "4.0.0-v4-beta.24" } -} \ No newline at end of file +}