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" + } + } + } + } + } +}