From bbed3bd976991c5a9a5b45900881abd71ff9483a Mon Sep 17 00:00:00 2001 From: Benoit Ranque Date: Wed, 25 Sep 2024 00:16:24 -0400 Subject: [PATCH 1/8] implement printSchemaAndCapabilities --- CHANGELOG.md | 1 + Cargo.lock | 4 ++ ci/templates/connector-metadata.yaml | 1 + crates/common/Cargo.toml | 5 ++ .../src/capabilities_response.rs} | 2 +- crates/common/src/config.rs | 9 ++- crates/common/src/{ => config}/config_file.rs | 0 crates/common/src/{ => config}/schema.rs | 0 crates/common/src/lib.rs | 4 +- .../src/schema_response.rs} | 60 ++++++++++--------- crates/ndc-graphql-cli/Cargo.toml | 3 + crates/ndc-graphql-cli/src/main.rs | 47 +++++++++++++-- crates/ndc-graphql/src/connector.rs | 13 ++-- crates/ndc-graphql/src/connector/setup.rs | 4 +- crates/ndc-graphql/src/query_builder.rs | 4 +- .../src/query_builder/operation_parameters.rs | 2 +- crates/ndc-graphql/tests/query_builder.rs | 15 +++-- 17 files changed, 117 insertions(+), 57 deletions(-) rename crates/{ndc-graphql/src/connector/capabilities.rs => common/src/capabilities_response.rs} (92%) rename crates/common/src/{ => config}/config_file.rs (100%) rename crates/common/src/{ => config}/schema.rs (100%) rename crates/{ndc-graphql/src/connector/schema.rs => common/src/schema_response.rs} (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c1b32e..80789d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix bug where introspection including interfaces would fail to parse in some circumstances - Config now defaults to asking for a `GRAPHQL_ENDPOINT` env var - Fix a bug where default values were not parsed as graphql values, and instead used as string literals +- CLI: Implement `print-schema-and-capabilities` command, allowing local dev to update config & schema without starting a connector instance ## [0.1.3] diff --git a/Cargo.lock b/Cargo.lock index b5bc242..7cd923f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,13 +369,16 @@ dependencies = [ name = "common" version = "0.1.3" dependencies = [ + "async-trait", "glob-match", "graphql-parser", "graphql_client", + "ndc-sdk", "reqwest 0.12.7", "schemars", "serde", "serde_json", + "tokio", ] [[package]] @@ -1218,6 +1221,7 @@ dependencies = [ "graphql-parser", "graphql_client", "insta", + "ndc-sdk", "schemars", "serde", "serde_json", diff --git a/ci/templates/connector-metadata.yaml b/ci/templates/connector-metadata.yaml index 6c86eed..8991acd 100644 --- a/ci/templates/connector-metadata.yaml +++ b/ci/templates/connector-metadata.yaml @@ -8,6 +8,7 @@ supportedEnvironmentVariables: required: true commands: update: hasura-ndc-graphql update + printSchemaAndCapabilities: hasura-ndc-graphql print-schema-and-capabilities cliPlugin: name: ndc-graphql version: "${CLI_VERSION}" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 114ec0f..1158a26 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -4,9 +4,13 @@ version.workspace = true edition.workspace = true [dependencies] +async-trait = "0.1.78" glob-match = "0.2.1" graphql_client = "0.14.0" graphql-parser = "0.4.0" +ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs", tag = "v0.1.5", package = "ndc-sdk", features = [ + "rustls", +], default-features = false } reqwest = { version = "0.12.3", features = [ "json", "rustls-tls", @@ -14,3 +18,4 @@ reqwest = { version = "0.12.3", features = [ schemars = "0.8.16" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" +tokio = "1.36.0" diff --git a/crates/ndc-graphql/src/connector/capabilities.rs b/crates/common/src/capabilities_response.rs similarity index 92% rename from crates/ndc-graphql/src/connector/capabilities.rs rename to crates/common/src/capabilities_response.rs index 435d776..b7151da 100644 --- a/crates/ndc-graphql/src/connector/capabilities.rs +++ b/crates/common/src/capabilities_response.rs @@ -1,6 +1,6 @@ use ndc_sdk::models; -pub fn capabilities() -> models::CapabilitiesResponse { +pub fn capabilities_response() -> models::CapabilitiesResponse { models::CapabilitiesResponse { version: "0.1.4".to_string(), capabilities: models::Capabilities { diff --git a/crates/common/src/config.rs b/crates/common/src/config.rs index fddbc7f..ad36dc5 100644 --- a/crates/common/src/config.rs +++ b/crates/common/src/config.rs @@ -1,9 +1,8 @@ +use config_file::{RequestConfigFile, ResponseConfigFile}; +use schema::SchemaDefinition; use std::collections::BTreeMap; - -use crate::{ - config_file::{RequestConfigFile, ResponseConfigFile}, - schema::SchemaDefinition, -}; +pub mod config_file; +pub mod schema; #[derive(Debug, Clone)] pub struct ServerConfig { diff --git a/crates/common/src/config_file.rs b/crates/common/src/config/config_file.rs similarity index 100% rename from crates/common/src/config_file.rs rename to crates/common/src/config/config_file.rs diff --git a/crates/common/src/schema.rs b/crates/common/src/config/schema.rs similarity index 100% rename from crates/common/src/schema.rs rename to crates/common/src/config/schema.rs diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 07b4bcd..520471d 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,4 +1,4 @@ +pub mod capabilities_response; pub mod client; pub mod config; -pub mod config_file; -pub mod schema; +pub mod schema_response; diff --git a/crates/ndc-graphql/src/connector/schema.rs b/crates/common/src/schema_response.rs similarity index 80% rename from crates/ndc-graphql/src/connector/schema.rs rename to crates/common/src/schema_response.rs index 6935675..2a5e068 100644 --- a/crates/ndc-graphql/src/connector/schema.rs +++ b/crates/common/src/schema_response.rs @@ -1,24 +1,27 @@ -use common::{ - config::ServerConfig, +use crate::config::{ schema::{ - InputObjectFieldDefinition, ObjectFieldArgumentDefinition, ObjectFieldDefinition, TypeRef, + InputObjectFieldDefinition, ObjectFieldArgumentDefinition, ObjectFieldDefinition, + SchemaDefinition, TypeDef, TypeRef, }, + RequestConfig, ResponseConfig, }; use ndc_sdk::models; use std::{collections::BTreeMap, iter}; -pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { - let forward_request_headers = !configuration.request.forward_headers.is_empty(); - let forward_response_headers = !configuration.response.forward_headers.is_empty(); +pub fn schema_response( + schema: &SchemaDefinition, + request: &RequestConfig, + response: &ResponseConfig, +) -> models::SchemaResponse { + let forward_request_headers = !request.forward_headers.is_empty(); + let forward_response_headers = !response.forward_headers.is_empty(); - let mut scalar_types: BTreeMap<_, _> = configuration - .schema + let mut scalar_types: BTreeMap<_, _> = schema .definitions .iter() .filter_map(|(name, typedef)| match typedef { - common::schema::TypeDef::Object { .. } - | common::schema::TypeDef::InputObject { .. } => None, - common::schema::TypeDef::Scalar { description: _ } => Some(( + TypeDef::Object { .. } | TypeDef::InputObject { .. } => None, + TypeDef::Scalar { description: _ } => Some(( name.to_owned(), models::ScalarType { representation: None, @@ -26,7 +29,7 @@ pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { comparison_operators: BTreeMap::new(), }, )), - common::schema::TypeDef::Enum { + TypeDef::Enum { values, description: _, } => Some(( @@ -44,7 +47,7 @@ pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { if forward_request_headers { scalar_types.insert( - configuration.request.headers_type_name.to_owned(), + request.headers_type_name.to_owned(), models::ScalarType { representation: Some(models::TypeRepresentation::JSON), aggregate_functions: BTreeMap::new(), @@ -53,13 +56,12 @@ pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { ); } - let mut object_types: BTreeMap<_, _> = configuration - .schema + let mut object_types: BTreeMap<_, _> = schema .definitions .iter() .filter_map(|(name, typedef)| match typedef { - common::schema::TypeDef::Scalar { .. } | common::schema::TypeDef::Enum { .. } => None, - common::schema::TypeDef::Object { + TypeDef::Scalar { .. } | TypeDef::Enum { .. } => None, + TypeDef::Object { fields, description, } => Some(( @@ -69,7 +71,7 @@ pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { fields: fields.iter().map(map_object_field).collect(), }, )), - common::schema::TypeDef::InputObject { + TypeDef::InputObject { fields, description, } => Some(( @@ -90,17 +92,17 @@ pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { )), fields: BTreeMap::from_iter(vec![ ( - configuration.response.headers_field.to_owned(), + response.headers_field.to_owned(), models::ObjectField { description: None, r#type: models::Type::Named { - name: configuration.request.headers_type_name.to_owned(), + name: request.headers_type_name.to_owned(), }, arguments: BTreeMap::new(), }, ), ( - configuration.response.response_field.to_owned(), + response.response_field.to_owned(), models::ObjectField { description: None, r#type: typeref_to_ndc_type(&field.r#type), @@ -113,16 +115,16 @@ pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { let mut functions = vec![]; - for (name, field) in &configuration.schema.query_fields { + for (name, field) in &schema.query_fields { let arguments = field.arguments.iter().map(map_argument); let arguments = if forward_request_headers { arguments .chain(iter::once(( - configuration.request.headers_argument.to_owned(), + request.headers_argument.to_owned(), models::ArgumentInfo { description: None, argument_type: models::Type::Named { - name: configuration.request.headers_type_name.to_owned(), + name: request.headers_type_name.to_owned(), }, }, ))) @@ -132,7 +134,7 @@ pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { }; let result_type = if forward_response_headers { - let response_type_name = configuration.response.query_response_type_name(name); + let response_type_name = response.query_response_type_name(name); object_types.insert( response_type_name.clone(), @@ -156,16 +158,16 @@ pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { let mut procedures = vec![]; - for (name, field) in &configuration.schema.mutation_fields { + for (name, field) in &schema.mutation_fields { let arguments = field.arguments.iter().map(map_argument); let arguments = if forward_request_headers { arguments .chain(iter::once(( - configuration.request.headers_argument.to_owned(), + request.headers_argument.to_owned(), models::ArgumentInfo { description: None, argument_type: models::Type::Named { - name: configuration.request.headers_type_name.to_owned(), + name: request.headers_type_name.to_owned(), }, }, ))) @@ -175,7 +177,7 @@ pub fn schema_response(configuration: &ServerConfig) -> models::SchemaResponse { }; let result_type = if forward_response_headers { - let response_type_name = configuration.response.mutation_response_type_name(name); + let response_type_name = response.mutation_response_type_name(name); object_types.insert( response_type_name.clone(), diff --git a/crates/ndc-graphql-cli/Cargo.toml b/crates/ndc-graphql-cli/Cargo.toml index 8f33c91..ed522e7 100644 --- a/crates/ndc-graphql-cli/Cargo.toml +++ b/crates/ndc-graphql-cli/Cargo.toml @@ -9,6 +9,9 @@ common = { path = "../common" } graphql_client = "0.14.0" graphql-introspection-query = "0.2.0" graphql-parser = "0.4.0" +ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs", tag = "v0.1.5", package = "ndc-sdk", features = [ + "rustls", +], default-features = false } schemars = "0.8.16" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" diff --git a/crates/ndc-graphql-cli/src/main.rs b/crates/ndc-graphql-cli/src/main.rs index 0d5105a..84c27ae 100644 --- a/crates/ndc-graphql-cli/src/main.rs +++ b/crates/ndc-graphql-cli/src/main.rs @@ -1,15 +1,22 @@ use clap::{Parser, Subcommand, ValueEnum}; use common::{ - config::ConnectionConfig, - config_file::{ - ConfigValue, ServerConfigFile, CONFIG_FILE_NAME, CONFIG_SCHEMA_FILE_NAME, SCHEMA_FILE_NAME, + capabilities_response::capabilities_response, + config::{ + config_file::{ + ConfigValue, ServerConfigFile, CONFIG_FILE_NAME, CONFIG_SCHEMA_FILE_NAME, + SCHEMA_FILE_NAME, + }, + schema::SchemaDefinition, + ConnectionConfig, }, - schema::SchemaDefinition, + schema_response::schema_response, }; use graphql::{execute_graphql_introspection, schema_from_introspection}; use graphql_parser::schema; use ndc_graphql_cli::graphql; +use ndc_sdk::models; use schemars::schema_for; +use serde::Serialize; use std::{ env, error::Error, @@ -64,6 +71,7 @@ enum Command { Update {}, Validate {}, Watch {}, + PrintSchemaAndCapabilities {}, } #[derive(Clone, ValueEnum)] @@ -77,6 +85,12 @@ enum LogLevel { Trace, } +#[derive(Serialize)] +struct SchemaAndCapabilities { + schema: models::SchemaResponse, + capabilities: models::CapabilitiesResponse, +} + #[tokio::main] async fn main() -> Result<(), Box> { let args = CliArgs::parse(); @@ -110,6 +124,31 @@ async fn main() -> Result<(), Box> { Command::Watch {} => { todo!("implement watch command") } + Command::PrintSchemaAndCapabilities {} => { + let config_file = read_config_file(&context_path) + .await? + .ok_or_else(|| format!("Could not find {CONFIG_FILE_NAME}"))?; + let schema_document = read_schema_file(&context_path) + .await? + .ok_or_else(|| format!("Could not find {SCHEMA_FILE_NAME}"))?; + + let request_config = config_file.request.into(); + let response_config = config_file.response.into(); + + let schema = + SchemaDefinition::new(&schema_document, &request_config, &response_config)?; + + let schema_and_capabilities = SchemaAndCapabilities { + schema: schema_response(&schema, &request_config, &response_config), + capabilities: capabilities_response(), + }; + + println!( + "{}", + serde_json::to_string(&schema_and_capabilities) + .expect("Schema and capabilities should serialize to JSON") + ) + } } Ok(()) diff --git a/crates/ndc-graphql/src/connector.rs b/crates/ndc-graphql/src/connector.rs index feaf846..fd7b152 100644 --- a/crates/ndc-graphql/src/connector.rs +++ b/crates/ndc-graphql/src/connector.rs @@ -2,8 +2,10 @@ use self::state::ServerState; use crate::query_builder::{build_mutation_document, build_query_document}; use async_trait::async_trait; use common::{ + capabilities_response::capabilities_response, client::{execute_graphql, GraphQLRequest}, config::ServerConfig, + schema_response::schema_response, }; use indexmap::IndexMap; use ndc_sdk::{ @@ -14,11 +16,8 @@ use ndc_sdk::{ json_response::JsonResponse, models, }; -use schema::schema_response; use std::{collections::BTreeMap, mem}; use tracing::{Instrument, Level}; -pub mod capabilities; -pub mod schema; pub mod setup; mod state; @@ -45,13 +44,17 @@ impl Connector for GraphQLConnector { } async fn get_capabilities() -> JsonResponse { - JsonResponse::Value(capabilities::capabilities()) + JsonResponse::Value(capabilities_response()) } async fn get_schema( configuration: &Self::Configuration, ) -> Result, SchemaError> { - Ok(JsonResponse::Value(schema_response(configuration))) + Ok(JsonResponse::Value(schema_response( + &configuration.schema, + &configuration.request, + &configuration.response, + ))) } async fn query_explain( diff --git a/crates/ndc-graphql/src/connector/setup.rs b/crates/ndc-graphql/src/connector/setup.rs index 2689c36..5409ec0 100644 --- a/crates/ndc-graphql/src/connector/setup.rs +++ b/crates/ndc-graphql/src/connector/setup.rs @@ -1,9 +1,9 @@ use super::{state::ServerState, GraphQLConnector}; use async_trait::async_trait; -use common::{ - config::{ConnectionConfig, ServerConfig}, +use common::config::{ config_file::{ConfigValue, ServerConfigFile, CONFIG_FILE_NAME, SCHEMA_FILE_NAME}, schema::SchemaDefinition, + ConnectionConfig, ServerConfig, }; use graphql_parser::parse_schema; use ndc_sdk::connector::{ diff --git a/crates/ndc-graphql/src/query_builder.rs b/crates/ndc-graphql/src/query_builder.rs index 22e7de5..e79a302 100644 --- a/crates/ndc-graphql/src/query_builder.rs +++ b/crates/ndc-graphql/src/query_builder.rs @@ -1,7 +1,7 @@ use self::{error::QueryBuilderError, operation_parameters::OperationParameters}; -use common::{ - config::ServerConfig, +use common::config::{ schema::{ObjectFieldDefinition, TypeDef}, + ServerConfig, }; use glob_match::glob_match; use graphql_parser::{ diff --git a/crates/ndc-graphql/src/query_builder/operation_parameters.rs b/crates/ndc-graphql/src/query_builder/operation_parameters.rs index 78a24cf..1ab79ed 100644 --- a/crates/ndc-graphql/src/query_builder/operation_parameters.rs +++ b/crates/ndc-graphql/src/query_builder/operation_parameters.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use common::schema::TypeRef; +use common::config::schema::TypeRef; use graphql_parser::{ query::{Type, Value, VariableDefinition}, Pos, diff --git a/crates/ndc-graphql/tests/query_builder.rs b/crates/ndc-graphql/tests/query_builder.rs index 8dffaf0..02225c2 100644 --- a/crates/ndc-graphql/tests/query_builder.rs +++ b/crates/ndc-graphql/tests/query_builder.rs @@ -1,10 +1,9 @@ +use common::capabilities_response::capabilities_response; use common::config::ServerConfig; -use common::config_file::ServerConfigFile; +use common::{config::config_file::ServerConfigFile, schema_response::schema_response}; use insta::{assert_json_snapshot, assert_snapshot, assert_yaml_snapshot, glob}; use ndc_graphql::{ - connector::{ - capabilities::capabilities, schema::schema_response, setup::GraphQLConnectorSetup, - }, + connector::setup::GraphQLConnectorSetup, query_builder::{build_mutation_document, build_query_document}, }; use ndc_sdk::models; @@ -98,12 +97,16 @@ async fn test_build_graphql_mutation() { async fn test_generated_schema() { for config in ["config-1", "config-2", "config-3"] { let configuration = read_configuration(config).await; - let schema = schema_response(&configuration); + let schema = schema_response( + &configuration.schema, + &configuration.request, + &configuration.response, + ); assert_yaml_snapshot!(format!("{config} NDC Schema"), schema); } } #[test] fn test_capabilities() { - assert_yaml_snapshot!("Capabilities", capabilities()) + assert_yaml_snapshot!("Capabilities", capabilities_response()) } From 663db02a5753e91f197aae965ead3487a49a9d1d Mon Sep 17 00:00:00 2001 From: Benoit Ranque Date: Wed, 25 Sep 2024 00:29:03 -0400 Subject: [PATCH 2/8] update CI to test builds on all PRs (but not release) --- .github/workflows/deploy-stage.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-stage.yaml b/.github/workflows/deploy-stage.yaml index 75c9678..fb3eee9 100644 --- a/.github/workflows/deploy-stage.yaml +++ b/.github/workflows/deploy-stage.yaml @@ -1,5 +1,8 @@ name: Deploy connector to dockerhub, release cli on github on: + pull_request: + branches: + - main push: branches: - main @@ -41,7 +44,7 @@ jobs: uses: docker/build-push-action@v5 with: context: . - push: true + push: ${{ startsWith(github.ref, 'refs/tags/v') }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From c966b272cbbe890267c1667fbb87fb5ee884b3c0 Mon Sep 17 00:00:00 2001 From: Benoit Ranque Date: Wed, 25 Sep 2024 10:57:00 -0400 Subject: [PATCH 3/8] update deps to avoid openssl dependency in cli plugin --- Cargo.lock | 9 ++++----- crates/common/Cargo.toml | 6 ++---- crates/common/src/capabilities_response.rs | 2 +- crates/common/src/schema_response.rs | 2 +- crates/ndc-graphql-cli/Cargo.toml | 4 +--- crates/ndc-graphql-cli/src/main.rs | 2 +- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cd923f..a3f690c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -373,7 +373,7 @@ dependencies = [ "glob-match", "graphql-parser", "graphql_client", - "ndc-sdk", + "ndc-models", "reqwest 0.12.7", "schemars", "serde", @@ -956,9 +956,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", @@ -969,7 +969,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -1221,7 +1220,7 @@ dependencies = [ "graphql-parser", "graphql_client", "insta", - "ndc-sdk", + "ndc-models", "schemars", "serde", "serde_json", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 1158a26..1b2c1df 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -8,10 +8,8 @@ async-trait = "0.1.78" glob-match = "0.2.1" graphql_client = "0.14.0" graphql-parser = "0.4.0" -ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs", tag = "v0.1.5", package = "ndc-sdk", features = [ - "rustls", -], default-features = false } -reqwest = { version = "0.12.3", features = [ +ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.4" } +reqwest = { version = "0.12.7", features = [ "json", "rustls-tls", ], default-features = false } diff --git a/crates/common/src/capabilities_response.rs b/crates/common/src/capabilities_response.rs index b7151da..753d09a 100644 --- a/crates/common/src/capabilities_response.rs +++ b/crates/common/src/capabilities_response.rs @@ -1,4 +1,4 @@ -use ndc_sdk::models; +use ndc_models as models; pub fn capabilities_response() -> models::CapabilitiesResponse { models::CapabilitiesResponse { diff --git a/crates/common/src/schema_response.rs b/crates/common/src/schema_response.rs index 2a5e068..138ca66 100644 --- a/crates/common/src/schema_response.rs +++ b/crates/common/src/schema_response.rs @@ -5,7 +5,7 @@ use crate::config::{ }, RequestConfig, ResponseConfig, }; -use ndc_sdk::models; +use ndc_models as models; use std::{collections::BTreeMap, iter}; pub fn schema_response( diff --git a/crates/ndc-graphql-cli/Cargo.toml b/crates/ndc-graphql-cli/Cargo.toml index ed522e7..4f07c6e 100644 --- a/crates/ndc-graphql-cli/Cargo.toml +++ b/crates/ndc-graphql-cli/Cargo.toml @@ -9,9 +9,7 @@ common = { path = "../common" } graphql_client = "0.14.0" graphql-introspection-query = "0.2.0" graphql-parser = "0.4.0" -ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs", tag = "v0.1.5", package = "ndc-sdk", features = [ - "rustls", -], default-features = false } +ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.4" } schemars = "0.8.16" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" diff --git a/crates/ndc-graphql-cli/src/main.rs b/crates/ndc-graphql-cli/src/main.rs index 84c27ae..4d6e7a0 100644 --- a/crates/ndc-graphql-cli/src/main.rs +++ b/crates/ndc-graphql-cli/src/main.rs @@ -14,7 +14,7 @@ use common::{ use graphql::{execute_graphql_introspection, schema_from_introspection}; use graphql_parser::schema; use ndc_graphql_cli::graphql; -use ndc_sdk::models; +use ndc_models as models; use schemars::schema_for; use serde::Serialize; use std::{ From 487b0738a85031ca8f08b27e0239f95b977436b8 Mon Sep 17 00:00:00 2001 From: Benoit Ranque Date: Fri, 27 Sep 2024 12:25:55 -0400 Subject: [PATCH 4/8] update to latest ndc sdk --- Cargo.lock | 30 ++- crates/common/Cargo.toml | 2 +- crates/common/src/capabilities.rs | 31 +++ crates/common/src/capabilities_response.rs | 24 --- crates/common/src/config.rs | 23 ++- crates/common/src/config/config_file.rs | 79 ++++---- crates/common/src/config/schema.rs | 82 ++++---- crates/common/src/lib.rs | 2 +- crates/common/src/schema_response.rs | 52 ++--- crates/ndc-graphql-cli/Cargo.toml | 2 +- crates/ndc-graphql-cli/src/main.rs | 10 +- crates/ndc-graphql/Cargo.toml | 2 +- crates/ndc-graphql/src/connector.rs | 94 ++++----- crates/ndc-graphql/src/connector/setup.rs | 14 +- crates/ndc-graphql/src/main.rs | 6 +- crates/ndc-graphql/src/query_builder.rs | 66 ++++--- crates/ndc-graphql/src/query_builder/error.rs | 34 ++-- .../src/query_builder/operation_parameters.rs | 3 +- .../configuration/configuration.schema.json | 48 +++-- .../mutations/_mutation_request.schema.json | 31 +++ .../queries/_query_request.schema.json | 31 +++ .../configuration/configuration.schema.json | 48 +++-- .../mutations/_mutation_request.schema.json | 31 +++ .../queries/_query_request.schema.json | 31 +++ .../configuration/configuration.schema.json | 48 +++-- .../mutations/_mutation_request.schema.json | 31 +++ .../queries/_query_request.schema.json | 31 +++ crates/ndc-graphql/tests/query_builder.rs | 11 +- .../query_builder__Capabilities.snap | 5 +- ...ry_builder__Configuration JSON Schema.snap | 184 ++++++++++++++++++ 30 files changed, 782 insertions(+), 304 deletions(-) create mode 100644 crates/common/src/capabilities.rs delete mode 100644 crates/common/src/capabilities_response.rs create mode 100644 crates/ndc-graphql/tests/snapshots/query_builder__Configuration JSON Schema.snap diff --git a/Cargo.lock b/Cargo.lock index a3f690c..7d6584e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1229,20 +1229,22 @@ dependencies = [ [[package]] name = "ndc-models" -version = "0.1.4" -source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.4#20172e3b2552b78d16dbafcd047f559ced420309" +version = "0.1.6" +source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.6#d1be19e9cdd86ac7b6ad003ff82b7e5b4e96b84f" dependencies = [ "indexmap 2.5.0", + "ref-cast", "schemars", "serde", "serde_json", "serde_with", + "smol_str", ] [[package]] name = "ndc-sdk" -version = "0.1.5" -source = "git+https://github.com/hasura/ndc-sdk-rs?tag=v0.1.5#7f8382001b745c24b5f066411dde6822df65f545" +version = "0.4.0" +source = "git+https://github.com/hasura/ndc-sdk-rs?tag=v0.4.0#665509f7d3b47ce4f014fc23f817a3599ba13933" dependencies = [ "async-trait", "axum", @@ -1711,6 +1713,26 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + [[package]] name = "regex" version = "1.10.6" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 1b2c1df..892c25e 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -8,7 +8,7 @@ async-trait = "0.1.78" glob-match = "0.2.1" graphql_client = "0.14.0" graphql-parser = "0.4.0" -ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.4" } +ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.6" } reqwest = { version = "0.12.7", features = [ "json", "rustls-tls", diff --git a/crates/common/src/capabilities.rs b/crates/common/src/capabilities.rs new file mode 100644 index 0000000..d1b9f0b --- /dev/null +++ b/crates/common/src/capabilities.rs @@ -0,0 +1,31 @@ +use ndc_models as models; + +pub fn capabilities() -> models::Capabilities { + models::Capabilities { + query: models::QueryCapabilities { + aggregates: None, + variables: Some(models::LeafCapability {}), + explain: Some(models::LeafCapability {}), + nested_fields: models::NestedFieldCapabilities { + aggregates: None, + filter_by: None, + order_by: None, + }, + exists: models::ExistsCapabilities { + nested_collections: None, + }, + }, + mutation: models::MutationCapabilities { + transactional: None, + explain: Some(models::LeafCapability {}), + }, + relationships: None, + } +} + +pub fn capabilities_response() -> models::CapabilitiesResponse { + models::CapabilitiesResponse { + version: models::VERSION.into(), + capabilities: capabilities(), + } +} diff --git a/crates/common/src/capabilities_response.rs b/crates/common/src/capabilities_response.rs deleted file mode 100644 index 753d09a..0000000 --- a/crates/common/src/capabilities_response.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ndc_models as models; - -pub fn capabilities_response() -> models::CapabilitiesResponse { - models::CapabilitiesResponse { - version: "0.1.4".to_string(), - capabilities: models::Capabilities { - query: models::QueryCapabilities { - aggregates: None, - variables: Some(models::LeafCapability {}), - explain: Some(models::LeafCapability {}), - nested_fields: models::NestedFieldCapabilities { - aggregates: None, - filter_by: None, - order_by: None, - }, - }, - mutation: models::MutationCapabilities { - transactional: None, - explain: Some(models::LeafCapability {}), - }, - relationships: None, - }, - } -} diff --git a/crates/common/src/config.rs b/crates/common/src/config.rs index ad36dc5..f8e708a 100644 --- a/crates/common/src/config.rs +++ b/crates/common/src/config.rs @@ -1,4 +1,5 @@ use config_file::{RequestConfigFile, ResponseConfigFile}; +use ndc_models::{ArgumentName, FieldName, FunctionName, ProcedureName, ScalarTypeName, TypeName}; use schema::SchemaDefinition; use std::collections::BTreeMap; pub mod config_file; @@ -20,14 +21,14 @@ pub struct ConnectionConfig { #[derive(Debug, Clone)] pub struct RequestConfig { - pub headers_argument: String, - pub headers_type_name: String, + pub headers_argument: ArgumentName, + pub headers_type_name: ScalarTypeName, pub forward_headers: Vec, } #[derive(Debug, Clone)] pub struct ResponseConfig { - pub headers_field: String, - pub response_field: String, + pub headers_field: FieldName, + pub response_field: FieldName, pub type_name_prefix: String, pub type_name_suffix: String, pub forward_headers: Vec, @@ -36,8 +37,8 @@ pub struct ResponseConfig { impl Default for RequestConfig { fn default() -> Self { Self { - headers_argument: "_headers".to_owned(), - headers_type_name: "_HeaderMap".to_owned(), + headers_argument: "_headers".to_owned().into(), + headers_type_name: "_HeaderMap".to_owned().into(), forward_headers: vec![], } } @@ -46,8 +47,8 @@ impl Default for RequestConfig { impl Default for ResponseConfig { fn default() -> Self { Self { - headers_field: "headers".to_owned(), - response_field: "response".to_owned(), + headers_field: "headers".to_owned().into(), + response_field: "response".to_owned().into(), type_name_prefix: "_".to_owned(), type_name_suffix: "Response".to_owned(), forward_headers: vec![], @@ -89,16 +90,18 @@ impl From for ResponseConfig { } impl ResponseConfig { - pub fn query_response_type_name(&self, query: &str) -> String { + pub fn query_response_type_name(&self, query: &FunctionName) -> TypeName { format!( "{}{}Query{}", self.type_name_prefix, query, self.type_name_suffix ) + .into() } - pub fn mutation_response_type_name(&self, mutation: &str) -> String { + pub fn mutation_response_type_name(&self, mutation: &ProcedureName) -> TypeName { format!( "{}{}Mutation{}", self.type_name_prefix, mutation, self.type_name_suffix ) + .into() } } diff --git a/crates/common/src/config/config_file.rs b/crates/common/src/config/config_file.rs index 1a1a15e..7a90755 100644 --- a/crates/common/src/config/config_file.rs +++ b/crates/common/src/config/config_file.rs @@ -1,3 +1,4 @@ +use ndc_models::{ArgumentName, FieldName, ScalarTypeName}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -10,14 +11,16 @@ pub const CONFIG_SCHEMA_FILE_NAME: &str = "configuration.schema.json"; pub struct ServerConfigFile { #[serde(rename = "$schema")] pub json_schema: String, - /// Connection Configuration for introspection + /// Connection Configuration for introspection. pub introspection: ConnectionConfigFile, - /// Connection configuration for query execution + /// Connection configuration for query execution. pub execution: ConnectionConfigFile, - /// Optional configuration for requests - pub request: RequestConfigFile, - /// Optional configuration for responses - pub response: ResponseConfigFile, + /// Optional configuration for requests. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub request: Option, + /// Optional configuration for responses. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub response: Option, } impl Default for ServerConfigFile { @@ -26,15 +29,17 @@ impl Default for ServerConfigFile { json_schema: CONFIG_SCHEMA_FILE_NAME.to_owned(), execution: ConnectionConfigFile::default(), introspection: ConnectionConfigFile::default(), - request: RequestConfigFile::default(), - response: ResponseConfigFile::default(), + request: None, + response: None, } } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct ConnectionConfigFile { + /// Target GraphQL endpoint URL pub endpoint: ConfigValue, + /// Static headers to include with each request #[serde(skip_serializing_if = "BTreeMap::is_empty", default)] pub headers: BTreeMap, } @@ -51,56 +56,58 @@ impl Default for ConnectionConfigFile { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct RequestConfigFile { - /// Name of the headers argument - /// Must not conflict with any arguments of root fields in the target schema - /// Defaults to "_headers", set to a different value if there is a conflict + /// Name of the headers argument. + /// Must not conflict with any arguments of root fields in the target schema. + /// Defaults to "_headers", set to a different value if there is a conflict. #[serde(skip_serializing_if = "Option::is_none", default)] - pub headers_argument: Option, - /// Name of the headers argument type - /// Must not conflict with other types in the target schema - /// Defaults to "_HeaderMap", set to a different value if there is a conflict + pub headers_argument: Option, + /// Name of the headers argument type. + /// Must not conflict with other types in the target schema. + /// Defaults to "_HeaderMap", set to a different value if there is a conflict. #[serde(skip_serializing_if = "Option::is_none", default)] - pub headers_type_name: Option, - /// List of headers to from the request - /// Defaults to [], AKA no headers/disabled - /// Supports glob patterns eg. "X-Hasura-*" - /// Enabling this requires additional configuration on the ddn side, see docs for more + pub headers_type_name: Option, + /// List of headers to forward from the request. + /// Defaults to [], AKA no headers/disabled. + /// Supports glob patterns eg. "X-Hasura-*". + /// Enabling this requires additional configuration on the ddn side, see docs for more. #[serde(skip_serializing_if = "Option::is_none", default)] pub forward_headers: Option>, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct ResponseConfigFile { - /// Name of the headers field in the response type - /// Defaults to "headers" + /// Name of the headers field in the response type. + /// Defaults to "headers". #[serde(skip_serializing_if = "Option::is_none", default)] - pub headers_field: Option, - /// Name of the response field in the response type - /// Defaults to "response" + pub headers_field: Option, + /// Name of the response field in the response type. + /// Defaults to "response". #[serde(skip_serializing_if = "Option::is_none", default)] - pub response_field: Option, - /// Prefix for response type names - /// Defaults to "_" - /// Generated response type names must be unique once prefix and suffix are applied + pub response_field: Option, + /// Prefix for response type names. + /// Defaults to "_". + /// Generated response type names must be unique once prefix and suffix are applied. #[serde(skip_serializing_if = "Option::is_none", default)] pub type_name_prefix: Option, - /// Suffix for response type names - /// Defaults to "Response" - /// Generated response type names must be unique once prefix and suffix are applied + /// Suffix for response type names. + /// Defaults to "Response". + /// Generated response type names must be unique once prefix and suffix are applied. #[serde(skip_serializing_if = "Option::is_none", default)] pub type_name_suffix: Option, - /// List of headers to from the response - /// Defaults to [], AKA no headers/disabled - /// Supports glob patterns eg. "X-Hasura-*" - /// Enabling this requires additional configuration on the ddn side, see docs for more + /// List of headers to forward from the response. + /// Defaults to [], AKA no headers/disabled. + /// Supports glob patterns eg. "X-Hasura-*". + /// Enabling this requires additional configuration on the ddn side, see docs for more. #[serde(skip_serializing_if = "Option::is_none", default)] pub forward_headers: Option>, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub enum ConfigValue { + /// A static string value #[serde(rename = "value")] Value(String), + /// A reference to an environment variable, from which the value will be read at runtime #[serde(rename = "valueFromEnv")] ValueFromEnv(String), } diff --git a/crates/common/src/config/schema.rs b/crates/common/src/config/schema.rs index 67537b6..3f7fd9b 100644 --- a/crates/common/src/config/schema.rs +++ b/crates/common/src/config/schema.rs @@ -1,14 +1,15 @@ use crate::config::{RequestConfig, ResponseConfig}; use graphql_parser::schema; +use ndc_models::{ArgumentName, FieldName, FunctionName, ProcedureName, ScalarTypeName, TypeName}; use std::{collections::BTreeMap, fmt::Display}; #[derive(Debug, Clone)] pub struct SchemaDefinition { - pub query_type_name: Option, - pub query_fields: BTreeMap, - pub mutation_type_name: Option, - pub mutation_fields: BTreeMap, - pub definitions: BTreeMap, + pub query_type_name: Option, + pub query_fields: BTreeMap, + pub mutation_type_name: Option, + pub mutation_fields: BTreeMap, + pub definitions: BTreeMap, } impl SchemaDefinition { @@ -29,7 +30,7 @@ impl SchemaDefinition { .ok_or(SchemaDefinitionError::MissingSchemaType)?; // note: if there are duplicate definitions, the last one will stick. - let definitions: BTreeMap<_, _> = schema_document + let definitions: BTreeMap = schema_document .definitions .iter() .filter_map(|definition| match definition { @@ -70,7 +71,7 @@ impl SchemaDefinition { }) .collect(); - if definitions.contains_key(&request_config.headers_type_name) { + if definitions.contains_key(request_config.headers_type_name.inner()) { return Err(SchemaDefinitionError::HeaderTypeNameConflict( request_config.headers_type_name.to_owned(), )); @@ -94,7 +95,7 @@ impl SchemaDefinition { if let Some(query_type) = query_type { for field in &query_type.fields { - let query_field = field.name.to_owned(); + let query_field = field.name.to_owned().into(); let response_type = response_config.query_response_type_name(&query_field); if definitions.contains_key(&response_type) { @@ -116,7 +117,7 @@ impl SchemaDefinition { }); } - query_fields.insert(field.name.to_owned(), field_definition); + query_fields.insert(field.name.to_owned().into(), field_definition); } } @@ -139,7 +140,7 @@ impl SchemaDefinition { if let Some(mutation_type) = mutation_type { for field in &mutation_type.fields { - let mutation_field = field.name.to_owned(); + let mutation_field = field.name.to_owned().into(); let response_type = response_config.mutation_response_type_name(&mutation_field); if definitions.contains_key(&response_type) { @@ -161,15 +162,15 @@ impl SchemaDefinition { }); } - mutation_fields.insert(field.name.to_owned(), field_definition); + mutation_fields.insert(field.name.to_owned().into(), field_definition); } } Ok(Self { query_fields, - query_type_name: schema_definition.query.to_owned(), + query_type_name: schema_definition.query.to_owned().map(Into::into), mutation_fields, - mutation_type_name: schema_definition.mutation.to_owned(), + mutation_type_name: schema_definition.mutation.to_owned().map(Into::into), definitions, }) } @@ -190,9 +191,9 @@ impl TypeRef { schema::Type::NonNullType(underlying) => Self::NonNull(Box::new(Self::new(underlying))), } } - pub fn name(&self) -> &str { + pub fn name(&self) -> TypeName { match self { - TypeRef::Named(n) => n.as_str(), + TypeRef::Named(n) => n.to_owned().into(), TypeRef::List(underlying) | TypeRef::NonNull(underlying) => underlying.name(), } } @@ -208,27 +209,27 @@ pub enum TypeDef { description: Option, }, Object { - fields: BTreeMap, + fields: BTreeMap, description: Option, }, InputObject { - fields: BTreeMap, + fields: BTreeMap, description: Option, }, } impl TypeDef { - fn new_scalar(scalar_definition: &schema::ScalarType) -> (String, Self) { + fn new_scalar(scalar_definition: &schema::ScalarType) -> (TypeName, Self) { ( - scalar_definition.name.to_owned(), + scalar_definition.name.to_owned().into(), Self::Scalar { description: scalar_definition.description.to_owned(), }, ) } - fn new_enum(enum_definition: &schema::EnumType) -> (String, Self) { + fn new_enum(enum_definition: &schema::EnumType) -> (TypeName, Self) { ( - enum_definition.name.to_owned(), + enum_definition.name.to_owned().into(), Self::Enum { values: enum_definition .values @@ -239,14 +240,19 @@ impl TypeDef { }, ) } - fn new_object(object_definition: &schema::ObjectType) -> (String, Self) { + fn new_object(object_definition: &schema::ObjectType) -> (TypeName, Self) { ( - object_definition.name.to_owned(), + object_definition.name.to_owned().into(), Self::Object { fields: object_definition .fields .iter() - .map(|field| (field.name.to_owned(), ObjectFieldDefinition::new(field))) + .map(|field| { + ( + field.name.to_owned().into(), + ObjectFieldDefinition::new(field), + ) + }) .collect(), description: object_definition.description.to_owned(), }, @@ -254,16 +260,16 @@ impl TypeDef { } fn new_input_object( input_object_definition: &schema::InputObjectType, - ) -> (String, Self) { + ) -> (TypeName, Self) { ( - input_object_definition.name.to_owned(), + input_object_definition.name.to_owned().into(), Self::InputObject { fields: input_object_definition .fields .iter() .map(|field| { ( - field.name.to_owned(), + field.name.to_owned().into(), InputObjectFieldDefinition::new(field), ) }) @@ -292,7 +298,7 @@ impl EnumValueDefinition { #[derive(Debug, Clone)] pub struct ObjectFieldDefinition { pub r#type: TypeRef, - pub arguments: BTreeMap, + pub arguments: BTreeMap, pub description: Option, } @@ -305,7 +311,7 @@ impl ObjectFieldDefinition { .iter() .map(|argument| { ( - argument.name.to_owned(), + argument.name.to_owned().into(), ObjectFieldArgumentDefinition::new(argument), ) }) @@ -348,22 +354,22 @@ impl InputObjectFieldDefinition { #[derive(Debug, Clone)] pub enum SchemaDefinitionError { MissingSchemaType, - HeaderTypeNameConflict(String), + HeaderTypeNameConflict(ScalarTypeName), QueryHeaderArgumentConflict { - query_field: String, - headers_argument: String, + query_field: FunctionName, + headers_argument: ArgumentName, }, MutationHeaderArgumentConflict { - mutation_field: String, - headers_argument: String, + mutation_field: ProcedureName, + headers_argument: ArgumentName, }, QueryResponseTypeConflict { - query_field: String, - response_type: String, + query_field: FunctionName, + response_type: TypeName, }, MutationResponseTypeConflict { - mutation_field: String, - response_type: String, + mutation_field: ProcedureName, + response_type: TypeName, }, } diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 520471d..7356297 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,4 +1,4 @@ -pub mod capabilities_response; +pub mod capabilities; pub mod client; pub mod config; pub mod schema_response; diff --git a/crates/common/src/schema_response.rs b/crates/common/src/schema_response.rs index 138ca66..5ed3908 100644 --- a/crates/common/src/schema_response.rs +++ b/crates/common/src/schema_response.rs @@ -5,14 +5,14 @@ use crate::config::{ }, RequestConfig, ResponseConfig, }; -use ndc_models as models; +use ndc_models::{self as models, ArgumentName, FieldName, SchemaResponse}; use std::{collections::BTreeMap, iter}; pub fn schema_response( schema: &SchemaDefinition, request: &RequestConfig, response: &ResponseConfig, -) -> models::SchemaResponse { +) -> SchemaResponse { let forward_request_headers = !request.forward_headers.is_empty(); let forward_response_headers = !response.forward_headers.is_empty(); @@ -22,7 +22,7 @@ pub fn schema_response( .filter_map(|(name, typedef)| match typedef { TypeDef::Object { .. } | TypeDef::InputObject { .. } => None, TypeDef::Scalar { description: _ } => Some(( - name.to_owned(), + name.to_owned().into(), models::ScalarType { representation: None, aggregate_functions: BTreeMap::new(), @@ -33,7 +33,7 @@ pub fn schema_response( values, description: _, } => Some(( - name.to_owned(), + name.to_owned().into(), models::ScalarType { representation: Some(models::TypeRepresentation::Enum { one_of: values.iter().map(|value| value.name.to_owned()).collect(), @@ -65,7 +65,7 @@ pub fn schema_response( fields, description, } => Some(( - name.to_owned(), + name.to_owned().into(), models::ObjectType { description: description.to_owned(), fields: fields.iter().map(map_object_field).collect(), @@ -75,7 +75,7 @@ pub fn schema_response( fields, description, } => Some(( - name.to_owned(), + name.to_owned().into(), models::ObjectType { description: description.to_owned(), fields: fields.iter().map(map_input_object_field).collect(), @@ -85,7 +85,7 @@ pub fn schema_response( .collect(); let response_type = - |field: &ObjectFieldDefinition, operation_type: &str, operation_name: &str| { + |field: &ObjectFieldDefinition, operation_type: &str, operation_name: &FieldName| { models::ObjectType { description: Some(format!( "Response type for {operation_type} {operation_name}" @@ -96,7 +96,7 @@ pub fn schema_response( models::ObjectField { description: None, r#type: models::Type::Named { - name: request.headers_type_name.to_owned(), + name: request.headers_type_name.inner().to_owned(), }, arguments: BTreeMap::new(), }, @@ -124,7 +124,7 @@ pub fn schema_response( models::ArgumentInfo { description: None, argument_type: models::Type::Named { - name: request.headers_type_name.to_owned(), + name: request.headers_type_name.inner().to_owned(), }, }, ))) @@ -137,8 +137,8 @@ pub fn schema_response( let response_type_name = response.query_response_type_name(name); object_types.insert( - response_type_name.clone(), - response_type(field, "function", name), + response_type_name.to_owned().into(), + response_type(field, "function", &name.to_string().into()), ); models::Type::Named { @@ -149,7 +149,7 @@ pub fn schema_response( }; functions.push(models::FunctionInfo { - name: name.to_owned(), + name: name.to_string().into(), description: field.description.to_owned(), arguments, result_type, @@ -167,7 +167,7 @@ pub fn schema_response( models::ArgumentInfo { description: None, argument_type: models::Type::Named { - name: request.headers_type_name.to_owned(), + name: request.headers_type_name.inner().to_owned(), }, }, ))) @@ -180,8 +180,8 @@ pub fn schema_response( let response_type_name = response.mutation_response_type_name(name); object_types.insert( - response_type_name.clone(), - response_type(field, "procedure", name), + response_type_name.to_owned().into(), + response_type(field, "procedure", &name.to_string().into()), ); models::Type::Named { @@ -192,7 +192,7 @@ pub fn schema_response( }; procedures.push(models::ProcedureInfo { - name: name.to_owned(), + name: name.to_string().into(), description: field.description.to_owned(), arguments, result_type, @@ -209,8 +209,8 @@ pub fn schema_response( } fn map_object_field( - (name, field): (&String, &ObjectFieldDefinition), -) -> (String, models::ObjectField) { + (name, field): (&FieldName, &ObjectFieldDefinition), +) -> (FieldName, models::ObjectField) { ( name.to_owned(), models::ObjectField { @@ -222,8 +222,8 @@ fn map_object_field( } fn map_argument( - (name, argument): (&String, &ObjectFieldArgumentDefinition), -) -> (String, models::ArgumentInfo) { + (name, argument): (&ArgumentName, &ObjectFieldArgumentDefinition), +) -> (ArgumentName, models::ArgumentInfo) { ( name.to_owned(), models::ArgumentInfo { @@ -234,8 +234,8 @@ fn map_argument( } fn map_input_object_field( - (name, field): (&String, &InputObjectFieldDefinition), -) -> (String, models::ObjectField) { + (name, field): (&FieldName, &InputObjectFieldDefinition), +) -> (FieldName, models::ObjectField) { ( name.to_owned(), models::ObjectField { @@ -249,7 +249,9 @@ fn map_input_object_field( fn typeref_to_ndc_type(typeref: &TypeRef) -> models::Type { match typeref { TypeRef::Named(name) => models::Type::Nullable { - underlying_type: Box::new(models::Type::Named { name: name.into() }), + underlying_type: Box::new(models::Type::Named { + name: name.to_owned().into(), + }), }, TypeRef::List(inner) => models::Type::Nullable { underlying_type: Box::new(models::Type::Array { @@ -257,7 +259,9 @@ fn typeref_to_ndc_type(typeref: &TypeRef) -> models::Type { }), }, TypeRef::NonNull(inner) => match &**inner { - TypeRef::Named(name) => models::Type::Named { name: name.into() }, + TypeRef::Named(name) => models::Type::Named { + name: name.to_owned().into(), + }, TypeRef::List(inner) => models::Type::Array { element_type: Box::new(typeref_to_ndc_type(inner)), }, diff --git a/crates/ndc-graphql-cli/Cargo.toml b/crates/ndc-graphql-cli/Cargo.toml index 4f07c6e..efde05f 100644 --- a/crates/ndc-graphql-cli/Cargo.toml +++ b/crates/ndc-graphql-cli/Cargo.toml @@ -9,7 +9,7 @@ common = { path = "../common" } graphql_client = "0.14.0" graphql-introspection-query = "0.2.0" graphql-parser = "0.4.0" -ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.4" } +ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.6" } schemars = "0.8.16" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" diff --git a/crates/ndc-graphql-cli/src/main.rs b/crates/ndc-graphql-cli/src/main.rs index 4d6e7a0..dde6bac 100644 --- a/crates/ndc-graphql-cli/src/main.rs +++ b/crates/ndc-graphql-cli/src/main.rs @@ -1,6 +1,6 @@ use clap::{Parser, Subcommand, ValueEnum}; use common::{ - capabilities_response::capabilities_response, + capabilities::capabilities_response, config::{ config_file::{ ConfigValue, ServerConfigFile, CONFIG_FILE_NAME, CONFIG_SCHEMA_FILE_NAME, @@ -132,8 +132,8 @@ async fn main() -> Result<(), Box> { .await? .ok_or_else(|| format!("Could not find {SCHEMA_FILE_NAME}"))?; - let request_config = config_file.request.into(); - let response_config = config_file.response.into(); + let request_config = config_file.request.unwrap_or_default().into(); + let response_config = config_file.response.unwrap_or_default().into(); let schema = SchemaDefinition::new(&schema_document, &request_config, &response_config)?; @@ -213,8 +213,8 @@ async fn validate_config( config_file: ServerConfigFile, schema_document: graphql_parser::schema::Document<'_, String>, ) -> Result<(), Box> { - let request_config = config_file.request.into(); - let response_config = config_file.response.into(); + let request_config = config_file.request.unwrap_or_default().into(); + let response_config = config_file.response.unwrap_or_default().into(); let _schema = SchemaDefinition::new(&schema_document, &request_config, &response_config)?; diff --git a/crates/ndc-graphql/Cargo.toml b/crates/ndc-graphql/Cargo.toml index 7a19d93..59d2506 100644 --- a/crates/ndc-graphql/Cargo.toml +++ b/crates/ndc-graphql/Cargo.toml @@ -9,7 +9,7 @@ common = { path = "../common" } glob-match = "0.2.1" graphql-parser = "0.4.0" indexmap = "2.1.0" -ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs", tag = "v0.1.5", package = "ndc-sdk", features = [ +ndc-sdk = { git = "https://github.com/hasura/ndc-sdk-rs", tag = "v0.4.0", package = "ndc-sdk", features = [ "rustls", ], default-features = false } prometheus = "0.13.3" diff --git a/crates/ndc-graphql/src/connector.rs b/crates/ndc-graphql/src/connector.rs index fd7b152..0e29a8f 100644 --- a/crates/ndc-graphql/src/connector.rs +++ b/crates/ndc-graphql/src/connector.rs @@ -2,19 +2,16 @@ use self::state::ServerState; use crate::query_builder::{build_mutation_document, build_query_document}; use async_trait::async_trait; use common::{ - capabilities_response::capabilities_response, + capabilities::capabilities, client::{execute_graphql, GraphQLRequest}, config::ServerConfig, schema_response::schema_response, }; use indexmap::IndexMap; use ndc_sdk::{ - connector::{ - Connector, ExplainError, FetchMetricsError, HealthError, MutationError, QueryError, - SchemaError, - }, + connector::{self, Connector, MutationError, QueryError}, json_response::JsonResponse, - models, + models::{self, FieldName}, }; use std::{collections::BTreeMap, mem}; use tracing::{Instrument, Level}; @@ -32,24 +29,17 @@ impl Connector for GraphQLConnector { fn fetch_metrics( _configuration: &Self::Configuration, _state: &Self::State, - ) -> Result<(), FetchMetricsError> { + ) -> connector::Result<()> { Ok(()) } - async fn health_check( - _configuration: &Self::Configuration, - _state: &Self::State, - ) -> Result<(), HealthError> { - Ok(()) - } - - async fn get_capabilities() -> JsonResponse { - JsonResponse::Value(capabilities_response()) + async fn get_capabilities() -> models::Capabilities { + capabilities() } async fn get_schema( configuration: &Self::Configuration, - ) -> Result, SchemaError> { + ) -> connector::Result> { Ok(JsonResponse::Value(schema_response( &configuration.schema, &configuration.request, @@ -61,15 +51,16 @@ impl Connector for GraphQLConnector { configuration: &Self::Configuration, _state: &Self::State, request: models::QueryRequest, - ) -> Result, ExplainError> { + ) -> connector::Result> { let operation = tracing::info_span!("Build Query Document", internal.visibility = "user") - .in_scope(|| build_query_document(&request, configuration))?; + .in_scope(|| build_query_document(&request, configuration)) + .map_err(|err| QueryError::new_invalid_request(&err))?; let query = serde_json::to_string_pretty(&GraphQLRequest::new( &operation.query, &operation.variables, )) - .map_err(ExplainError::new)?; + .map_err(|err| QueryError::new_invalid_request(&err))?; let details = BTreeMap::from_iter(vec![ ("SQL Query".to_string(), operation.query), @@ -87,16 +78,17 @@ impl Connector for GraphQLConnector { configuration: &Self::Configuration, _state: &Self::State, request: models::MutationRequest, - ) -> Result, ExplainError> { + ) -> connector::Result> { let operation = tracing::info_span!("Build Mutation Document", internal.visibility = "user") - .in_scope(|| build_mutation_document(&request, configuration))?; + .in_scope(|| build_mutation_document(&request, configuration)) + .map_err(|err| MutationError::new_invalid_request(&err))?; let query = serde_json::to_string_pretty(&GraphQLRequest::new( &operation.query, &operation.variables, )) - .map_err(ExplainError::new)?; + .map_err(|err| MutationError::new_invalid_request(&err))?; let details = BTreeMap::from_iter(vec![ ("SQL Query".to_string(), operation.query), @@ -114,22 +106,27 @@ impl Connector for GraphQLConnector { configuration: &Self::Configuration, state: &Self::State, request: models::MutationRequest, - ) -> Result, MutationError> { + ) -> connector::Result> { #[cfg(debug_assertions)] { // this block only present in debug builds, to avoid leaking sensitive information - let request_string = serde_json::to_string(&request).map_err(MutationError::new)?; + let request_string = serde_json::to_string(&request) + .map_err(|err| MutationError::new_invalid_request(&err))?; tracing::event!(Level::DEBUG, "Incoming IR" = request_string); } let operation = - tracing::info_span!("Build Mutation Document", internal.visibility = "user") - .in_scope(|| build_mutation_document(&request, configuration))?; + tracing::info_span!("Build Mutation Document", internal.visibility = "user").in_scope( + || { + build_mutation_document(&request, configuration) + .map_err(|err| MutationError::new_invalid_request(&err)) + }, + )?; let client = state .client(configuration) .await - .map_err(MutationError::new)?; + .map_err(|err| MutationError::new_invalid_request(&err))?; let execution_span = tracing::info_span!("Execute GraphQL Mutation", internal.visibility = "user"); @@ -144,9 +141,9 @@ impl Connector for GraphQLConnector { ) .instrument(execution_span) .await - .map_err(MutationError::new)?; + .map_err(|err| MutationError::new_invalid_request(&err))?; - tracing::info_span!("Process Response").in_scope(|| { + Ok(tracing::info_span!("Process Response").in_scope(|| { if let Some(errors) = response.errors { Err(MutationError::new_unprocessable_content(&errors[0].message) .with_details(serde_json::json!({ "errors": errors }))) @@ -180,7 +177,7 @@ impl Connector for GraphQLConnector { }), }) .collect::, serde_json::Error>>() - .map_err(MutationError::new)?; + .map_err(|err| MutationError::new_invalid_request(&err))?; Ok(JsonResponse::Value(models::MutationResponse { operation_results, @@ -190,30 +187,37 @@ impl Connector for GraphQLConnector { &"No data or errors in response", )) } - }) + })?) } async fn query( configuration: &Self::Configuration, state: &Self::State, request: models::QueryRequest, - ) -> Result, QueryError> { + ) -> connector::Result> { #[cfg(debug_assertions)] { // this block only present in debug builds, to avoid leaking sensitive information - let request_string = serde_json::to_string(&request).map_err(QueryError::new)?; + let request_string = serde_json::to_string(&request) + .map_err(|err| QueryError::new_invalid_request(&err))?; tracing::event!(Level::DEBUG, "Incoming IR" = request_string); } let operation = tracing::info_span!("Build Query Document", internal.visibility = "user") - .in_scope(|| build_query_document(&request, configuration))?; + .in_scope(|| { + build_query_document(&request, configuration) + .map_err(|err| QueryError::new_invalid_request(&err)) + })?; - let client = state.client(configuration).await.map_err(QueryError::new)?; + let client = state + .client(configuration) + .await + .map_err(|err| QueryError::new_invalid_request(&err))?; let execution_span = tracing::info_span!("Execute GraphQL Query", internal.visibility = "user"); - let (headers, response) = execute_graphql::>( + let (headers, response) = execute_graphql::>( &operation.query, operation.variables, &configuration.connection.endpoint, @@ -223,9 +227,9 @@ impl Connector for GraphQLConnector { ) .instrument(execution_span) .await - .map_err(QueryError::new)?; + .map_err(|err| QueryError::new_invalid_request(&err))?; - tracing::info_span!("Process Response").in_scope(|| { + Ok(tracing::info_span!("Process Response").in_scope(|| { if let Some(errors) = response.errors { Err(QueryError::new_unprocessable_content(&errors[0].message) .with_details(serde_json::json!({ "errors": errors }))) @@ -233,16 +237,18 @@ impl Connector for GraphQLConnector { let forward_response_headers = !configuration.response.forward_headers.is_empty(); let row = if forward_response_headers { - let headers = serde_json::to_value(headers).map_err(QueryError::new)?; - let data = serde_json::to_value(data).map_err(QueryError::new)?; + let headers = serde_json::to_value(headers) + .map_err(|err| QueryError::new_invalid_request(&err))?; + let data = serde_json::to_value(data) + .map_err(|err| QueryError::new_invalid_request(&err))?; IndexMap::from_iter(vec![ ( - configuration.response.headers_field.to_string(), + configuration.response.headers_field.to_string().into(), models::RowFieldValue(headers), ), ( - configuration.response.response_field.to_string(), + configuration.response.response_field.to_string().into(), models::RowFieldValue(data), ), ]) @@ -261,6 +267,6 @@ impl Connector for GraphQLConnector { &"No data or errors in response", )) } - }) + })?) } } diff --git a/crates/ndc-graphql/src/connector/setup.rs b/crates/ndc-graphql/src/connector/setup.rs index 5409ec0..fabcfe1 100644 --- a/crates/ndc-graphql/src/connector/setup.rs +++ b/crates/ndc-graphql/src/connector/setup.rs @@ -7,8 +7,8 @@ use common::config::{ }; use graphql_parser::parse_schema; use ndc_sdk::connector::{ - Connector, ConnectorSetup, InitializationError, InvalidNode, InvalidNodes, KeyOrIndex, - LocatedError, ParseError, + self, Connector, ConnectorSetup, InvalidNode, InvalidNodes, KeyOrIndex, LocatedError, + ParseError, }; use std::{ collections::HashMap, @@ -29,15 +29,15 @@ impl ConnectorSetup for GraphQLConnectorSetup { async fn parse_configuration( &self, configuration_dir: impl AsRef + Send, - ) -> Result<::Configuration, ParseError> { - self.read_configuration(configuration_dir).await + ) -> connector::Result<::Configuration> { + Ok(self.read_configuration(configuration_dir).await?) } async fn try_init_state( &self, configuration: &::Configuration, _metrics: &mut prometheus::Registry, - ) -> Result<::State, InitializationError> { + ) -> connector::Result<::State> { Ok(ServerState::new(configuration)) } } @@ -85,8 +85,8 @@ impl GraphQLConnectorSetup { }) })?; - let request_config = config_file.request.into(); - let response_config = config_file.response.into(); + let request_config = config_file.request.unwrap_or_default().into(); + let response_config = config_file.response.unwrap_or_default().into(); let schema = SchemaDefinition::new(&schema_document, &request_config, &response_config) .map_err(|err| { diff --git a/crates/ndc-graphql/src/main.rs b/crates/ndc-graphql/src/main.rs index f7757c6..bcc735d 100644 --- a/crates/ndc-graphql/src/main.rs +++ b/crates/ndc-graphql/src/main.rs @@ -1,9 +1,7 @@ use ndc_graphql::connector::setup::GraphQLConnectorSetup; -use ndc_sdk::default_main::default_main; - -use std::error::Error; +use ndc_sdk::{connector::ErrorResponse, default_main::default_main}; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), ErrorResponse> { default_main::().await } diff --git a/crates/ndc-graphql/src/query_builder.rs b/crates/ndc-graphql/src/query_builder.rs index e79a302..aba99d9 100644 --- a/crates/ndc-graphql/src/query_builder.rs +++ b/crates/ndc-graphql/src/query_builder.rs @@ -12,7 +12,9 @@ use graphql_parser::{ Pos, }; use indexmap::IndexMap; -use ndc_sdk::models::{self, Argument, NestedField}; +use ndc_sdk::models::{ + self, Argument, ArgumentName, FieldName, NestedField, TypeName, VariableName, +}; use std::collections::BTreeMap; pub mod error; @@ -52,6 +54,7 @@ pub fn build_mutation_document( arguments, fields, } => { + let field_name: FieldName = name.to_string().into(); let alias = format!("procedure_{index}"); let field_definition = configuration @@ -73,13 +76,13 @@ pub fn build_mutation_document( let item = selection_set_field( &alias, - name, + &field_name, field_arguments( &procedure_arguments, map_arg, field_definition, &mut parameters, - name, + &field_name, mutation_type_name, &dummy_variables, )?, @@ -142,7 +145,7 @@ pub fn build_query_document( column, fields, arguments, - } if column == "__value" => Ok((fields, arguments)), + } if column == &"__value".into() => Ok((fields, arguments)), models::Field::Column { column, fields: _, @@ -188,13 +191,13 @@ pub fn build_query_document( let item = selection_set_field( &format!("q{}__value", index + 1), - &request.collection, + &request.collection.to_string().into(), field_arguments( &request_arguments, map_arg, root_field_definition, &mut parameters, - &request.collection, + &request.collection.to_string().into(), query_type_name, variables, )?, @@ -228,13 +231,13 @@ pub fn build_query_document( let item = selection_set_field( "__value", - &request.collection, + &request.collection.to_string().into(), field_arguments( &request_arguments, map_arg, root_field_definition, &mut parameters, - &request.collection, + &request.collection.to_string().into(), query_type_name, &dummy_variables, )?, @@ -273,18 +276,21 @@ pub fn build_query_document( } type Headers = BTreeMap; -type Arguments = BTreeMap; +type Arguments = BTreeMap; /// extract the headers argument if present and applicable /// returns the headers for this request, including base headers and forwarded headers fn extract_headers( - arguments: &BTreeMap, + arguments: &BTreeMap, map_argument: M, configuration: &ServerConfig, - variables: &BTreeMap, + variables: &BTreeMap, ) -> Result<(Headers, Arguments), QueryBuilderError> where - M: Fn(&A, &BTreeMap) -> Result, + M: Fn( + &A, + &BTreeMap, + ) -> Result, { let mut request_arguments = BTreeMap::new(); let mut headers = configuration.connection.headers.clone(); @@ -339,13 +345,13 @@ where #[allow(clippy::too_many_arguments)] fn selection_set_field<'a>( alias: &str, - field_name: &str, + field_name: &FieldName, arguments: Vec<(String, Value<'a, String>)>, fields: &Option, field_definition: &ObjectFieldDefinition, parameters: &mut OperationParameters, configuration: &ServerConfig, - variables: &BTreeMap, + variables: &BTreeMap, ) -> Result, QueryBuilderError> { let selection_set = match fields.as_ref().map(underlying_fields) { Some(fields) => { @@ -368,7 +374,8 @@ fn selection_set_field<'a>( let object_name = field_definition.r#type.name(); // subfield selection should only exist on object types - let field_definition = match configuration.schema.definitions.get(object_name) { + let field_definition = match configuration.schema.definitions.get(&object_name) + { Some(TypeDef::Object { fields, description: _, @@ -384,7 +391,7 @@ fn selection_set_field<'a>( }?; selection_set_field( - alias, + &alias.to_string(), field_name, field_arguments( arguments, @@ -392,7 +399,7 @@ fn selection_set_field<'a>( field_definition, parameters, field_name, - object_name, + &object_name, variables, )?, fields, @@ -416,28 +423,31 @@ fn selection_set_field<'a>( }; Ok(Selection::Field(Field { position: pos(), - alias: if alias == field_name { + alias: if alias == field_name.inner() { None } else { Some(alias.to_owned()) }, - name: field_name.to_owned(), + name: field_name.to_string(), arguments, directives: vec![], selection_set, })) } fn field_arguments<'a, A, M>( - arguments: &BTreeMap, + arguments: &BTreeMap, map_argument: M, field_definition: &ObjectFieldDefinition, parameters: &mut OperationParameters, - field_name: &str, - object_name: &str, - variables: &BTreeMap, + field_name: &FieldName, + object_name: &TypeName, + variables: &BTreeMap, ) -> Result)>, QueryBuilderError> where - M: Fn(&A, &BTreeMap) -> Result, + M: Fn( + &A, + &BTreeMap, + ) -> Result, { arguments .iter() @@ -456,14 +466,14 @@ where let value = parameters.insert(name, value, input_type); - Ok((name.to_owned(), value)) + Ok((name.to_string(), value)) }) .collect() } fn map_query_arg( argument: &models::Argument, - variables: &BTreeMap, + variables: &BTreeMap, ) -> Result { match argument { Argument::Variable { name } => variables @@ -475,12 +485,12 @@ fn map_query_arg( } fn map_arg( argument: &serde_json::Value, - _variables: &BTreeMap, + _variables: &BTreeMap, ) -> Result { Ok(argument.to_owned()) } -fn underlying_fields(nested_field: &NestedField) -> &IndexMap { +fn underlying_fields(nested_field: &NestedField) -> &IndexMap { match nested_field { NestedField::Object(obj) => &obj.fields, NestedField::Array(arr) => underlying_fields(&arr.fields), diff --git a/crates/ndc-graphql/src/query_builder/error.rs b/crates/ndc-graphql/src/query_builder/error.rs index 213da84..2bef84e 100644 --- a/crates/ndc-graphql/src/query_builder/error.rs +++ b/crates/ndc-graphql/src/query_builder/error.rs @@ -1,38 +1,41 @@ use std::fmt::Display; -use ndc_sdk::connector::{ExplainError, MutationError, QueryError}; +use ndc_sdk::{ + connector::{MutationError, QueryError}, + models::{ArgumentName, CollectionName, FieldName, ProcedureName, TypeName, VariableName}, +}; #[derive(Debug)] pub enum QueryBuilderError { SchemaDefinitionNotFound, - ObjectTypeNotFound(String), - InputObjectTypeNotFound(String), + ObjectTypeNotFound(TypeName), + InputObjectTypeNotFound(TypeName), NoRequesQueryFields, NoQueryType, NoMutationType, NotSupported(String), QueryFieldNotFound { - field: String, + field: CollectionName, }, MutationFieldNotFound { - field: String, + field: ProcedureName, }, ObjectFieldNotFound { - object: String, - field: String, + object: TypeName, + field: FieldName, }, InputObjectFieldNotFound { - input_object: String, - field: String, + input_object: TypeName, + field: FieldName, }, ArgumentNotFound { - object: String, - field: String, - argument: String, + object: TypeName, + field: FieldName, + argument: ArgumentName, }, MisshapenHeadersArgument(serde_json::Value), Unexpected(String), - MissingVariable(String), + MissingVariable(VariableName), } impl std::error::Error for QueryBuilderError {} @@ -47,11 +50,6 @@ impl From for MutationError { MutationError::new_invalid_request(&value) } } -impl From for ExplainError { - fn from(value: QueryBuilderError) -> Self { - ExplainError::new_invalid_request(&value) - } -} impl Display for QueryBuilderError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/crates/ndc-graphql/src/query_builder/operation_parameters.rs b/crates/ndc-graphql/src/query_builder/operation_parameters.rs index 1ab79ed..f5ab7bc 100644 --- a/crates/ndc-graphql/src/query_builder/operation_parameters.rs +++ b/crates/ndc-graphql/src/query_builder/operation_parameters.rs @@ -5,6 +5,7 @@ use graphql_parser::{ query::{Type, Value, VariableDefinition}, Pos, }; +use ndc_sdk::models::ArgumentName; pub struct OperationParameters { namespace: String, @@ -22,7 +23,7 @@ impl<'c> OperationParameters { } pub fn insert( &mut self, - name: &str, + name: &ArgumentName, value: serde_json::Value, r#type: &TypeRef, ) -> Value<'c, String> { diff --git a/crates/ndc-graphql/tests/config-1/configuration/configuration.schema.json b/crates/ndc-graphql/tests/config-1/configuration/configuration.schema.json index 88c1025..924d54f 100644 --- a/crates/ndc-graphql/tests/config-1/configuration/configuration.schema.json +++ b/crates/ndc-graphql/tests/config-1/configuration/configuration.schema.json @@ -5,16 +5,14 @@ "required": [ "$schema", "execution", - "introspection", - "request", - "response" + "introspection" ], "properties": { "$schema": { "type": "string" }, "introspection": { - "description": "Connection Configuration for introspection", + "description": "Connection Configuration for introspection.", "allOf": [ { "$ref": "#/definitions/ConnectionConfigFile" @@ -22,7 +20,7 @@ ] }, "execution": { - "description": "Connection configuration for query execution", + "description": "Connection configuration for query execution.", "allOf": [ { "$ref": "#/definitions/ConnectionConfigFile" @@ -30,18 +28,24 @@ ] }, "request": { - "description": "Optional configuration for requests", - "allOf": [ + "description": "Optional configuration for requests.", + "anyOf": [ { "$ref": "#/definitions/RequestConfigFile" + }, + { + "type": "null" } ] }, "response": { - "description": "Optional configuration for responses", - "allOf": [ + "description": "Optional configuration for responses.", + "anyOf": [ { "$ref": "#/definitions/ResponseConfigFile" + }, + { + "type": "null" } ] } @@ -54,9 +58,15 @@ ], "properties": { "endpoint": { - "$ref": "#/definitions/ConfigValue" + "description": "Target GraphQL endpoint URL", + "allOf": [ + { + "$ref": "#/definitions/ConfigValue" + } + ] }, "headers": { + "description": "Static headers to include with each request", "type": "object", "additionalProperties": { "$ref": "#/definitions/ConfigValue" @@ -67,6 +77,7 @@ "ConfigValue": { "oneOf": [ { + "description": "A static string value", "type": "object", "required": [ "value" @@ -79,6 +90,7 @@ "additionalProperties": false }, { + "description": "A reference to an environment variable, from which the value will be read at runtime", "type": "object", "required": [ "valueFromEnv" @@ -96,21 +108,21 @@ "type": "object", "properties": { "headersArgument": { - "description": "Name of the headers argument Must not conflict with any arguments of root fields in the target schema Defaults to \"_headers\", set to a different value if there is a conflict", + "description": "Name of the headers argument. Must not conflict with any arguments of root fields in the target schema. Defaults to \"_headers\", set to a different value if there is a conflict.", "type": [ "string", "null" ] }, "headersTypeName": { - "description": "Name of the headers argument type Must not conflict with other types in the target schema Defaults to \"_HeaderMap\", set to a different value if there is a conflict", + "description": "Name of the headers argument type. Must not conflict with other types in the target schema. Defaults to \"_HeaderMap\", set to a different value if there is a conflict.", "type": [ "string", "null" ] }, "forwardHeaders": { - "description": "List of headers to from the request Defaults to [], AKA no headers/disabled Supports glob patterns eg. \"X-Hasura-*\" Enabling this requires additional configuration on the ddn side, see docs for more", + "description": "List of headers to forward from the request. Defaults to [], AKA no headers/disabled. Supports glob patterns eg. \"X-Hasura-*\". Enabling this requires additional configuration on the ddn side, see docs for more.", "type": [ "array", "null" @@ -125,35 +137,35 @@ "type": "object", "properties": { "headersField": { - "description": "Name of the headers field in the response type Defaults to \"headers\"", + "description": "Name of the headers field in the response type. Defaults to \"headers\".", "type": [ "string", "null" ] }, "responseField": { - "description": "Name of the response field in the response type Defaults to \"response\"", + "description": "Name of the response field in the response type. Defaults to \"response\".", "type": [ "string", "null" ] }, "typeNamePrefix": { - "description": "Prefix for response type names Defaults to \"_\" Generated response type names must be unique once prefix and suffix are applied", + "description": "Prefix for response type names. Defaults to \"_\". Generated response type names must be unique once prefix and suffix are applied.", "type": [ "string", "null" ] }, "typeNameSuffix": { - "description": "Suffix for response type names Defaults to \"Response\" Generated response type names must be unique once prefix and suffix are applied", + "description": "Suffix for response type names. Defaults to \"Response\". Generated response type names must be unique once prefix and suffix are applied.", "type": [ "string", "null" ] }, "forwardHeaders": { - "description": "List of headers to from the response Defaults to [], AKA no headers/disabled Supports glob patterns eg. \"X-Hasura-*\" Enabling this requires additional configuration on the ddn side, see docs for more", + "description": "List of headers to forward from the response. Defaults to [], AKA no headers/disabled. Supports glob patterns eg. \"X-Hasura-*\". Enabling this requires additional configuration on the ddn side, see docs for more.", "type": [ "array", "null" diff --git a/crates/ndc-graphql/tests/config-1/mutations/_mutation_request.schema.json b/crates/ndc-graphql/tests/config-1/mutations/_mutation_request.schema.json index 4ccdb61..1b72e10 100644 --- a/crates/ndc-graphql/tests/config-1/mutations/_mutation_request.schema.json +++ b/crates/ndc-graphql/tests/config-1/mutations/_mutation_request.schema.json @@ -941,6 +941,37 @@ } } } + }, + { + "type": "object", + "required": [ + "column_name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "nested_collection" + ] + }, + "column_name": { + "type": "string" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Argument" + } + }, + "field_path": { + "description": "Path to a nested collection via object columns", + "type": "array", + "items": { + "type": "string" + } + } + } } ] }, diff --git a/crates/ndc-graphql/tests/config-1/queries/_query_request.schema.json b/crates/ndc-graphql/tests/config-1/queries/_query_request.schema.json index 6f13801..95c1105 100644 --- a/crates/ndc-graphql/tests/config-1/queries/_query_request.schema.json +++ b/crates/ndc-graphql/tests/config-1/queries/_query_request.schema.json @@ -926,6 +926,37 @@ } } } + }, + { + "type": "object", + "required": [ + "column_name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "nested_collection" + ] + }, + "column_name": { + "type": "string" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Argument" + } + }, + "field_path": { + "description": "Path to a nested collection via object columns", + "type": "array", + "items": { + "type": "string" + } + } + } } ] }, diff --git a/crates/ndc-graphql/tests/config-2/configuration/configuration.schema.json b/crates/ndc-graphql/tests/config-2/configuration/configuration.schema.json index 88c1025..924d54f 100644 --- a/crates/ndc-graphql/tests/config-2/configuration/configuration.schema.json +++ b/crates/ndc-graphql/tests/config-2/configuration/configuration.schema.json @@ -5,16 +5,14 @@ "required": [ "$schema", "execution", - "introspection", - "request", - "response" + "introspection" ], "properties": { "$schema": { "type": "string" }, "introspection": { - "description": "Connection Configuration for introspection", + "description": "Connection Configuration for introspection.", "allOf": [ { "$ref": "#/definitions/ConnectionConfigFile" @@ -22,7 +20,7 @@ ] }, "execution": { - "description": "Connection configuration for query execution", + "description": "Connection configuration for query execution.", "allOf": [ { "$ref": "#/definitions/ConnectionConfigFile" @@ -30,18 +28,24 @@ ] }, "request": { - "description": "Optional configuration for requests", - "allOf": [ + "description": "Optional configuration for requests.", + "anyOf": [ { "$ref": "#/definitions/RequestConfigFile" + }, + { + "type": "null" } ] }, "response": { - "description": "Optional configuration for responses", - "allOf": [ + "description": "Optional configuration for responses.", + "anyOf": [ { "$ref": "#/definitions/ResponseConfigFile" + }, + { + "type": "null" } ] } @@ -54,9 +58,15 @@ ], "properties": { "endpoint": { - "$ref": "#/definitions/ConfigValue" + "description": "Target GraphQL endpoint URL", + "allOf": [ + { + "$ref": "#/definitions/ConfigValue" + } + ] }, "headers": { + "description": "Static headers to include with each request", "type": "object", "additionalProperties": { "$ref": "#/definitions/ConfigValue" @@ -67,6 +77,7 @@ "ConfigValue": { "oneOf": [ { + "description": "A static string value", "type": "object", "required": [ "value" @@ -79,6 +90,7 @@ "additionalProperties": false }, { + "description": "A reference to an environment variable, from which the value will be read at runtime", "type": "object", "required": [ "valueFromEnv" @@ -96,21 +108,21 @@ "type": "object", "properties": { "headersArgument": { - "description": "Name of the headers argument Must not conflict with any arguments of root fields in the target schema Defaults to \"_headers\", set to a different value if there is a conflict", + "description": "Name of the headers argument. Must not conflict with any arguments of root fields in the target schema. Defaults to \"_headers\", set to a different value if there is a conflict.", "type": [ "string", "null" ] }, "headersTypeName": { - "description": "Name of the headers argument type Must not conflict with other types in the target schema Defaults to \"_HeaderMap\", set to a different value if there is a conflict", + "description": "Name of the headers argument type. Must not conflict with other types in the target schema. Defaults to \"_HeaderMap\", set to a different value if there is a conflict.", "type": [ "string", "null" ] }, "forwardHeaders": { - "description": "List of headers to from the request Defaults to [], AKA no headers/disabled Supports glob patterns eg. \"X-Hasura-*\" Enabling this requires additional configuration on the ddn side, see docs for more", + "description": "List of headers to forward from the request. Defaults to [], AKA no headers/disabled. Supports glob patterns eg. \"X-Hasura-*\". Enabling this requires additional configuration on the ddn side, see docs for more.", "type": [ "array", "null" @@ -125,35 +137,35 @@ "type": "object", "properties": { "headersField": { - "description": "Name of the headers field in the response type Defaults to \"headers\"", + "description": "Name of the headers field in the response type. Defaults to \"headers\".", "type": [ "string", "null" ] }, "responseField": { - "description": "Name of the response field in the response type Defaults to \"response\"", + "description": "Name of the response field in the response type. Defaults to \"response\".", "type": [ "string", "null" ] }, "typeNamePrefix": { - "description": "Prefix for response type names Defaults to \"_\" Generated response type names must be unique once prefix and suffix are applied", + "description": "Prefix for response type names. Defaults to \"_\". Generated response type names must be unique once prefix and suffix are applied.", "type": [ "string", "null" ] }, "typeNameSuffix": { - "description": "Suffix for response type names Defaults to \"Response\" Generated response type names must be unique once prefix and suffix are applied", + "description": "Suffix for response type names. Defaults to \"Response\". Generated response type names must be unique once prefix and suffix are applied.", "type": [ "string", "null" ] }, "forwardHeaders": { - "description": "List of headers to from the response Defaults to [], AKA no headers/disabled Supports glob patterns eg. \"X-Hasura-*\" Enabling this requires additional configuration on the ddn side, see docs for more", + "description": "List of headers to forward from the response. Defaults to [], AKA no headers/disabled. Supports glob patterns eg. \"X-Hasura-*\". Enabling this requires additional configuration on the ddn side, see docs for more.", "type": [ "array", "null" diff --git a/crates/ndc-graphql/tests/config-2/mutations/_mutation_request.schema.json b/crates/ndc-graphql/tests/config-2/mutations/_mutation_request.schema.json index 4ccdb61..1b72e10 100644 --- a/crates/ndc-graphql/tests/config-2/mutations/_mutation_request.schema.json +++ b/crates/ndc-graphql/tests/config-2/mutations/_mutation_request.schema.json @@ -941,6 +941,37 @@ } } } + }, + { + "type": "object", + "required": [ + "column_name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "nested_collection" + ] + }, + "column_name": { + "type": "string" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Argument" + } + }, + "field_path": { + "description": "Path to a nested collection via object columns", + "type": "array", + "items": { + "type": "string" + } + } + } } ] }, diff --git a/crates/ndc-graphql/tests/config-2/queries/_query_request.schema.json b/crates/ndc-graphql/tests/config-2/queries/_query_request.schema.json index 6f13801..95c1105 100644 --- a/crates/ndc-graphql/tests/config-2/queries/_query_request.schema.json +++ b/crates/ndc-graphql/tests/config-2/queries/_query_request.schema.json @@ -926,6 +926,37 @@ } } } + }, + { + "type": "object", + "required": [ + "column_name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "nested_collection" + ] + }, + "column_name": { + "type": "string" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Argument" + } + }, + "field_path": { + "description": "Path to a nested collection via object columns", + "type": "array", + "items": { + "type": "string" + } + } + } } ] }, diff --git a/crates/ndc-graphql/tests/config-3/configuration/configuration.schema.json b/crates/ndc-graphql/tests/config-3/configuration/configuration.schema.json index 88c1025..924d54f 100644 --- a/crates/ndc-graphql/tests/config-3/configuration/configuration.schema.json +++ b/crates/ndc-graphql/tests/config-3/configuration/configuration.schema.json @@ -5,16 +5,14 @@ "required": [ "$schema", "execution", - "introspection", - "request", - "response" + "introspection" ], "properties": { "$schema": { "type": "string" }, "introspection": { - "description": "Connection Configuration for introspection", + "description": "Connection Configuration for introspection.", "allOf": [ { "$ref": "#/definitions/ConnectionConfigFile" @@ -22,7 +20,7 @@ ] }, "execution": { - "description": "Connection configuration for query execution", + "description": "Connection configuration for query execution.", "allOf": [ { "$ref": "#/definitions/ConnectionConfigFile" @@ -30,18 +28,24 @@ ] }, "request": { - "description": "Optional configuration for requests", - "allOf": [ + "description": "Optional configuration for requests.", + "anyOf": [ { "$ref": "#/definitions/RequestConfigFile" + }, + { + "type": "null" } ] }, "response": { - "description": "Optional configuration for responses", - "allOf": [ + "description": "Optional configuration for responses.", + "anyOf": [ { "$ref": "#/definitions/ResponseConfigFile" + }, + { + "type": "null" } ] } @@ -54,9 +58,15 @@ ], "properties": { "endpoint": { - "$ref": "#/definitions/ConfigValue" + "description": "Target GraphQL endpoint URL", + "allOf": [ + { + "$ref": "#/definitions/ConfigValue" + } + ] }, "headers": { + "description": "Static headers to include with each request", "type": "object", "additionalProperties": { "$ref": "#/definitions/ConfigValue" @@ -67,6 +77,7 @@ "ConfigValue": { "oneOf": [ { + "description": "A static string value", "type": "object", "required": [ "value" @@ -79,6 +90,7 @@ "additionalProperties": false }, { + "description": "A reference to an environment variable, from which the value will be read at runtime", "type": "object", "required": [ "valueFromEnv" @@ -96,21 +108,21 @@ "type": "object", "properties": { "headersArgument": { - "description": "Name of the headers argument Must not conflict with any arguments of root fields in the target schema Defaults to \"_headers\", set to a different value if there is a conflict", + "description": "Name of the headers argument. Must not conflict with any arguments of root fields in the target schema. Defaults to \"_headers\", set to a different value if there is a conflict.", "type": [ "string", "null" ] }, "headersTypeName": { - "description": "Name of the headers argument type Must not conflict with other types in the target schema Defaults to \"_HeaderMap\", set to a different value if there is a conflict", + "description": "Name of the headers argument type. Must not conflict with other types in the target schema. Defaults to \"_HeaderMap\", set to a different value if there is a conflict.", "type": [ "string", "null" ] }, "forwardHeaders": { - "description": "List of headers to from the request Defaults to [], AKA no headers/disabled Supports glob patterns eg. \"X-Hasura-*\" Enabling this requires additional configuration on the ddn side, see docs for more", + "description": "List of headers to forward from the request. Defaults to [], AKA no headers/disabled. Supports glob patterns eg. \"X-Hasura-*\". Enabling this requires additional configuration on the ddn side, see docs for more.", "type": [ "array", "null" @@ -125,35 +137,35 @@ "type": "object", "properties": { "headersField": { - "description": "Name of the headers field in the response type Defaults to \"headers\"", + "description": "Name of the headers field in the response type. Defaults to \"headers\".", "type": [ "string", "null" ] }, "responseField": { - "description": "Name of the response field in the response type Defaults to \"response\"", + "description": "Name of the response field in the response type. Defaults to \"response\".", "type": [ "string", "null" ] }, "typeNamePrefix": { - "description": "Prefix for response type names Defaults to \"_\" Generated response type names must be unique once prefix and suffix are applied", + "description": "Prefix for response type names. Defaults to \"_\". Generated response type names must be unique once prefix and suffix are applied.", "type": [ "string", "null" ] }, "typeNameSuffix": { - "description": "Suffix for response type names Defaults to \"Response\" Generated response type names must be unique once prefix and suffix are applied", + "description": "Suffix for response type names. Defaults to \"Response\". Generated response type names must be unique once prefix and suffix are applied.", "type": [ "string", "null" ] }, "forwardHeaders": { - "description": "List of headers to from the response Defaults to [], AKA no headers/disabled Supports glob patterns eg. \"X-Hasura-*\" Enabling this requires additional configuration on the ddn side, see docs for more", + "description": "List of headers to forward from the response. Defaults to [], AKA no headers/disabled. Supports glob patterns eg. \"X-Hasura-*\". Enabling this requires additional configuration on the ddn side, see docs for more.", "type": [ "array", "null" diff --git a/crates/ndc-graphql/tests/config-3/mutations/_mutation_request.schema.json b/crates/ndc-graphql/tests/config-3/mutations/_mutation_request.schema.json index 4ccdb61..1b72e10 100644 --- a/crates/ndc-graphql/tests/config-3/mutations/_mutation_request.schema.json +++ b/crates/ndc-graphql/tests/config-3/mutations/_mutation_request.schema.json @@ -941,6 +941,37 @@ } } } + }, + { + "type": "object", + "required": [ + "column_name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "nested_collection" + ] + }, + "column_name": { + "type": "string" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Argument" + } + }, + "field_path": { + "description": "Path to a nested collection via object columns", + "type": "array", + "items": { + "type": "string" + } + } + } } ] }, diff --git a/crates/ndc-graphql/tests/config-3/queries/_query_request.schema.json b/crates/ndc-graphql/tests/config-3/queries/_query_request.schema.json index 6f13801..95c1105 100644 --- a/crates/ndc-graphql/tests/config-3/queries/_query_request.schema.json +++ b/crates/ndc-graphql/tests/config-3/queries/_query_request.schema.json @@ -926,6 +926,37 @@ } } } + }, + { + "type": "object", + "required": [ + "column_name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "nested_collection" + ] + }, + "column_name": { + "type": "string" + }, + "arguments": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Argument" + } + }, + "field_path": { + "description": "Path to a nested collection via object columns", + "type": "array", + "items": { + "type": "string" + } + } + } } ] }, diff --git a/crates/ndc-graphql/tests/query_builder.rs b/crates/ndc-graphql/tests/query_builder.rs index 02225c2..54a3acb 100644 --- a/crates/ndc-graphql/tests/query_builder.rs +++ b/crates/ndc-graphql/tests/query_builder.rs @@ -1,4 +1,4 @@ -use common::capabilities_response::capabilities_response; +use common::capabilities::capabilities_response; use common::config::ServerConfig; use common::{config::config_file::ServerConfigFile, schema_response::schema_response}; use insta::{assert_json_snapshot, assert_snapshot, assert_yaml_snapshot, glob}; @@ -110,3 +110,12 @@ async fn test_generated_schema() { fn test_capabilities() { assert_yaml_snapshot!("Capabilities", capabilities_response()) } + +#[test] +fn configuration_schema() { + assert_snapshot!( + "Configuration JSON Schema", + serde_json::to_string_pretty(&schema_for!(ServerConfigFile)) + .expect("Should serialize schema to json") + ) +} diff --git a/crates/ndc-graphql/tests/snapshots/query_builder__Capabilities.snap b/crates/ndc-graphql/tests/snapshots/query_builder__Capabilities.snap index 26ec2b4..e736e36 100644 --- a/crates/ndc-graphql/tests/snapshots/query_builder__Capabilities.snap +++ b/crates/ndc-graphql/tests/snapshots/query_builder__Capabilities.snap @@ -1,12 +1,13 @@ --- source: crates/ndc-graphql/tests/query_builder.rs -expression: capabilities() +expression: capabilities_response() --- -version: 0.1.4 +version: 0.1.6 capabilities: query: variables: {} explain: {} nested_fields: {} + exists: {} mutation: explain: {} diff --git a/crates/ndc-graphql/tests/snapshots/query_builder__Configuration JSON Schema.snap b/crates/ndc-graphql/tests/snapshots/query_builder__Configuration JSON Schema.snap new file mode 100644 index 0000000..431629e --- /dev/null +++ b/crates/ndc-graphql/tests/snapshots/query_builder__Configuration JSON Schema.snap @@ -0,0 +1,184 @@ +--- +source: crates/ndc-graphql/tests/query_builder.rs +expression: "serde_json::to_string_pretty(&schema_for!(ServerConfigFile)).expect(\"Should serialize schema to json\")" +--- +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ServerConfigFile", + "type": "object", + "required": [ + "$schema", + "execution", + "introspection" + ], + "properties": { + "$schema": { + "type": "string" + }, + "introspection": { + "description": "Connection Configuration for introspection.", + "allOf": [ + { + "$ref": "#/definitions/ConnectionConfigFile" + } + ] + }, + "execution": { + "description": "Connection configuration for query execution.", + "allOf": [ + { + "$ref": "#/definitions/ConnectionConfigFile" + } + ] + }, + "request": { + "description": "Optional configuration for requests.", + "anyOf": [ + { + "$ref": "#/definitions/RequestConfigFile" + }, + { + "type": "null" + } + ] + }, + "response": { + "description": "Optional configuration for responses.", + "anyOf": [ + { + "$ref": "#/definitions/ResponseConfigFile" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "ConnectionConfigFile": { + "type": "object", + "required": [ + "endpoint" + ], + "properties": { + "endpoint": { + "description": "Target GraphQL endpoint URL", + "allOf": [ + { + "$ref": "#/definitions/ConfigValue" + } + ] + }, + "headers": { + "description": "Static headers to include with each request", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ConfigValue" + } + } + } + }, + "ConfigValue": { + "oneOf": [ + { + "description": "A static string value", + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A reference to an environment variable, from which the value will be read at runtime", + "type": "object", + "required": [ + "valueFromEnv" + ], + "properties": { + "valueFromEnv": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + }, + "RequestConfigFile": { + "type": "object", + "properties": { + "headersArgument": { + "description": "Name of the headers argument. Must not conflict with any arguments of root fields in the target schema. Defaults to \"_headers\", set to a different value if there is a conflict.", + "type": [ + "string", + "null" + ] + }, + "headersTypeName": { + "description": "Name of the headers argument type. Must not conflict with other types in the target schema. Defaults to \"_HeaderMap\", set to a different value if there is a conflict.", + "type": [ + "string", + "null" + ] + }, + "forwardHeaders": { + "description": "List of headers to forward from the request. Defaults to [], AKA no headers/disabled. Supports glob patterns eg. \"X-Hasura-*\". Enabling this requires additional configuration on the ddn side, see docs for more.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + }, + "ResponseConfigFile": { + "type": "object", + "properties": { + "headersField": { + "description": "Name of the headers field in the response type. Defaults to \"headers\".", + "type": [ + "string", + "null" + ] + }, + "responseField": { + "description": "Name of the response field in the response type. Defaults to \"response\".", + "type": [ + "string", + "null" + ] + }, + "typeNamePrefix": { + "description": "Prefix for response type names. Defaults to \"_\". Generated response type names must be unique once prefix and suffix are applied.", + "type": [ + "string", + "null" + ] + }, + "typeNameSuffix": { + "description": "Suffix for response type names. Defaults to \"Response\". Generated response type names must be unique once prefix and suffix are applied.", + "type": [ + "string", + "null" + ] + }, + "forwardHeaders": { + "description": "List of headers to forward from the response. Defaults to [], AKA no headers/disabled. Supports glob patterns eg. \"X-Hasura-*\". Enabling this requires additional configuration on the ddn side, see docs for more.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + } + } + } +} From a2920f3dfa868bcb84428abbd40bdcc71ea92be5 Mon Sep 17 00:00:00 2001 From: Benoit Ranque Date: Fri, 27 Sep 2024 12:43:07 -0400 Subject: [PATCH 5/8] add route handlers so we can keep returning MutationError/QueryError after sdk update --- crates/ndc-graphql/src/connector.rs | 226 ++----------------- crates/ndc-graphql/src/connector/mutation.rs | 115 ++++++++++ crates/ndc-graphql/src/connector/query.rs | 112 +++++++++ 3 files changed, 246 insertions(+), 207 deletions(-) create mode 100644 crates/ndc-graphql/src/connector/mutation.rs create mode 100644 crates/ndc-graphql/src/connector/query.rs diff --git a/crates/ndc-graphql/src/connector.rs b/crates/ndc-graphql/src/connector.rs index 0e29a8f..84154fb 100644 --- a/crates/ndc-graphql/src/connector.rs +++ b/crates/ndc-graphql/src/connector.rs @@ -1,20 +1,15 @@ use self::state::ServerState; -use crate::query_builder::{build_mutation_document, build_query_document}; use async_trait::async_trait; -use common::{ - capabilities::capabilities, - client::{execute_graphql, GraphQLRequest}, - config::ServerConfig, - schema_response::schema_response, -}; -use indexmap::IndexMap; +use common::{capabilities::capabilities, config::ServerConfig, schema_response::schema_response}; +use mutation::{handle_mutation, handle_mutation_explain}; use ndc_sdk::{ - connector::{self, Connector, MutationError, QueryError}, + connector::{self, Connector}, json_response::JsonResponse, - models::{self, FieldName}, + models, }; -use std::{collections::BTreeMap, mem}; -use tracing::{Instrument, Level}; +use query::{handle_query, handle_query_explain}; +mod mutation; +mod query; pub mod setup; mod state; @@ -49,57 +44,22 @@ impl Connector for GraphQLConnector { async fn query_explain( configuration: &Self::Configuration, - _state: &Self::State, + state: &Self::State, request: models::QueryRequest, ) -> connector::Result> { - let operation = tracing::info_span!("Build Query Document", internal.visibility = "user") - .in_scope(|| build_query_document(&request, configuration)) - .map_err(|err| QueryError::new_invalid_request(&err))?; - - let query = serde_json::to_string_pretty(&GraphQLRequest::new( - &operation.query, - &operation.variables, + Ok(JsonResponse::Value( + handle_query_explain(configuration, state, request).await?, )) - .map_err(|err| QueryError::new_invalid_request(&err))?; - - let details = BTreeMap::from_iter(vec![ - ("SQL Query".to_string(), operation.query), - ("Execution Plan".to_string(), query), - ( - "Headers".to_string(), - serde_json::to_string(&operation.headers).expect("should convert headers to json"), - ), - ]); - - Ok(JsonResponse::Value(models::ExplainResponse { details })) } async fn mutation_explain( configuration: &Self::Configuration, - _state: &Self::State, + state: &Self::State, request: models::MutationRequest, ) -> connector::Result> { - let operation = - tracing::info_span!("Build Mutation Document", internal.visibility = "user") - .in_scope(|| build_mutation_document(&request, configuration)) - .map_err(|err| MutationError::new_invalid_request(&err))?; - - let query = serde_json::to_string_pretty(&GraphQLRequest::new( - &operation.query, - &operation.variables, + Ok(JsonResponse::Value( + handle_mutation_explain(configuration, state, request).await?, )) - .map_err(|err| MutationError::new_invalid_request(&err))?; - - let details = BTreeMap::from_iter(vec![ - ("SQL Query".to_string(), operation.query), - ("Execution Plan".to_string(), query), - ( - "Headers".to_string(), - serde_json::to_string(&operation.headers).expect("should convert headers to json"), - ), - ]); - - Ok(JsonResponse::Value(models::ExplainResponse { details })) } async fn mutation( @@ -107,87 +67,9 @@ impl Connector for GraphQLConnector { state: &Self::State, request: models::MutationRequest, ) -> connector::Result> { - #[cfg(debug_assertions)] - { - // this block only present in debug builds, to avoid leaking sensitive information - let request_string = serde_json::to_string(&request) - .map_err(|err| MutationError::new_invalid_request(&err))?; - tracing::event!(Level::DEBUG, "Incoming IR" = request_string); - } - - let operation = - tracing::info_span!("Build Mutation Document", internal.visibility = "user").in_scope( - || { - build_mutation_document(&request, configuration) - .map_err(|err| MutationError::new_invalid_request(&err)) - }, - )?; - - let client = state - .client(configuration) - .await - .map_err(|err| MutationError::new_invalid_request(&err))?; - - let execution_span = - tracing::info_span!("Execute GraphQL Mutation", internal.visibility = "user"); - - let (headers, response) = execute_graphql::>( - &operation.query, - operation.variables, - &configuration.connection.endpoint, - &operation.headers, - &client, - &configuration.response.forward_headers, - ) - .instrument(execution_span) - .await - .map_err(|err| MutationError::new_invalid_request(&err))?; - - Ok(tracing::info_span!("Process Response").in_scope(|| { - if let Some(errors) = response.errors { - Err(MutationError::new_unprocessable_content(&errors[0].message) - .with_details(serde_json::json!({ "errors": errors }))) - } else if let Some(mut data) = response.data { - let forward_response_headers = !configuration.response.forward_headers.is_empty(); - - let operation_results = request - .operations - .iter() - .enumerate() - .map(|(index, operation)| match operation { - models::MutationOperation::Procedure { .. } => Ok({ - let alias = format!("procedure_{index}"); - let result = data - .get_mut(&alias) - .map(|val| mem::replace(val, serde_json::Value::Null)) - .unwrap_or(serde_json::Value::Null); - let result = if forward_response_headers { - serde_json::to_value(BTreeMap::from_iter(vec![ - ( - configuration.response.headers_field.to_string(), - serde_json::to_value(&headers)?, - ), - (configuration.response.response_field.to_string(), result), - ]))? - } else { - result - }; - - models::MutationOperationResults::Procedure { result } - }), - }) - .collect::, serde_json::Error>>() - .map_err(|err| MutationError::new_invalid_request(&err))?; - - Ok(JsonResponse::Value(models::MutationResponse { - operation_results, - })) - } else { - Err(MutationError::new_unprocessable_content( - &"No data or errors in response", - )) - } - })?) + Ok(JsonResponse::Value( + handle_mutation(configuration, state, request).await?, + )) } async fn query( @@ -195,78 +77,8 @@ impl Connector for GraphQLConnector { state: &Self::State, request: models::QueryRequest, ) -> connector::Result> { - #[cfg(debug_assertions)] - { - // this block only present in debug builds, to avoid leaking sensitive information - let request_string = serde_json::to_string(&request) - .map_err(|err| QueryError::new_invalid_request(&err))?; - tracing::event!(Level::DEBUG, "Incoming IR" = request_string); - } - - let operation = tracing::info_span!("Build Query Document", internal.visibility = "user") - .in_scope(|| { - build_query_document(&request, configuration) - .map_err(|err| QueryError::new_invalid_request(&err)) - })?; - - let client = state - .client(configuration) - .await - .map_err(|err| QueryError::new_invalid_request(&err))?; - - let execution_span = - tracing::info_span!("Execute GraphQL Query", internal.visibility = "user"); - - let (headers, response) = execute_graphql::>( - &operation.query, - operation.variables, - &configuration.connection.endpoint, - &operation.headers, - &client, - &configuration.response.forward_headers, - ) - .instrument(execution_span) - .await - .map_err(|err| QueryError::new_invalid_request(&err))?; - - Ok(tracing::info_span!("Process Response").in_scope(|| { - if let Some(errors) = response.errors { - Err(QueryError::new_unprocessable_content(&errors[0].message) - .with_details(serde_json::json!({ "errors": errors }))) - } else if let Some(data) = response.data { - let forward_response_headers = !configuration.response.forward_headers.is_empty(); - - let row = if forward_response_headers { - let headers = serde_json::to_value(headers) - .map_err(|err| QueryError::new_invalid_request(&err))?; - let data = serde_json::to_value(data) - .map_err(|err| QueryError::new_invalid_request(&err))?; - - IndexMap::from_iter(vec![ - ( - configuration.response.headers_field.to_string().into(), - models::RowFieldValue(headers), - ), - ( - configuration.response.response_field.to_string().into(), - models::RowFieldValue(data), - ), - ]) - } else { - data - }; - - Ok(JsonResponse::Value(models::QueryResponse(vec![ - models::RowSet { - aggregates: None, - rows: Some(vec![row]), - }, - ]))) - } else { - Err(QueryError::new_unprocessable_content( - &"No data or errors in response", - )) - } - })?) + Ok(JsonResponse::Value( + handle_query(configuration, state, request).await?, + )) } } diff --git a/crates/ndc-graphql/src/connector/mutation.rs b/crates/ndc-graphql/src/connector/mutation.rs new file mode 100644 index 0000000..e0abdff --- /dev/null +++ b/crates/ndc-graphql/src/connector/mutation.rs @@ -0,0 +1,115 @@ +use super::state::ServerState; +use crate::query_builder::build_mutation_document; +use common::{ + client::{execute_graphql, GraphQLRequest}, + config::ServerConfig, +}; +use indexmap::IndexMap; +use ndc_sdk::{connector::MutationError, models}; +use std::{collections::BTreeMap, mem}; +use tracing::{Instrument, Level}; + +pub async fn handle_mutation_explain( + configuration: &ServerConfig, + _state: &ServerState, + request: models::MutationRequest, +) -> Result { + let operation = tracing::info_span!("Build Mutation Document", internal.visibility = "user") + .in_scope(|| build_mutation_document(&request, configuration))?; + + let query = + serde_json::to_string_pretty(&GraphQLRequest::new(&operation.query, &operation.variables)) + .map_err(|err| MutationError::new_invalid_request(&err))?; + + let details = BTreeMap::from_iter(vec![ + ("SQL Query".to_string(), operation.query), + ("Execution Plan".to_string(), query), + ( + "Headers".to_string(), + serde_json::to_string(&operation.headers).expect("should convert headers to json"), + ), + ]); + + Ok(models::ExplainResponse { details }) +} + +pub async fn handle_mutation( + configuration: &ServerConfig, + state: &ServerState, + request: models::MutationRequest, +) -> Result { + #[cfg(debug_assertions)] + { + // this block only present in debug builds, to avoid leaking sensitive information + let request_string = serde_json::to_string(&request) + .map_err(|err| MutationError::new_invalid_request(&err))?; + tracing::event!(Level::DEBUG, "Incoming IR" = request_string); + } + + let operation = tracing::info_span!("Build Mutation Document", internal.visibility = "user") + .in_scope(|| build_mutation_document(&request, configuration))?; + + let client = state + .client(configuration) + .await + .map_err(|err| MutationError::new_invalid_request(&err))?; + + let execution_span = + tracing::info_span!("Execute GraphQL Mutation", internal.visibility = "user"); + + let (headers, response) = execute_graphql::>( + &operation.query, + operation.variables, + &configuration.connection.endpoint, + &operation.headers, + &client, + &configuration.response.forward_headers, + ) + .instrument(execution_span) + .await + .map_err(|err| MutationError::new_unprocessable_content(&err))?; + + tracing::info_span!("Process Response").in_scope(|| { + if let Some(errors) = response.errors { + Err(MutationError::new_unprocessable_content(&errors[0].message) + .with_details(serde_json::json!({ "errors": errors }))) + } else if let Some(mut data) = response.data { + let forward_response_headers = !configuration.response.forward_headers.is_empty(); + + let operation_results = request + .operations + .iter() + .enumerate() + .map(|(index, operation)| match operation { + models::MutationOperation::Procedure { .. } => Ok({ + let alias = format!("procedure_{index}"); + let result = data + .get_mut(&alias) + .map(|val| mem::replace(val, serde_json::Value::Null)) + .unwrap_or(serde_json::Value::Null); + let result = if forward_response_headers { + serde_json::to_value(BTreeMap::from_iter(vec![ + ( + configuration.response.headers_field.to_string(), + serde_json::to_value(&headers)?, + ), + (configuration.response.response_field.to_string(), result), + ]))? + } else { + result + }; + + models::MutationOperationResults::Procedure { result } + }), + }) + .collect::, serde_json::Error>>() + .map_err(|err| MutationError::new_unprocessable_content(&err))?; + + Ok(models::MutationResponse { operation_results }) + } else { + Err(MutationError::new_unprocessable_content( + &"No data or errors in response", + )) + } + }) +} diff --git a/crates/ndc-graphql/src/connector/query.rs b/crates/ndc-graphql/src/connector/query.rs new file mode 100644 index 0000000..2c9acaf --- /dev/null +++ b/crates/ndc-graphql/src/connector/query.rs @@ -0,0 +1,112 @@ +use super::state::ServerState; +use crate::query_builder::build_query_document; +use common::{ + client::{execute_graphql, GraphQLRequest}, + config::ServerConfig, +}; +use indexmap::IndexMap; +use ndc_sdk::{ + connector::QueryError, + models::{self, FieldName}, +}; +use std::collections::BTreeMap; +use tracing::{Instrument, Level}; + +pub async fn handle_query_explain( + configuration: &ServerConfig, + _state: &ServerState, + request: models::QueryRequest, +) -> Result { + let operation = tracing::info_span!("Build Query Document", internal.visibility = "user") + .in_scope(|| build_query_document(&request, configuration)) + .map_err(|err| QueryError::new_invalid_request(&err))?; + + let query = + serde_json::to_string_pretty(&GraphQLRequest::new(&operation.query, &operation.variables)) + .map_err(|err| QueryError::new_invalid_request(&err))?; + + let details = BTreeMap::from_iter(vec![ + ("SQL Query".to_string(), operation.query), + ("Execution Plan".to_string(), query), + ( + "Headers".to_string(), + serde_json::to_string(&operation.headers).expect("should convert headers to json"), + ), + ]); + + Ok(models::ExplainResponse { details }) +} + +pub async fn handle_query( + configuration: &ServerConfig, + state: &ServerState, + request: models::QueryRequest, +) -> Result { + #[cfg(debug_assertions)] + { + // this block only present in debug builds, to avoid leaking sensitive information + let request_string = + serde_json::to_string(&request).map_err(|err| QueryError::new_invalid_request(&err))?; + tracing::event!(Level::DEBUG, "Incoming IR" = request_string); + } + + let operation = tracing::info_span!("Build Query Document", internal.visibility = "user") + .in_scope(|| build_query_document(&request, configuration))?; + + let client = state + .client(configuration) + .await + .map_err(|err| QueryError::new_invalid_request(&err))?; + + let execution_span = tracing::info_span!("Execute GraphQL Query", internal.visibility = "user"); + + let (headers, response) = execute_graphql::>( + &operation.query, + operation.variables, + &configuration.connection.endpoint, + &operation.headers, + &client, + &configuration.response.forward_headers, + ) + .instrument(execution_span) + .await + .map_err(|err| QueryError::new_invalid_request(&err))?; + + tracing::info_span!("Process Response").in_scope(|| { + if let Some(errors) = response.errors { + Err(QueryError::new_unprocessable_content(&errors[0].message) + .with_details(serde_json::json!({ "errors": errors }))) + } else if let Some(data) = response.data { + let forward_response_headers = !configuration.response.forward_headers.is_empty(); + + let row = if forward_response_headers { + let headers = serde_json::to_value(headers) + .map_err(|err| QueryError::new_unprocessable_content(&err))?; + let data = serde_json::to_value(data) + .map_err(|err| QueryError::new_unprocessable_content(&err))?; + + IndexMap::from_iter(vec![ + ( + configuration.response.headers_field.to_string().into(), + models::RowFieldValue(headers), + ), + ( + configuration.response.response_field.to_string().into(), + models::RowFieldValue(data), + ), + ]) + } else { + data + }; + + Ok(models::QueryResponse(vec![models::RowSet { + aggregates: None, + rows: Some(vec![row]), + }])) + } else { + Err(QueryError::new_unprocessable_content( + &"No data or errors in response", + )) + } + }) +} From 7c6d974361385ed6721cb53d060656d9b63efcae Mon Sep 17 00:00:00 2001 From: Benoit Ranque Date: Fri, 27 Sep 2024 12:53:28 -0400 Subject: [PATCH 6/8] trim down readme, point to docs --- README.md | 405 +----------------------------------------------------- 1 file changed, 2 insertions(+), 403 deletions(-) diff --git a/README.md b/README.md index e0915ec..d3abac5 100644 --- a/README.md +++ b/README.md @@ -53,419 +53,18 @@ Below, you'll find a matrix of all supported features for the GraphQL connector: #### Other Considerations and limitations -* The V2 and V3 projects must share an auth provider in order to support JWT query authorization - - Either pre-shared credentials, or shared auth providers are supported - - Seperate providers for engine and upstream are not currently supported * Error formatting - The format of errors from the connector does not currently match V2 error formatting - No "partial error" or "multiple errors" responses -* `_headers` argument needs to be removed from commands by hand - - This should not be required when it is provided by argument presets - - This is a bug which will be addressed in future -* Role based schemas - - Only a single schema is supported. - - We recommend creating different subgraphs for different roles. -* Pulling items out of session - Currently no session parsing and refined mappings are supported * Pattern matching in request header forwarding configuration - This uses simple glob patterns - More advanced matching and extraction is not currently supported * Response headers only allow at most one header per name - For example you may only use one `Set-Cookie` response header -## Before you get Started - -[Prerequisites or recommended steps before using the connector.] - -1. The [DDN CLI](https://hasura.io/docs/3.0/cli/installation) and [Docker](https://docs.docker.com/engine/install/) installed -2. A [supergraph](https://hasura.io/docs/3.0/getting-started/init-supergraph) -3. A [subgraph](https://hasura.io/docs/3.0/getting-started/init-subgraph) - - -The steps below explain how to Initialize and configure a connector for local development. You can learn how to deploy a connector — after it's been configured — [here](https://hasura.io/docs/3.0/getting-started/deployment/deploy-a-connector). - ## Using the GraphQL Connector -The high-level steps for working with the GraphQL connector follows -the same pattern as any connector: - -* Add the connector -* Configure the connector -* Integrate into your supergraph -* Configure in your supergraph - -The main focus with respect to the GraphQL connector will be: - -* Configuring the introspection role -* Configuring the header passthrough behaviour -* Configuring the argument preset and response header behaviour in the connector link -* Replicating specific permissions in models - -### Authenticate your CLI session - -```bash -ddn auth login -``` - -### Add the connector - -The connector has been designed to work best in its own subgraph. While it is possible to -use in an existing subgraph, we recommend [creating a subgraph](https://hasura.io/docs/3.0/getting-started/init-subgraph)for the purposes of connecting to a GraphQL schema with this connector. -Once you are operating within a subgraph you can add the GraphQL connector: - -```sh -ddn subgraph init app --dir app --target-supergraph supergraph.local.yaml --target-supergraph supergraph.cloud.yaml -ddn connector init graphql --subgraph ./app/subgraph.yaml --add-to-compose-file compose.yaml --configure-port 8081 --hub-connector hasura/graphql -``` - -
-An example of a full connector configuration. - -```json -{ - "$schema": "configuration.schema.json", - "execution": { - "endpoint": { - "value": "https://my-app.hasura.app/v1/graphql" - }, - "headers": { - "Content-Type": { - "value": "application/json" - } - } - }, - "introspection": { - "endpoint": { - "value": "https://my-app.hasura.app/v1/graphql" - }, - "headers": { - "X-Hasura-Admin-Secret": { - "value": "hunter2" - }, - "Content-Type": { - "value": "application/json" - } - } - }, - "request": { - "headersArgument": "_headers", - "forwardHeaders": [ - "Authorization" - ] - }, - "response": {} -} -``` -
- -**Note: Any literal `"value"` in the configuration can be replaced with `"valueFromEnv"` in order to read from environments.** - - -### Configuring the introspection role - -Once the connector has been added it will expose its configuration in -`config/configuration.json`. You should update some values in this config before -performing introspection. - -The configuration is split into request/connection/introspection sections. -You should update the introspection of the configuration to have the -`x-hasura-admin-secret` and `x-hasura-role` headers set in order to allow -the introspection request to be executed. - -You may wish to provide a pre-baked value for `x-hasura-role` if you want -to have introspection occur as the desired role, or more interestingly, -if you wish to have requests execute under a role with no forwarded -auth credentials required. - -```json -{ - ... - "introspection": { - "endpoint": { - "value": "https://my-hasura-v2-service/v1/graphql" - }, - "headers": { - "X-Hasura-Admin-Secret": { - "value": "my-super-secret-admin-secret" - }, - "Content-Type": { - "value": "application/json" - } - } - } -} -``` - -Without an explicit role set this will use the admin role to fetch the schema, which may or may not be appropriate for your application! - -### Configuring the Execution Role - -You may also configure the connector for execution (i.e. GraphQL request) time behaviour. -This could include pointing to a different instance, forwarding different headers, -returning different response headers, etc. - -You may also set the headers argument name, which by default will be `_headers`. - -```json -{ - ... - "execution": { - "endpoint": { - "value": "https://my-hasura-v2-service/v1/graphql" - }, - "headers": { - "Content-Type": { - "value": "application/json" - } - } - }, - "request": { - "headersArgument": "_headers", - "forwardHeaders": [ "Authorization" ] - } -} -``` - -### Performing Introspection - -Once the connector introspection configuration is updated, you can perform an update -in order to fetch the schema for use and then add the connector link: - -```sh -ddn connector introspect --connector ./app/connector/graphql/connector.local.yaml -ddn connector-link add graphql --configure-host http://local.hasura.dev:8081 --subgraph app/subgraph.yaml --target-env-file app/.env.app.local -``` - -At this point you will want to have your connector running locally if you are performing local development: - -```sh -docker compose up -d --build app_graphql -``` - - -### Configuring the header passthrough behaviour - -The connector link will probably need to be updated to pass through headers. - -This is done with the following metadata configuration: - -```yaml -kind: DataConnectorLink -version: v1 -definition: - name: graphql - url: - readWriteUrls: - read: - valueFromEnv: APP_GRAPHQL_READ_URL - write: - valueFromEnv: APP_GRAPHQL_WRITE_URL - schema: - # This is read from the connector schema configuration - argumentPresets: - - argument: headers - value: - httpHeaders: - forward: - - X-Hasura-Admin-Secret - - Authorization - additional: {} -``` - -You may also want to configuring the response header behaviour at this point if you -need to have response headers passed back to the client. - -If the connector is "pre-baked" in that it does not require any request credentials -to use, then you may omit the header forwarding entirely. This may be appropriate -if there is a public readonly instance of Hasura available for use. - - -### Integrate into your supergraph - -Track the associated commands (functions/procedures) in your supergraph: - -```sh -ddn connector-link update graphql --subgraph app/subgraph.yaml --env-file app/.env.app.local --add-all-resources -``` - -If you just need to update your existing connector you can run the update command again. - -This will add/update the commands defined in the subgraph metadata defined in `metadata/graphql/commands`. Currently you will want to remove the `headers` argument defined in these commands -as this will be supplied automatically by the argument presets. - -An example of a generated command metadata snippet: - -```yaml ---- -kind: Command -version: v1 -definition: - name: Child - outputType: ChildQueryResponse! - arguments: - - name: headers # REMOVE - type: HeaderMap! # REMOVE - - name: distinctOn - type: "[ChildSelectColumn!]" - description: distinct select on columns - - name: limit - type: Int - description: limit the number of rows returned - - name: offset - type: Int - description: skip the first n rows. Use only with order_by - - name: orderBy - type: "[ChildOrderBy!]" - description: sort the rows by one or more columns - - name: where - type: ChildBoolExp - description: filter the rows returned - source: - dataConnectorName: graphql - dataConnectorCommand: - function: child - argumentMapping: - headers: _headers # REMOVE - distinctOn: distinct_on - limit: limit - offset: offset - orderBy: order_by - where: where - graphql: - rootFieldName: app_child - rootFieldKind: Query - description: 'fetch data from the table: "child"' -``` - -**Note: You will currently need to remove the references to the header argument - if it is being included through the argument presets feature! - See the `# REMOVE` comments. You will encounter a build error if you - forget to do this.** - -### Testing your Supergraph - -You can then build and test your supergraph: - -```sh -ddn supergraph build local --supergraph supergraph.local.yaml --output-dir engine --subgraph-env-file=app:./app/.env.app.local -``` - -See your docker compose file for information on exposed service ports. - - -### Replicating specific permissions in models - -While this may be sufficient if your schema and role matches, -if you wish to have additionally restrictive permissions imposed you may -do so at the model level with [the Hasura V3 permissions system](https://hasura.io/docs/3.0/supergraph-modeling/permissions). - - -### Removing namespacing - -While integrating the connector may be sufficient for building new applications, -if you wish to preserve API behaviour for existing V2 applications you may wish -to alther the subgraph namspacing configuration in order to return the API -the one that matches the original upstream GraphQL source. - -You can remove the namespace for a subgraph as per the following example -of a supergraph configuratgion metadata: - -```yaml -kind: Supergraph -version: v1 -definition: - project: - supergraph_globals: - generator: - rootPath: ./supergraph - envFile: ./supergraph/.env.supergraph - includePaths: - - ./supergraph/auth-config.hml - - ./supergraph/compatibility-config.hml - - ./supergraph/graphql-config.hml - subgraphs: - app: - generator: - rootPath: app/metadata - envFile: app/.env.app - namingConvention: "no-mod" - includePaths: - - app -``` - -Note the `namingConvention` field! - - -## Schemas - -One limitation of the current state of the connector is that it can only serve -a single schema. While requests can adopt any role that the auth behaviour of -the V2 instance (provided the same JWT auth provider is used) if the schema -changes per-role then this won't be reflected in the Hasura engine's schema -without additional permissions being configured. - -As such the current recommended pattern is to select a single "application user" -schema to use for your application and perform the connector configuration -introspection using this user role. - -If admin or additional roles are required for non application use then this can be -done directly with the V2 instance without having to expose it via the connector. - -Additional application roles can be added via multiple instances of the connector -with different roles used for introspection, however this has the limitation of -requiring distinct namespaces which may not be ideal. - -Finally if you need to share a namespace but have different roles you can currently -just expose the admin schema and have each role connect and issue their requests -with the correct role with permissions enforced correctly at runtime, but the -schema not reflecting the correct restrictive permissions at development time. - -In future we wish to be able to replicate the correct permissions in the engine -assisted by tooling which will resolve these issues. - -See the [Removing namespacing](#removing-namespacing) section for more details -on removing default namespacing for fields and types. - -## Authorization Use-Cases - -There are several ways we anticipate users may wish to integrate upstream GraphQL schemas. - - -### Admin secret mode - -Since arbitrary headers can be configured as presets in the connector, you may choose -to set the admin-secret or pre-shared key. This will allow the engine to make upstream -requests via the connector without having to deal with any user-level auth concerns. - -This may be useful for prototyping or development, but it is dangerous for several reasons: - -* Credentials for the V2 instance need to be saved -* The V2 instance acts on behalf of the configured user instead of the application user -* Auditing requests may be more difficult -* Requests may inadvertantly or deliberately interact with data that the application user - should not be able to access or modify - - -### Shared JWT provider mode - -This is the recommended execution mode. - -The JWT provider / authenticator is shared between the V2 and V3 instance and as such -all requests are able to be understood sematically by both instances. While -permissions can be configured explicitly in V3, they will automatically also be enforced -by the V2 instance as per its configuration. - -One consideration to make here is that the JWT TTL slack should take into account execution -time in case a request times out after receipt by V3 engine but before receipt from -the V2 instance. - -If the `exp` JWT claim and client behaviour are not set up to ensure that this scenario -doesn't occur then you *could* use `ClockSkew` configuration in order to account for the -latency between the V3 and V2 instances. - - -### Independent auth scenario - -Users may with to have seperate providers for V3 Engine and V2 Upstream source. +This connector should be used with Hasura DDN. +Please see the [relevant documentation](https://hasura.info/graphql-getting-started). -This is not currently supported, however, we would like to add support for this in future. From c444ceb5c28d0048d1fd7c1b857657d2d4f29a26 Mon Sep 17 00:00:00 2001 From: Benoit Ranque Date: Fri, 27 Sep 2024 12:53:46 -0400 Subject: [PATCH 7/8] bump version for release --- CHANGELOG.md | 3 +++ Cargo.lock | 6 +++--- Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80789d5..71b279d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.0] + - Implement foreach capability. Instead of producing multiple parallel requests, we produce a single, larger request to send to the target endpoint. - Fix bug where introspection including interfaces would fail to parse in some circumstances - Config now defaults to asking for a `GRAPHQL_ENDPOINT` env var - Fix a bug where default values were not parsed as graphql values, and instead used as string literals - CLI: Implement `print-schema-and-capabilities` command, allowing local dev to update config & schema without starting a connector instance +- Update to latest connector SDK version (0.4.0) ## [0.1.3] diff --git a/Cargo.lock b/Cargo.lock index 7d6584e..3e4c2d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,7 +367,7 @@ dependencies = [ [[package]] name = "common" -version = "0.1.3" +version = "0.2.0" dependencies = [ "async-trait", "glob-match", @@ -1192,7 +1192,7 @@ dependencies = [ [[package]] name = "ndc-graphql" -version = "0.1.3" +version = "0.2.0" dependencies = [ "async-trait", "common", @@ -1212,7 +1212,7 @@ dependencies = [ [[package]] name = "ndc-graphql-cli" -version = "0.1.3" +version = "0.2.0" dependencies = [ "clap", "common", diff --git a/Cargo.toml b/Cargo.toml index 8f78d73..26981a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["crates/ndc-graphql", "crates/ndc-graphql-cli", "crates/common"] resolver = "2" -package.version = "0.1.3" +package.version = "0.2.0" package.edition = "2021" # insta performs better in release mode From 46b3a35fd8a471dfcdd79ed1f3cc36143164b90f Mon Sep 17 00:00:00 2001 From: Benoit Ranque Date: Fri, 27 Sep 2024 13:02:04 -0400 Subject: [PATCH 8/8] add noop upgradeConfiguration cli command --- ci/templates/connector-metadata.yaml | 1 + crates/ndc-graphql-cli/src/main.rs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ci/templates/connector-metadata.yaml b/ci/templates/connector-metadata.yaml index 8991acd..7f60a62 100644 --- a/ci/templates/connector-metadata.yaml +++ b/ci/templates/connector-metadata.yaml @@ -9,6 +9,7 @@ supportedEnvironmentVariables: commands: update: hasura-ndc-graphql update printSchemaAndCapabilities: hasura-ndc-graphql print-schema-and-capabilities + upgradeConfiguration: hasura-ndc-graphql upgrade-configuration cliPlugin: name: ndc-graphql version: "${CLI_VERSION}" diff --git a/crates/ndc-graphql-cli/src/main.rs b/crates/ndc-graphql-cli/src/main.rs index dde6bac..b74d258 100644 --- a/crates/ndc-graphql-cli/src/main.rs +++ b/crates/ndc-graphql-cli/src/main.rs @@ -72,6 +72,7 @@ enum Command { Validate {}, Watch {}, PrintSchemaAndCapabilities {}, + UpgradeConfiguration {}, } #[derive(Clone, ValueEnum)] @@ -149,7 +150,10 @@ async fn main() -> Result<(), Box> { .expect("Schema and capabilities should serialize to JSON") ) } - } + Command::UpgradeConfiguration {} => { + println!("Upgrade Configuration command is currently a NOOP") + } + }; Ok(()) }