Skip to content

Commit fc364b8

Browse files
committed
add sl support
1 parent 6a97f03 commit fc364b8

7 files changed

Lines changed: 63 additions & 15 deletions

File tree

src/api/semantic_layer/mod.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,47 @@ pub mod types;
44

55
/// Construct the Semantic Layer API base URL from a dbt Cloud host.
66
///
7-
/// Multi-tenant hosts (cloud.getdbt.com, emea.dbt.com, au.dbt.com)
8-
/// use the pattern `https://semantic-layer.{host}`.
7+
/// Multi-tenant: `https://tk626.us1.dbt.com` → `https://tk626.semantic-layer.us1.dbt.com`
8+
/// Legacy: `https://cloud.getdbt.com` → `https://semantic-layer.cloud.getdbt.com`
99
pub fn semantic_layer_url(host: &str) -> String {
10-
let host = host
11-
.trim_start_matches("https://")
12-
.trim_start_matches("http://")
13-
.trim_end_matches('/');
14-
format!("https://semantic-layer.{host}")
10+
let host = host.trim_end_matches('/');
11+
12+
let (scheme, rest) = if let Some(rest) = host.strip_prefix("https://") {
13+
("https://", rest)
14+
} else if let Some(rest) = host.strip_prefix("http://") {
15+
("http://", rest)
16+
} else {
17+
("https://", host)
18+
};
19+
20+
// Multi-tenant pattern: {slug}.{region}.dbt.com → {slug}.semantic-layer.{region}.dbt.com
21+
// Only applies when host has the form {slug}.{region}.dbt.com (two+ segments before .dbt.com)
22+
if let Some(prefix) = rest.strip_suffix(".dbt.com") {
23+
if prefix.contains('.') {
24+
if let Some(dot) = rest.find('.') {
25+
let slug = &rest[..dot];
26+
let remainder = &rest[dot + 1..];
27+
return format!("{scheme}{slug}.semantic-layer.{remainder}");
28+
}
29+
}
30+
}
31+
32+
// Legacy: cloud.getdbt.com → semantic-layer.cloud.getdbt.com
33+
format!("{scheme}semantic-layer.{rest}")
1534
}
1635

1736
#[cfg(test)]
1837
mod tests {
1938
use super::*;
2039

40+
#[test]
41+
fn test_semantic_layer_url_multi_tenant() {
42+
assert_eq!(
43+
semantic_layer_url("https://tk626.us1.dbt.com"),
44+
"https://tk626.semantic-layer.us1.dbt.com"
45+
);
46+
}
47+
2148
#[test]
2249
fn test_semantic_layer_url_plain_host() {
2350
assert_eq!(
@@ -37,8 +64,8 @@ mod tests {
3764
#[test]
3865
fn test_semantic_layer_url_with_trailing_slash() {
3966
assert_eq!(
40-
semantic_layer_url("https://au.dbt.com/"),
41-
"https://semantic-layer.au.dbt.com"
67+
semantic_layer_url("https://tk626.us1.dbt.com/"),
68+
"https://tk626.semantic-layer.us1.dbt.com"
4269
);
4370
}
4471
}

src/cli/commands/init.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub async fn exec(args: &InitArgs) -> Result<()> {
4949
connection: Connection {
5050
host: Some(host),
5151
token: Some(token),
52+
service_token: None,
5253
account_id: Some(account_id),
5354
},
5455
defaults: Defaults {

src/cli/commands/mod.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,17 +127,23 @@ pub async fn exec(
127127
println!("{}", format_output(&val, output_format));
128128
}
129129

130-
// Semantic Layer API commands
130+
// Semantic Layer API commands — require a service token
131131
Commands::Metrics(args) => {
132-
let val = metrics::exec(args, gql, config).await?;
132+
let sl_token = config.service_token.as_deref().unwrap_or(&config.token);
133+
let gql_sl = GraphqlClient::new(sl_token)?;
134+
let val = metrics::exec(args, &gql_sl, config).await?;
133135
println!("{}", format_output(&val, output_format));
134136
}
135137
Commands::SavedQueries(args) => {
136-
let val = saved_queries::exec(args, gql, config).await?;
138+
let sl_token = config.service_token.as_deref().unwrap_or(&config.token);
139+
let gql_sl = GraphqlClient::new(sl_token)?;
140+
let val = saved_queries::exec(args, &gql_sl, config).await?;
137141
println!("{}", format_output(&val, output_format));
138142
}
139143
Commands::DimensionValues(args) => {
140-
let val = dimension_values::exec(args, gql, config).await?;
144+
let sl_token = config.service_token.as_deref().unwrap_or(&config.token);
145+
let gql_sl = GraphqlClient::new(sl_token)?;
146+
let val = dimension_values::exec(args, &gql_sl, config).await?;
141147
println!("{}", format_output(&val, output_format));
142148
}
143149
}

src/cli/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ pub struct GlobalOpts {
5151
#[arg(long, global = true, env = "DBTP_TOKEN")]
5252
pub token: Option<String>,
5353

54+
/// Semantic Layer service token (required for metrics/saved-queries/dimension-values)
55+
#[arg(long, global = true, env = "DBTP_SERVICE_TOKEN")]
56+
pub service_token: Option<String>,
57+
5458
/// dbt Cloud account ID
5559
#[arg(long, global = true, env = "DBTP_ACCOUNT_ID")]
5660
pub account_id: Option<u64>,

src/core/config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use super::error::{DbtpError, Result};
1010
pub struct Config {
1111
pub host: String,
1212
pub token: String,
13+
/// Service token for the Semantic Layer API. Falls back to `token` if not set.
14+
pub service_token: Option<String>,
1315
pub account_id: Option<u64>,
1416
pub project_id: Option<String>,
1517
pub environment_id: Option<String>,
@@ -38,6 +40,7 @@ pub struct ConfigFile {
3840
pub struct Connection {
3941
pub host: Option<String>,
4042
pub token: Option<String>,
43+
pub service_token: Option<String>,
4144
pub account_id: Option<u64>,
4245
}
4346

@@ -64,6 +67,7 @@ fn default_output() -> String {
6467
pub struct ConfigOverrides {
6568
pub host: Option<String>,
6669
pub token: Option<String>,
70+
pub service_token: Option<String>,
6771
pub account_id: Option<u64>,
6872
pub project_id: Option<String>,
6973
pub environment_id: Option<String>,
@@ -163,6 +167,11 @@ pub fn load(overrides: ConfigOverrides) -> Result<Config> {
163167
.or(file.connection.token)
164168
.unwrap_or_default();
165169

170+
let service_token = overrides
171+
.service_token
172+
.or_else(|| std::env::var("DBTP_SERVICE_TOKEN").ok())
173+
.or(file.connection.service_token);
174+
166175
let account_id = overrides
167176
.account_id
168177
.or_else(|| {
@@ -186,6 +195,7 @@ pub fn load(overrides: ConfigOverrides) -> Result<Config> {
186195
Ok(Config {
187196
host,
188197
token,
198+
service_token,
189199
account_id,
190200
project_id,
191201
environment_id,

src/core/graphql_client.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ impl GraphqlClient {
8585
pub async fn semantic_layer(
8686
&self,
8787
host: &str,
88-
environment_id: u64,
88+
_environment_id: u64,
8989
query: &str,
9090
variables: Option<Value>,
9191
) -> Result<Value> {
@@ -97,7 +97,6 @@ impl GraphqlClient {
9797
let body = json!({
9898
"query": query,
9999
"variables": variables.unwrap_or(json!({})),
100-
"environmentId": environment_id,
101100
});
102101

103102
let resp = self

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ async fn run(cli: Cli) -> crate::core::error::Result<()> {
2424
let overrides = ConfigOverrides {
2525
host: cli.global.host.clone(),
2626
token: cli.global.token.clone(),
27+
service_token: cli.global.service_token.clone(),
2728
account_id: cli.global.account_id,
2829
project_id: cli.global.project_id.clone(),
2930
environment_id: cli.global.environment_id.clone(),

0 commit comments

Comments
 (0)