Skip to content

Commit

Permalink
feat: implement a query interface for turborepo (#8977)
Browse files Browse the repository at this point in the history
### Description

Adds a GraphQL query interface to turborepo via the `turbo query`
command. If you pass it a query string, then it directly runs the query.
If you pass it a file, it reads the file and runs the query. If you
don't pass a query, then it opens up a server with GraphiQL (we can ITG
this interface).


### Testing Instructions
Added tests to `affected.t` and `command-query.t`

---------

Co-authored-by: Chris Olszewski <[email protected]>
Co-authored-by: Thomas Knickman <[email protected]>
  • Loading branch information
3 people authored Aug 20, 2024
1 parent d9bd4f0 commit 6636370
Show file tree
Hide file tree
Showing 17 changed files with 1,388 additions and 156 deletions.
687 changes: 576 additions & 111 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ async-compression = { version = "0.3.13", default-features = false, features = [
] }
async-trait = "0.1.64"
atty = "0.2.14"
axum = "0.6.2"
axum-server = "0.4.4"
axum = "0.7.5"
axum-server = "0.7.1"
biome_console = { version = "0.5.7" }
biome_deserialize = { version = "0.6.0", features = ["serde"] }
biome_deserialize_macros = { version = "0.6.0" }
Expand Down
2 changes: 1 addition & 1 deletion crates/turborepo-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ pub struct CacheHitMetadata {
pub time_saved: u64,
}

#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
pub struct CacheOpts {
pub cache_dir: Utf8PathBuf,
pub remote_cache_read_only: bool,
Expand Down
2 changes: 2 additions & 0 deletions crates/turborepo-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ turborepo-vercel-api-mock = { workspace = true }
workspace = true

[dependencies]
async-graphql = "7.0.7"
async-graphql-axum = "7.0.7"
atty = { workspace = true }
axum = { workspace = true }
biome_deserialize = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions crates/turborepo-lib/src/cli/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use turborepo_ui::{color, BOLD, GREY};
use crate::{
commands::{bin, generate, ls, prune, run::get_signal, CommandBase},
daemon::DaemonError,
query,
rewrite_json::RewriteError,
run,
run::{builder::RunBuilder, watch},
Expand Down Expand Up @@ -54,6 +55,9 @@ pub enum Error {
#[diagnostic(transparent)]
Run(#[from] run::Error),
#[error(transparent)]
#[diagnostic(transparent)]
Query(#[from] query::Error),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[error(transparent)]
#[diagnostic(transparent)]
Expand Down
21 changes: 20 additions & 1 deletion crates/turborepo-lib/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use turborepo_ui::{ColorConfig, GREY};
use crate::{
cli::error::print_potential_tasks,
commands::{
bin, config, daemon, generate, link, login, logout, ls, prune, run, scan, telemetry,
bin, config, daemon, generate, link, login, logout, ls, prune, query, run, scan, telemetry,
unlink, CommandBase,
},
get_version,
Expand Down Expand Up @@ -588,6 +588,13 @@ pub enum Command {
#[clap(flatten)]
execution_args: Box<ExecutionArgs>,
},
/// Query your monorepo using GraphQL. If no query is provided, spins up a
/// GraphQL server with GraphiQL.
#[clap(hide = true)]
Query {
/// The query to run, either a file path or a query string
query: Option<String>,
},
Watch(Box<ExecutionArgs>),
/// Unlink the current directory from your Vercel organization and disable
/// Remote Caching
Expand Down Expand Up @@ -1198,6 +1205,7 @@ pub async fn run(
let filter = filter.clone();
let packages = packages.clone();
let base = CommandBase::new(cli_args, repo_root, version, color_config);

ls::run(base, packages, event, filter, affected, output).await?;

Ok(0)
Expand Down Expand Up @@ -1301,6 +1309,17 @@ pub async fn run(
})?;
Ok(exit_code)
}
Command::Query { query } => {
warn!("query command is experimental and may change in the future");
let query = query.clone();
let event = CommandEventBuilder::new("query").with_parent(&root_telemetry);
event.track_call();
let base = CommandBase::new(cli_args, repo_root, version, color_config);

let query = query::run(base, event, query).await?;

Ok(query)
}
Command::Watch(_) => {
let event = CommandEventBuilder::new("watch").with_parent(&root_telemetry);
event.track_call();
Expand Down
1 change: 1 addition & 0 deletions crates/turborepo-lib/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub(crate) mod login;
pub(crate) mod logout;
pub(crate) mod ls;
pub(crate) mod prune;
pub(crate) mod query;
pub(crate) mod run;
pub(crate) mod scan;
pub(crate) mod telemetry;
Expand Down
103 changes: 103 additions & 0 deletions crates/turborepo-lib/src/commands/query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::fs;

use async_graphql::{EmptyMutation, EmptySubscription, Schema, ServerError};
use miette::{Diagnostic, Report, SourceSpan};
use thiserror::Error;
use turbopath::AbsoluteSystemPathBuf;
use turborepo_telemetry::events::command::CommandEventBuilder;

use crate::{
cli::Command,
commands::{run::get_signal, CommandBase},
query,
query::{Error, Query},
run::builder::RunBuilder,
signal::SignalHandler,
};

#[derive(Debug, Diagnostic, Error)]
#[error("{message}")]
struct QueryError {
message: String,
#[source_code]
query: String,
#[label]
span: Option<SourceSpan>,
#[label]
span2: Option<SourceSpan>,
#[label]
span3: Option<SourceSpan>,
}

impl QueryError {
fn get_index_from_row_column(query: &str, row: usize, column: usize) -> usize {
let mut index = 0;
for line in query.lines().take(row + 1) {
index += line.len() + 1;
}
index + column
}
fn new(server_error: ServerError, query: String) -> Self {
let span: Option<SourceSpan> = server_error.locations.first().map(|location| {
let idx =
Self::get_index_from_row_column(query.as_ref(), location.line, location.column);
(idx, idx + 1).into()
});

QueryError {
message: server_error.message,
query,
span,
span2: None,
span3: None,
}
}
}

pub async fn run(
mut base: CommandBase,
telemetry: CommandEventBuilder,
query: Option<String>,
) -> Result<i32, Error> {
let signal = get_signal()?;
let handler = SignalHandler::new(signal);

// We fake a run command, so we can construct a `Run` type
base.args_mut().command = Some(Command::Run {
run_args: Box::default(),
execution_args: Box::default(),
});

let run_builder = RunBuilder::new(base)?;
let run = run_builder.build(&handler, telemetry).await?;

if let Some(query) = query {
let trimmed_query = query.trim();
// If the arg starts with "query" or "mutation", and ends in a bracket, it's
// likely a direct query If it doesn't, it's a file path, so we need to
// read it
let query = if (trimmed_query.starts_with("query") || trimmed_query.starts_with("mutation"))
&& trimmed_query.ends_with('}')
{
query
} else {
fs::read_to_string(AbsoluteSystemPathBuf::from_unknown(run.repo_root(), query))?
};

let schema = Schema::new(Query::new(run), EmptyMutation, EmptySubscription);

let result = schema.execute(&query).await;
if result.errors.is_empty() {
println!("{}", serde_json::to_string_pretty(&result)?);
} else {
for error in result.errors {
let error = QueryError::new(error, query.clone());
eprintln!("{:?}", Report::new(error));
}
}
} else {
query::run_server(run, handler).await?;
}

Ok(0)
}
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/daemon/default_timeout_layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ mod test {
sync::{Arc, Mutex},
};

use axum::http::HeaderValue;
use reqwest::header::HeaderValue;
use test_case::test_case;

use super::*;
Expand Down
1 change: 1 addition & 0 deletions crates/turborepo-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod opts;
mod package_changes_watcher;
mod panic_handler;
mod process;
mod query;
mod rewrite_json;
mod run;
mod shim;
Expand Down
10 changes: 5 additions & 5 deletions crates/turborepo-lib/src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub enum Error {
Config(#[from] crate::config::Error),
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Opts {
pub cache_opts: CacheOpts,
pub run_opts: RunOpts,
Expand Down Expand Up @@ -127,7 +127,7 @@ struct OptsInputs<'a> {
api_auth: &'a Option<APIAuth>,
}

#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
pub struct RunCacheOpts {
pub(crate) skip_reads: bool,
pub(crate) skip_writes: bool,
Expand All @@ -144,7 +144,7 @@ impl<'a> From<OptsInputs<'a>> for RunCacheOpts {
}
}

#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct RunOpts {
pub(crate) tasks: Vec<String>,
pub(crate) concurrency: u32,
Expand Down Expand Up @@ -183,7 +183,7 @@ impl RunOpts {
}
}

#[derive(Debug)]
#[derive(Clone, Debug)]
pub enum GraphOpts {
Stdout,
File(String),
Expand Down Expand Up @@ -302,7 +302,7 @@ impl From<LogPrefix> for ResolvedLogPrefix {
}
}

#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct ScopeOpts {
pub pkg_inference_root: Option<AnchoredSystemPathBuf>,
pub global_deps: Vec<String>,
Expand Down
Loading

0 comments on commit 6636370

Please sign in to comment.