Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changesets/fix_evan_minify_include_builtin_directives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Minify: Add support for deprecated directive - @esilverm PR #367

Includes any existing `@deprecated` directives in the schema in the minified output of builtin tools. Now operations generated via these tools should take into account deprecated fields when being generated.
74 changes: 70 additions & 4 deletions crates/apollo-mcp-server/src/introspection/minify.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use apollo_compiler::schema::{ExtendedType, Type};
use regex::Regex;
use std::sync::OnceLock;
use std::{collections::HashMap, sync::OnceLock};

pub trait MinifyExt {
/// Serialize in minified form
Expand Down Expand Up @@ -72,6 +72,39 @@ fn minify_input_object(input_object_type: &apollo_compiler::schema::InputObjectT
format!("I:{type_name}:{fields}")
}

// We should only minify directives that assist the LLM in understanding the schema. This included @deprecated
fn minify_directives(directives: &apollo_compiler::ast::DirectiveList) -> String {
let mut result = String::new();

static DIRECTIVES_TO_MINIFY: OnceLock<HashMap<&str, &str>> = OnceLock::new();
let directives_to_minify =
DIRECTIVES_TO_MINIFY.get_or_init(|| HashMap::from([("deprecated", "D")]));

for directive in directives.iter() {
if let Some(minified_name) = directives_to_minify.get(directive.name.as_str()) {
if !directive.arguments.is_empty() {
// Since we're only handling @deprecated right now we can just add the reason and minify it.
// We should handle this more generically in the future.
let reason = directive
.arguments
.iter()
.find(|a| a.name == "reason")
.and_then(|a| a.value.as_str())
.unwrap_or("No longer supported")
.to_string();
result.push_str(&format!(
"@{}(\"{}\")",
minified_name,
normalize_description(&reason)
));
} else {
result.push_str(&format!("@{}", minified_name));
}
}
}
result
}

fn minify_fields(
fields: &apollo_compiler::collections::IndexMap<
apollo_compiler::Name,
Expand Down Expand Up @@ -99,6 +132,8 @@ fn minify_fields(
// Add field type
result.push(':');
result.push_str(&type_name(&field.ty));
result.push_str(&minify_directives(&field.directives));

result.push(',');
}

Expand Down Expand Up @@ -128,6 +163,7 @@ fn minify_input_fields(
result.push_str(field_name.as_str());
result.push(':');
result.push_str(&type_name(&field.ty));
result.push_str(&minify_directives(&field.directives));
result.push(',');
}

Expand All @@ -147,13 +183,19 @@ fn minify_arguments(
.map(|arg| {
if let Some(desc) = arg.description.as_ref() {
format!(
"\"{}\"{}:{}",
"\"{}\"{}:{}{}",
normalize_description(desc),
arg.name.as_str(),
type_name(&arg.ty)
type_name(&arg.ty),
minify_directives(&arg.directives)
)
} else {
format!("{}:{}", arg.name.as_str(), type_name(&arg.ty))
format!(
"{}:{}{}",
arg.name.as_str(),
type_name(&arg.ty),
minify_directives(&arg.directives)
)
}
})
.collect::<Vec<String>>()
Expand Down Expand Up @@ -211,3 +253,27 @@ fn normalize_description(desc: &str) -> String {
let re = WHITESPACE_PATTERN.get_or_init(|| Regex::new(r"\s+").expect("regex pattern compiles"));
re.replace_all(desc, "").to_string()
}

#[cfg(test)]
mod tests {
use super::*;

const TEST_SCHEMA: &str = include_str!("tools/testdata/schema.graphql");

#[test]
fn test_minify_schema() {
let schema = apollo_compiler::schema::Schema::parse(TEST_SCHEMA, "schema.graphql")
.expect("Failed to parse schema")
.validate()
.expect("Failed to validate schema");

let minified = schema
.types
.iter()
.map(|(_, type_)| format!("{}: {}", type_.name().as_str(), type_.minify()))
.collect::<Vec<String>>()
.join("\n");

insta::assert_snapshot!(minified);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
source: crates/apollo-mcp-server/src/introspection/minify.rs
expression: minified
---
__Schema: T:"AGraphQLSchemadefinesthecapabilitiesofaGraphQLserver.Itexposesallavailabletypesanddirectivesontheserver,aswellastheentrypointsforquery,mutation,andsubscriptionoperations."__Schema:description:s,"Alistofalltypessupportedbythisserver."types:[__Type],"Thetypethatqueryoperationswillberootedat."queryType:__Type!,"Ifthisserversupportsmutation,thetypethatmutationoperationswillberootedat."mutationType:__Type,"Ifthisserversupportsubscription,thetypethatsubscriptionoperationswillberootedat."subscriptionType:__Type,"Alistofalldirectivessupportedbythisserver."directives:[__Directive]
__Type: T:"ThefundamentalunitofanyGraphQLSchemaisthetype.TherearemanykindsoftypesinGraphQLasrepresentedbythe`__TypeKind`enum.Dependingonthekindofatype,certainfieldsdescribeinformationaboutthattype.Scalartypesprovidenoinformationbeyondaname,descriptionandoptional`specifiedByURL`,whileEnumtypesprovidetheirvalues.ObjectandInterfacetypesprovidethefieldstheydescribe.Abstracttypes,UnionandInterface,providetheObjecttypespossibleatruntime.ListandNonNulltypescomposeothertypes."__Type:kind:__TypeKind!,name:s,description:s,fields(includeDeprecated:b):[__Field],interfaces:[__Type],possibleTypes:[__Type],enumValues(includeDeprecated:b):[__EnumValue],inputFields(includeDeprecated:b):[__InputValue],ofType:__Type,specifiedByURL:s
__TypeKind: E:"Anenumdescribingwhatkindoftypeagiven`__Type`is."__TypeKind:SCALAR,OBJECT,INTERFACE,UNION,ENUM,INPUT_OBJECT,LIST,NON_NULL
__Field: T:"ObjectandInterfacetypesaredescribedbyalistofFields,eachofwhichhasaname,potentiallyalistofarguments,andareturntype."__Field:name:s!,description:s,args(includeDeprecated:b):[__InputValue],type:__Type!,isDeprecated:b!,deprecationReason:s
__InputValue: T:"ArgumentsprovidedtoFieldsorDirectivesandtheinputfieldsofanInputObjectarerepresentedasInputValueswhichdescribetheirtypeandoptionallyadefaultvalue."__InputValue:name:s!,description:s,type:__Type!,"AGraphQL-formattedstringrepresentingthedefaultvalueforthisinputvalue."defaultValue:s,isDeprecated:b!,deprecationReason:s
__EnumValue: T:"OnepossiblevalueforagivenEnum.Enumvaluesareuniquevalues,notaplaceholderforastringornumericvalue.HoweveranEnumvalueisreturnedinaJSONresponseasastring."__EnumValue:name:s!,description:s,isDeprecated:b!,deprecationReason:s
__Directive: T:"ADirectiveprovidesawaytodescribealternateruntimeexecutionandtypevalidationbehaviorinaGraphQLdocument.Insomecases,youneedtoprovideoptionstoalterGraphQL'sexecutionbehaviorinwaysfieldargumentswillnotsuffice,suchasconditionallyincludingorskippingafield.Directivesprovidethisbydescribingadditionalinformationtotheexecutor."__Directive:name:s!,description:s,locations:[__DirectiveLocation],args(includeDeprecated:b):[__InputValue],isRepeatable:b!
__DirectiveLocation: E:"ADirectivecanbeadjacenttomanypartsoftheGraphQLlanguage,a__DirectiveLocationdescribesonesuchpossibleadjacencies."__DirectiveLocation:QUERY,MUTATION,SUBSCRIPTION,FIELD,FRAGMENT_DEFINITION,FRAGMENT_SPREAD,INLINE_FRAGMENT,VARIABLE_DEFINITION,SCHEMA,SCALAR,OBJECT,FIELD_DEFINITION,ARGUMENT_DEFINITION,INTERFACE,UNION,ENUM,ENUM_VALUE,INPUT_OBJECT,INPUT_FIELD_DEFINITION
Int: i
Float: f
String: s
Boolean: b
ID: d
DateTime: DateTime
JSON: JSON
Upload: Upload
UserRole: E:UserRole:ADMIN,MODERATOR,USER,GUEST
ContentStatus: E:ContentStatus:DRAFT,PUBLISHED,ARCHIVED,DELETED
NotificationPriority: E:NotificationPriority:LOW,MEDIUM,HIGH,URGENT
MediaType: E:MediaType:IMAGE,VIDEO,AUDIO,DOCUMENT
Node: F:Node:id:d!,createdAt:DateTime!,updatedAt:DateTime!
Content: F:Content:id:d!,title:s!,status:ContentStatus!,author:User!,metadata:JSON
User: T:User<Node>:id:d!,createdAt:DateTime!,updatedAt:DateTime!,username:s!,email:s!,role:UserRole!,profile:UserProfile,posts:[Post],comments:[Comment],notifications:[Notification],preferences:UserPreferences!
UserProfile: T:UserProfile:firstName:s,lastName:s,bio:s,avatar:Media,socialLinks:[SocialLink],location:Location
Location: T:Location:country:s!,city:s,coordinates:Coordinates
Coordinates: T:Coordinates:latitude:f!,longitude:f!
SocialLink: T:SocialLink:platform:s!,url:s!,verified:b!
Post: T:Post<Node,Content>:id:d!,createdAt:DateTime!,updatedAt:DateTime!,title:s!,content:s!,status:ContentStatus!,author:User!,metadata:JSON,comments:[Comment],media:[Media],tags:[Tag],analytics:PostAnalytics!
Comment: T:Comment<Node>:id:d!,createdAt:DateTime!,updatedAt:DateTime!,content:s!,author:User!,post:Post!,parentComment:Comment,replies:[Comment],reactions:[Reaction]
Media: T:Media:id:d!,type:MediaType!,url:s!,thumbnail:s,metadata:MediaMetadata!,uploader:User!
MediaMetadata: T:MediaMetadata:size:i!,format:s!,dimensions:Dimensions,duration:i
Dimensions: T:Dimensions:width:i!,height:i!
Tag: T:Tag:id:d!,name:s!,slug:s!,description:s,posts:[Post]
Reaction: T:Reaction:id:d!,type:s!,user:User!,comment:Comment!,createdAt:DateTime!
Notification: T:Notification:id:d!,type:s!,priority:NotificationPriority!,message:s!,recipient:User!,read:b!,createdAt:DateTime!,metadata:JSON
PostAnalytics: T:PostAnalytics:views:i!,likes:i!,shares:i!,comments:i!,engagement:f!,demographics:Demographics!
Demographics: T:Demographics:ageGroups:[AgeGroup],locations:[LocationStats],devices:[DeviceStats]
AgeGroup: T:AgeGroup:range:s!,percentage:f!
LocationStats: T:LocationStats:country:s!,count:i!
DeviceStats: T:DeviceStats:type:s!,count:i!
UserPreferences: T:UserPreferences:theme:s!,language:s!,notifications:NotificationPreferences!,privacy:PrivacySettings!
NotificationPreferences: T:NotificationPreferences:email:b!,push:b!,sms:b!,frequency:s!
PrivacySettings: T:PrivacySettings:profileVisibility:s!,showEmail:b!,showLocation:b!
CreateUserInput: I:CreateUserInput:username:s!,email:s!,password:s!,role:UserRole,profile:CreateUserProfileInput
CreateUserProfileInput: I:CreateUserProfileInput:firstName:s,lastName:s,bio:s,location:CreateLocationInput
CreateLocationInput: I:CreateLocationInput:country:s!,city:s,coordinates:CreateCoordinatesInput
CreateCoordinatesInput: I:CreateCoordinatesInput:latitude:f!,longitude:f!
CreatePostInput: I:CreatePostInput:title:s!,content:s!,status:ContentStatus,tags:[s],media:[Upload]
UpdatePostInput: I:UpdatePostInput:title:s,content:s,status:ContentStatus,tags:[s]
CreateCommentInput: I:CreateCommentInput:content:s!,postId:d!,parentCommentId:d
NotificationFilter: I:NotificationFilter:priority:NotificationPriority,read:b,type:s,startDate:DateTime,endDate:DateTime
Query: T:Query:node(id:d!):Node,user(id:d!):User,post(id:d!):Post,postsOld(filter:[d]):[Post]@D("Usepostsinstead"),posts(filter:PostFilter):[Post],comments(postId:d!):[Comment],notifications(filter:NotificationFilter):[Notification],search(query:s!):SearchResult!
Mutation: T:Mutation:createUser(input:CreateUserInput!):User!,createPost(input:CreatePostInput!):Post!,updatePost(id:d!,input:UpdatePostInput!):Post!,createComment(input:CreateCommentInput!):Comment!,deletePost(id:d!):b!,uploadMedia(file:Upload!):Media!,updateUserPreferences(id:d!,preferences:UserPreferencesInput!):UserPreferences!
Subscription: T:Subscription:postUpdated(id:d!):Post!,newComment(postId:d!):Comment!,notificationReceived(userId:d!):Notification!
SearchResult: U:SearchResult:User,Post,Comment,Tag
PostFilter: I:PostFilter:status:ContentStatus,authorId:d,tags:[s],dateRange:DateRangeInput
DateRangeInput: I:DateRangeInput:start:DateTime!,end:DateTime!
UserPreferencesInput: I:UserPreferencesInput:theme:s,language:s,notifications:NotificationPreferencesInput,privacy:PrivacySettingsInput
NotificationPreferencesInput: I:NotificationPreferencesInput:email:b,push:b,sms:b,frequency:s
PrivacySettingsInput: I:PrivacySettingsInput:profileVisibility:s,showEmail:b,showLocation:b
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ fn tool_description(
minify: bool,
) -> String {
if minify {
"Get GraphQL type information - T=type,I=input,E=enum,U=union,F=interface;s=String,i=Int,f=Float,b=Boolean,d=ID;!=required,[]=list,<>=implements;".to_string()
"Get GraphQL type information - T=type,I=input,E=enum,U=union,F=interface;s=String,i=Int,f=Float,b=Boolean,d=ID;@D=deprecated;!=required,[]=list,<>=implements;".to_string()
} else {
format!(
"Get detailed information about types from the GraphQL schema.{}{}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl Search {
format!(
"Search a GraphQL schema{}",
if minify {
" - T=type,I=input,E=enum,U=union,F=interface;s=String,i=Int,f=Float,b=Boolean,d=ID;!=required,[]=list,<>=implements"
" - T=type,I=input,E=enum,U=union,F=interface;s=String,i=Int,f=Float,b=Boolean,d=ID;@D=deprecated;!=required,[]=list,<>=implements"
} else {
""
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type Post implements Node & Content {

type Query {
user(id: ID!): User
post(id: ID!): Post
postsOld(filter: [ID!]): [Post!]! @deprecated(reason: "Use posts instead")
posts(filter: PostFilter): [Post!]!
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ type Query {
node(id: ID!): Node
user(id: ID!): User
post(id: ID!): Post
postsOld(filter: [ID!]) : [Post!]! @deprecated(reason: "Use posts instead")
posts(filter: PostFilter): [Post!]!
comments(postId: ID!): [Comment!]!
notifications(filter: NotificationFilter): [Notification!]!
Expand Down