Add secret attribute for arguments, they will not appear in the log. #463
This commit is contained in:
parent
709bb49e07
commit
a9ac598e2e
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- Add `secret` attribute for arguments, they will not appear in the log.
|
||||
|
||||
```rust
|
||||
#[Object]
|
||||
impl Query {
|
||||
async fn login(&self, username:String, #[graphql(secret)] password: String) -> i32 {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## [2.8.0] 2021-04-05
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -185,6 +185,7 @@ pub struct Argument {
|
|||
pub validator: Option<Meta>,
|
||||
pub key: bool, // for entity
|
||||
pub visible: Option<Visible>,
|
||||
pub secret: bool,
|
||||
}
|
||||
|
||||
#[derive(FromMeta, Default)]
|
||||
|
@ -322,6 +323,8 @@ pub struct InputObjectField {
|
|||
pub skip: bool,
|
||||
#[darling(default)]
|
||||
pub visible: Option<Visible>,
|
||||
#[darling(default)]
|
||||
pub secret: bool,
|
||||
}
|
||||
|
||||
#[derive(FromDeriveInput)]
|
||||
|
@ -357,6 +360,8 @@ pub struct InterfaceFieldArgument {
|
|||
pub default_with: Option<LitStr>,
|
||||
#[darling(default)]
|
||||
pub visible: Option<Visible>,
|
||||
#[darling(default)]
|
||||
pub secret: bool,
|
||||
}
|
||||
|
||||
#[derive(FromMeta)]
|
||||
|
@ -441,6 +446,7 @@ pub struct SubscriptionFieldArgument {
|
|||
pub default_with: Option<LitStr>,
|
||||
pub validator: Option<Meta>,
|
||||
pub visible: Option<Visible>,
|
||||
pub secret: bool,
|
||||
}
|
||||
|
||||
#[derive(FromMeta, Default)]
|
||||
|
|
|
@ -122,6 +122,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
|
|||
}
|
||||
})
|
||||
.unwrap_or_else(|| quote!(::std::option::Option::None));
|
||||
let secret = field.secret;
|
||||
|
||||
if let Some(default) = default {
|
||||
get_fields.push(quote! {
|
||||
|
@ -161,6 +162,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
|
|||
default_value: #schema_default,
|
||||
validator: #validator,
|
||||
visible: #visible,
|
||||
is_secret: #secret,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
|
|
@ -184,6 +184,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
|
|||
default,
|
||||
default_with,
|
||||
visible,
|
||||
secret,
|
||||
} in args
|
||||
{
|
||||
let ident = Ident::new(name, Span::call_site());
|
||||
|
@ -229,6 +230,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
|
|||
default_value: #schema_default,
|
||||
validator: ::std::option::Option::None,
|
||||
visible: #visible,
|
||||
is_secret: #secret,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -350,6 +350,7 @@ pub fn generate(
|
|||
default_with,
|
||||
validator,
|
||||
visible,
|
||||
secret,
|
||||
..
|
||||
},
|
||||
) in &args
|
||||
|
@ -392,6 +393,7 @@ pub fn generate(
|
|||
default_value: #schema_default,
|
||||
validator: #validator,
|
||||
visible: #visible,
|
||||
is_secret: #secret,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -152,6 +152,7 @@ pub fn generate(
|
|||
default_with,
|
||||
validator,
|
||||
visible: arg_visible,
|
||||
secret,
|
||||
},
|
||||
) in &args
|
||||
{
|
||||
|
@ -194,6 +195,7 @@ pub fn generate(
|
|||
default_value: #schema_default,
|
||||
validator: #validator,
|
||||
visible: #visible,
|
||||
is_secret: #secret,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use std::fmt::{self, Display, Formatter};
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::lock::Mutex;
|
||||
|
||||
use crate::extensions::{
|
||||
Extension, ExtensionContext, ExtensionFactory, NextParseQuery, NextResolve, ResolveInfo,
|
||||
Extension, ExtensionContext, ExtensionFactory, NextExecute, NextParseQuery,
|
||||
};
|
||||
use crate::parser::types::{ExecutableDocument, OperationType, Selection};
|
||||
use crate::{PathSegment, ServerError, ServerResult, Value, Variables};
|
||||
use crate::{PathSegment, Response, ServerResult, Variables};
|
||||
|
||||
/// Logger extension
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "log")))]
|
||||
|
@ -15,25 +13,11 @@ pub struct Logger;
|
|||
|
||||
impl ExtensionFactory for Logger {
|
||||
fn create(&self) -> Arc<dyn Extension> {
|
||||
Arc::new(LoggerExtension {
|
||||
inner: Mutex::new(Inner {
|
||||
enabled: true,
|
||||
query: String::new(),
|
||||
variables: Default::default(),
|
||||
}),
|
||||
})
|
||||
Arc::new(LoggerExtension)
|
||||
}
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
enabled: bool,
|
||||
query: String,
|
||||
variables: Variables,
|
||||
}
|
||||
|
||||
struct LoggerExtension {
|
||||
inner: Mutex<Inner>,
|
||||
}
|
||||
struct LoggerExtension;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Extension for LoggerExtension {
|
||||
|
@ -44,82 +28,53 @@ impl Extension for LoggerExtension {
|
|||
variables: &Variables,
|
||||
next: NextParseQuery<'_>,
|
||||
) -> ServerResult<ExecutableDocument> {
|
||||
let mut inner = self.inner.lock().await;
|
||||
inner.query = query.replace(char::is_whitespace, "");
|
||||
inner.variables = variables.clone();
|
||||
let document = next.run(ctx, query, variables).await?;
|
||||
let is_schema = document
|
||||
.operations
|
||||
.iter()
|
||||
.filter(|(_, operation)| operation.node.ty == OperationType::Query)
|
||||
.any(|(_, operation)| operation.node.selection_set.node.items.iter().any(|selection| matches!(&selection.node, Selection::Field(field) if field.node.name.node == "__schema")));
|
||||
inner.enabled = !is_schema;
|
||||
log::info!(target: "async-graphql", "[Query] query: \"{}\", variables: {}", inner.query, inner.variables);
|
||||
if !is_schema {
|
||||
log::info!(
|
||||
target: "async-graphql",
|
||||
"[Execute] {}", ctx.stringify_execute_doc(&document, variables)
|
||||
);
|
||||
}
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
&self,
|
||||
ctx: &ExtensionContext<'_>,
|
||||
info: ResolveInfo<'_>,
|
||||
next: NextResolve<'_>,
|
||||
) -> ServerResult<Option<Value>> {
|
||||
let enabled = self.inner.lock().await.enabled;
|
||||
if enabled {
|
||||
let path = info.path_node.to_string();
|
||||
log::trace!(target: "async-graphql", "[ResolveStart] path: \"{}\"", path);
|
||||
let res = next.run(ctx, info).await;
|
||||
if let Err(err) = &res {
|
||||
let inner = self.inner.lock().await;
|
||||
log::error!(
|
||||
target: "async-graphql",
|
||||
"{}",
|
||||
DisplayError { query:&inner.query,variables:&inner.variables, e: &err }
|
||||
);
|
||||
async fn execute(&self, ctx: &ExtensionContext<'_>, next: NextExecute<'_>) -> Response {
|
||||
let resp = next.run(ctx).await;
|
||||
if resp.is_err() {
|
||||
for err in &resp.errors {
|
||||
if !err.path.is_empty() {
|
||||
let mut path = String::new();
|
||||
for (idx, s) in err.path.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
path.push('.');
|
||||
}
|
||||
match s {
|
||||
PathSegment::Index(idx) => {
|
||||
let _ = write!(&mut path, "{}", idx);
|
||||
}
|
||||
PathSegment::Field(name) => {
|
||||
let _ = write!(&mut path, "{}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::error!(
|
||||
target: "async-graphql",
|
||||
"[Error] path={} message={}", path, err.message,
|
||||
);
|
||||
} else {
|
||||
log::error!(
|
||||
target: "async-graphql",
|
||||
"[Error] message={}", err.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
log::trace!(target: "async-graphql", "[ResolveEnd] path: \"{}\"", path);
|
||||
res
|
||||
} else {
|
||||
next.run(ctx, info).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayError<'a> {
|
||||
query: &'a str,
|
||||
variables: &'a Variables,
|
||||
e: &'a ServerError,
|
||||
}
|
||||
impl<'a> Display for DisplayError<'a> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "[Error] ")?;
|
||||
|
||||
if !self.e.path.is_empty() {
|
||||
write!(f, "path: ")?;
|
||||
for (i, segment) in self.e.path.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, ".")?;
|
||||
}
|
||||
|
||||
match segment {
|
||||
PathSegment::Field(field) => write!(f, "{}", field),
|
||||
PathSegment::Index(i) => write!(f, "{}", i),
|
||||
}?;
|
||||
}
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
if !self.e.locations.is_empty() {
|
||||
write!(f, "pos: [")?;
|
||||
for (i, location) in self.e.locations.iter().enumerate() {
|
||||
if i != 0 {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
write!(f, "{}:{}", location.line, location.column)?;
|
||||
}
|
||||
write!(f, "], ")?;
|
||||
}
|
||||
write!(f, r#"query: "{}", "#, self.query)?;
|
||||
write!(f, "variables: {}", self.variables)?;
|
||||
write!(f, "{}", self.e.message)
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ use crate::{
|
|||
/// Context for extension
|
||||
pub struct ExtensionContext<'a> {
|
||||
#[doc(hidden)]
|
||||
pub schema_data: &'a Data,
|
||||
pub schema_env: &'a SchemaEnv,
|
||||
|
||||
#[doc(hidden)]
|
||||
pub session_data: &'a Data,
|
||||
|
@ -47,6 +47,16 @@ pub struct ExtensionContext<'a> {
|
|||
}
|
||||
|
||||
impl<'a> ExtensionContext<'a> {
|
||||
/// Convert the specified [ExecutableDocument] into a query string.
|
||||
///
|
||||
/// Usually used for log extension, it can hide secret arguments.
|
||||
pub fn stringify_execute_doc(&self, doc: &ExecutableDocument, variables: &Variables) -> String {
|
||||
self.schema_env
|
||||
.registry
|
||||
.stringify_exec_doc(variables, doc)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Gets the global data defined in the `Context` or `Schema`.
|
||||
///
|
||||
/// If both `Schema` and `Query` have the same data type, the data in the `Query` is obtained.
|
||||
|
@ -78,7 +88,7 @@ impl<'a> ExtensionContext<'a> {
|
|||
self.query_data
|
||||
.and_then(|query_data| query_data.get(&TypeId::of::<D>()))
|
||||
.or_else(|| self.session_data.get(&TypeId::of::<D>()))
|
||||
.or_else(|| self.schema_data.get(&TypeId::of::<D>()))
|
||||
.or_else(|| self.schema_env.data.get(&TypeId::of::<D>()))
|
||||
.and_then(|d| d.downcast_ref::<D>())
|
||||
}
|
||||
}
|
||||
|
@ -393,7 +403,7 @@ impl Extensions {
|
|||
#[inline]
|
||||
fn create_context(&self) -> ExtensionContext {
|
||||
ExtensionContext {
|
||||
schema_data: &self.schema_env.data,
|
||||
schema_env: &self.schema_env,
|
||||
session_data: &self.session_data,
|
||||
query_data: self.query_data.as_deref(),
|
||||
}
|
||||
|
|
|
@ -98,9 +98,18 @@ impl<T: Tracer + Send + Sync> Extension for OpenTelemetryExtension<T> {
|
|||
.with_kind(SpanKind::Server)
|
||||
.with_attributes(attributes)
|
||||
.start(&*self.tracer);
|
||||
next.run(ctx, query, variables)
|
||||
.with_context(OpenTelemetryContext::current_with_span(span))
|
||||
.await
|
||||
|
||||
async move {
|
||||
let res = next.run(ctx, query, variables).await;
|
||||
if let Ok(doc) = &res {
|
||||
OpenTelemetryContext::current()
|
||||
.span()
|
||||
.set_attribute(KEY_SOURCE.string(ctx.stringify_execute_doc(doc, variables)));
|
||||
}
|
||||
res
|
||||
}
|
||||
.with_context(OpenTelemetryContext::current_with_span(span))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn validation(
|
||||
|
|
|
@ -85,10 +85,19 @@ impl Extension for TracingExtension {
|
|||
target: "async_graphql::graphql",
|
||||
Level::INFO,
|
||||
"parse",
|
||||
source = query,
|
||||
variables = %serde_json::to_string(&variables).unwrap(),
|
||||
);
|
||||
next.run(ctx, query, variables).instrument(span).await
|
||||
async move {
|
||||
let res = next.run(ctx, query, variables).await;
|
||||
if let Ok(doc) = &res {
|
||||
tracinglib::Span::current().record(
|
||||
"source",
|
||||
&ctx.stringify_execute_doc(doc, variables).as_str(),
|
||||
);
|
||||
}
|
||||
res
|
||||
}
|
||||
.instrument(span)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn validation(
|
||||
|
|
|
@ -283,6 +283,7 @@ pub type FieldResult<T> = Result<T>;
|
|||
/// | complexity | Custom field complexity. | string | Y |
|
||||
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
|
||||
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
|
||||
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
|
||||
/// | key | Is entity key(for Federation) | bool | Y |
|
||||
///
|
||||
/// # Valid field return types
|
||||
|
@ -630,6 +631,7 @@ pub use async_graphql_derive::Enum;
|
|||
/// | skip | Skip this field, use `Default::default` to get a default value for this field. | bool | Y |
|
||||
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
|
||||
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
|
||||
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
@ -710,6 +712,7 @@ pub use async_graphql_derive::InputObject;
|
|||
/// | default_with | Expression to generate default value | code string | Y |
|
||||
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
|
||||
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
|
||||
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
|
||||
///
|
||||
/// # Define an interface
|
||||
///
|
||||
|
@ -908,6 +911,7 @@ pub use async_graphql_derive::Union;
|
|||
/// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y |
|
||||
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
|
||||
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
|
||||
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
|
||||
///
|
||||
/// # Field argument parameters
|
||||
///
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod cache_control;
|
||||
mod export_sdl;
|
||||
mod stringify_exec_doc;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
@ -108,6 +109,7 @@ pub struct MetaInputValue {
|
|||
pub default_value: Option<String>,
|
||||
pub validator: Option<Arc<dyn InputValueValidator>>,
|
||||
pub visible: Option<MetaVisibleFn>,
|
||||
pub is_secret: bool,
|
||||
}
|
||||
|
||||
type ComputeComplexityFn = fn(
|
||||
|
@ -552,6 +554,7 @@ impl Registry {
|
|||
default_value: None,
|
||||
validator: None,
|
||||
visible: None,
|
||||
is_secret: false,
|
||||
},
|
||||
);
|
||||
args
|
||||
|
|
|
@ -0,0 +1,315 @@
|
|||
use std::fmt::{Error, Result as FmtResult, Write};
|
||||
|
||||
use async_graphql_value::ConstValue;
|
||||
|
||||
use crate::parser::types::{
|
||||
ExecutableDocument, FragmentDefinition, OperationType, Selection, SelectionSet,
|
||||
};
|
||||
use crate::registry::{MetaInputValue, MetaType, MetaTypeName, Registry};
|
||||
use crate::Variables;
|
||||
|
||||
impl Registry {
|
||||
pub(crate) fn stringify_exec_doc(
|
||||
&self,
|
||||
variables: &Variables,
|
||||
doc: &ExecutableDocument,
|
||||
) -> Result<String, Error> {
|
||||
let mut output = String::new();
|
||||
for (name, fragment) in &doc.fragments {
|
||||
self.stringify_fragment_definition(
|
||||
&mut output,
|
||||
variables,
|
||||
&*name,
|
||||
self.types
|
||||
.get(fragment.node.type_condition.node.on.node.as_str()),
|
||||
&fragment.node,
|
||||
)?;
|
||||
}
|
||||
for (name, operation_definition) in doc.operations.iter() {
|
||||
write!(&mut output, "{} ", operation_definition.node.ty)?;
|
||||
if let Some(name) = name {
|
||||
write!(&mut output, "{}", name)?;
|
||||
|
||||
if !operation_definition.node.variable_definitions.is_empty() {
|
||||
output.push('(');
|
||||
for (idx, variable_definition) in operation_definition
|
||||
.node
|
||||
.variable_definitions
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
if idx > 0 {
|
||||
output.push_str(", ");
|
||||
}
|
||||
write!(
|
||||
output,
|
||||
"{}: {}",
|
||||
variable_definition.node.name.node,
|
||||
variable_definition.node.var_type.node
|
||||
);
|
||||
if let Some(default_value) = &variable_definition.node.default_value {
|
||||
write!(output, " = {}", default_value.node);
|
||||
}
|
||||
}
|
||||
output.push(')');
|
||||
}
|
||||
|
||||
output.push(' ');
|
||||
}
|
||||
let root_type = match operation_definition.node.ty {
|
||||
OperationType::Query => self.types.get(&self.query_type),
|
||||
OperationType::Mutation => self
|
||||
.mutation_type
|
||||
.as_ref()
|
||||
.and_then(|name| self.types.get(name)),
|
||||
OperationType::Subscription => self
|
||||
.subscription_type
|
||||
.as_ref()
|
||||
.and_then(|name| self.types.get(name)),
|
||||
};
|
||||
self.stringify_selection_set(
|
||||
&mut output,
|
||||
variables,
|
||||
&operation_definition.node.selection_set.node,
|
||||
root_type,
|
||||
)?;
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn stringify_fragment_definition(
|
||||
&self,
|
||||
output: &mut String,
|
||||
variables: &Variables,
|
||||
name: &str,
|
||||
parent_type: Option<&MetaType>,
|
||||
fragment_definition: &FragmentDefinition,
|
||||
) -> FmtResult {
|
||||
write!(
|
||||
output,
|
||||
"fragment {} on {}",
|
||||
name, fragment_definition.type_condition.node.on.node
|
||||
)?;
|
||||
self.stringify_selection_set(
|
||||
output,
|
||||
variables,
|
||||
&fragment_definition.selection_set.node,
|
||||
parent_type,
|
||||
)?;
|
||||
output.push_str("}\n\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stringify_input_value(
|
||||
&self,
|
||||
output: &mut String,
|
||||
meta_input_value: Option<&MetaInputValue>,
|
||||
value: &ConstValue,
|
||||
) -> FmtResult {
|
||||
if meta_input_value.map(|v| v.is_secret).unwrap_or_default() {
|
||||
output.push_str("\"<secret>\"");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match value {
|
||||
ConstValue::Object(obj) => {
|
||||
let parent_type = meta_input_value.and_then(|input_value| {
|
||||
self.types
|
||||
.get(MetaTypeName::concrete_typename(&input_value.ty))
|
||||
});
|
||||
if let Some(MetaType::InputObject { input_fields, .. }) = parent_type {
|
||||
output.push('{');
|
||||
for (idx, (key, value)) in obj.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
output.push_str(", ");
|
||||
}
|
||||
write!(output, "{}: ", key)?;
|
||||
self.stringify_input_value(output, input_fields.get(key.as_str()), value)?;
|
||||
}
|
||||
output.push('}');
|
||||
} else {
|
||||
write!(output, "{}", value)?;
|
||||
}
|
||||
}
|
||||
_ => write!(output, "{}", value)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stringify_selection_set(
|
||||
&self,
|
||||
output: &mut String,
|
||||
variables: &Variables,
|
||||
selection_set: &SelectionSet,
|
||||
parent_type: Option<&MetaType>,
|
||||
) -> FmtResult {
|
||||
output.push_str("{ ");
|
||||
for (idx, selection) in selection_set.items.iter().map(|s| &s.node).enumerate() {
|
||||
if idx > 0 {
|
||||
output.push(' ');
|
||||
}
|
||||
match selection {
|
||||
Selection::Field(field) => {
|
||||
if let Some(alias) = &field.node.alias {
|
||||
write!(output, "{}:", alias.node)?;
|
||||
}
|
||||
write!(output, "{}", field.node.name.node)?;
|
||||
if !field.node.arguments.is_empty() {
|
||||
output.push('(');
|
||||
for (idx, (name, argument)) in field.node.arguments.iter().enumerate() {
|
||||
let meta_input_value = parent_type
|
||||
.and_then(|parent_type| {
|
||||
parent_type.field_by_name(field.node.name.node.as_str())
|
||||
})
|
||||
.and_then(|field| field.args.get(name.node.as_str()));
|
||||
if idx > 0 {
|
||||
output.push_str(", ");
|
||||
}
|
||||
write!(output, "{}: ", name)?;
|
||||
let value = argument
|
||||
.node
|
||||
.clone()
|
||||
.into_const_with(|name| variables.get(&name).cloned().ok_or(()))
|
||||
.unwrap_or_default();
|
||||
self.stringify_input_value(output, meta_input_value, &value)?;
|
||||
}
|
||||
output.push(')');
|
||||
}
|
||||
if !field.node.selection_set.node.items.is_empty() {
|
||||
output.push(' ');
|
||||
let parent_type = parent_type
|
||||
.and_then(|ty| ty.field_by_name(field.node.name.node.as_str()))
|
||||
.and_then(|field| {
|
||||
self.types.get(MetaTypeName::concrete_typename(&field.ty))
|
||||
});
|
||||
self.stringify_selection_set(
|
||||
output,
|
||||
variables,
|
||||
&field.node.selection_set.node,
|
||||
parent_type,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Selection::FragmentSpread(fragment_spread) => {
|
||||
write!(output, "... {}", fragment_spread.node.fragment_name.node)?;
|
||||
}
|
||||
Selection::InlineFragment(inline_fragment) => {
|
||||
output.push_str("... ");
|
||||
let parent_type = if let Some(name) = &inline_fragment.node.type_condition {
|
||||
write!(output, "on {} ", name.node.on.node)?;
|
||||
self.types.get(name.node.on.node.as_str())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.stringify_selection_set(
|
||||
output,
|
||||
variables,
|
||||
&inline_fragment.node.selection_set.node,
|
||||
parent_type,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
output.push_str(" }");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::parser::parse_query;
|
||||
use crate::*;
|
||||
|
||||
#[test]
|
||||
fn test_stringify() {
|
||||
let registry = Registry::default();
|
||||
let doc = parse_query(
|
||||
r#"
|
||||
query Abc {
|
||||
a b c(a:1,b:2) {
|
||||
d e f
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
registry
|
||||
.stringify_exec_doc(&Default::default(), &doc)
|
||||
.unwrap(),
|
||||
r#"query Abc { a b c(a: 1, b: 2) { d e f } }"#
|
||||
);
|
||||
|
||||
let doc = parse_query(
|
||||
r#"
|
||||
query Abc($a:Int) {
|
||||
value(input:$a)
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
registry
|
||||
.stringify_exec_doc(
|
||||
&Variables::from_value(value! ({
|
||||
"a": 10,
|
||||
})),
|
||||
&doc
|
||||
)
|
||||
.unwrap(),
|
||||
r#"query Abc { value(input: 10) }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stringify_secret() {
|
||||
#[derive(InputObject)]
|
||||
#[graphql(internal)]
|
||||
struct MyInput {
|
||||
v1: i32,
|
||||
#[graphql(secret)]
|
||||
v2: i32,
|
||||
v3: MyInput2,
|
||||
}
|
||||
|
||||
#[derive(InputObject)]
|
||||
#[graphql(internal)]
|
||||
struct MyInput2 {
|
||||
v4: i32,
|
||||
#[graphql(secret)]
|
||||
v5: i32,
|
||||
}
|
||||
|
||||
struct Query;
|
||||
|
||||
#[Object(internal)]
|
||||
#[allow(unreachable_code, unused_variables)]
|
||||
impl Query {
|
||||
async fn value(&self, a: i32, #[graphql(secret)] b: i32, c: MyInput) -> i32 {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
let registry = schema.registry();
|
||||
let s = registry
|
||||
.stringify_exec_doc(
|
||||
&Default::default(),
|
||||
&parse_query(
|
||||
r#"
|
||||
{
|
||||
value(a: 10, b: 20, c: { v1: 1, v2: 2, v3: { v4: 4, v5: 5}})
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s,
|
||||
r#"query { value(a: 10, b: "<secret>", c: {v1: 1, v2: "<secret>", v3: {v4: 4, v5: "<secret>"}}) }"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -271,6 +271,7 @@ where
|
|||
default_value: None,
|
||||
validator: None,
|
||||
visible: None,
|
||||
is_secret: false,
|
||||
});
|
||||
args
|
||||
}
|
||||
|
@ -293,6 +294,7 @@ where
|
|||
default_value: None,
|
||||
validator: None,
|
||||
visible: None,
|
||||
is_secret: false,
|
||||
});
|
||||
args
|
||||
}
|
||||
|
@ -332,6 +334,12 @@ where
|
|||
Self::build(query, mutation, subscription).finish()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(unused)]
|
||||
pub(crate) fn registry(&self) -> &Registry {
|
||||
&self.env.registry
|
||||
}
|
||||
|
||||
/// Returns SDL(Schema Definition Language) of this schema.
|
||||
pub fn sdl(&self) -> String {
|
||||
self.0.env.registry.export_sdl(false)
|
||||
|
|
|
@ -67,6 +67,7 @@ impl<T: Type> Type for QueryRoot<T> {
|
|||
default_value: None,
|
||||
validator: None,
|
||||
visible: None,
|
||||
is_secret: false,
|
||||
},
|
||||
);
|
||||
args
|
||||
|
|
Loading…
Reference in New Issue