Schema Filtering#
graphql-api provides powerful schema filtering capabilities that automatically control field access and maintain proper GraphQL schema structure. This includes separating query and mutation fields, validating field combinations, and ensuring interface compliance.
Understanding Schema Filtering#
Schema filtering works automatically to:
- Separate query fields from mutation fields
- Prevent invalid field combinations
- Maintain interface contracts
- Ensure proper GraphQL schema structure
- Filter out inappropriate fields based on context
This happens transparently during schema generation and query execution, ensuring your API follows GraphQL best practices.
Field-Level Access Control#
Query vs Mutation Field Separation#
You can control which fields are available in queries vs mutations using the mutable parameter:
@api.type(is_root_type=True)
class Root:
@api.field
def public_data(self) -> str:
"""Available in queries only."""
return "Available in queries"
@api.field(mutable=True)
def update_data(self, value: str) -> str:
"""Available in mutations only."""
return f"Updated: {value}"
@api.field
def read_user(self, id: str) -> str:
"""Query operation."""
return f"User: {id}"
@api.field(mutable=True)
def create_user(self, name: str) -> str:
"""Mutation operation."""
return f"Created user: {name}"Schema separation:
- In queries: only non-mutable fields (
public_data,read_user) are available - In mutations: both regular and mutable fields may be available depending on context
GraphQL schema generated:
type Query {
publicData: String!
readUser(id: String!): String!
}
type Mutation {
updateData(value: String!): String!
createUser(name: String!): String!
}Automatic Schema Validation#
The library automatically filters invalid field combinations to maintain GraphQL correctness:
class Person:
@api.field
def name(self) -> str:
return "Alice"
@api.field(mutable=True)
def update_name(self, name: str) -> str:
self.name = name
return name
@api.type(is_root_type=True)
class Root:
@api.field
def person(self) -> Person:
return Person()Automatic filtering behavior:
# ❌ This query will fail - can't use mutation fields in queries
query {
person {
updateName(name: "Bob") # ERROR: mutation field in query
}
}
# ✅ This query will succeed - only query fields
query {
person {
name # OK: query field in query
}
}
# ✅ This mutation will succeed - mutation fields allowed
mutation {
person {
updateName(name: "Bob") # OK: mutation field in mutation
}
}Interface and Implementation Filtering#
The library maintains proper GraphQL schema structure even when implementations are filtered:
@api.type(interface=True)
class Animal:
@api.field
def name(self) -> str:
return "Generic Animal"
@api.field
def species(self) -> str:
return "Unknown"
class Dog(Animal):
@api.field
def name(self) -> str:
return "Dog"
@api.field
def breed(self) -> str:
return "Golden Retriever"
@api.field(mutable=True)
def update_name(self, name: str) -> str:
return f"Updated dog name to {name}"
class Cat(Animal):
@api.field
def name(self) -> str:
return "Cat"
@api.field
def indoor(self) -> bool:
return True
@api.type(is_root_type=True)
class Root:
@api.field
def animals(self) -> List[Animal]:
return [Dog(), Cat()]Schema filtering ensures:
- Interface contracts are maintained
- Only appropriate fields are available in each operation type
- Implementation-specific fields are properly filtered
Context-Based Filtering#
Fields can be filtered based on request context and user permissions:
from graphql_api.context import GraphQLContext
@api.type(is_root_type=True)
class Root:
@api.field
def public_info(self) -> str:
"""Always available."""
return "Public information"
@api.field
def user_info(self, context: GraphQLContext) -> Optional[str]:
"""Only available to authenticated users."""
user = getattr(context, 'current_user', None)
if not user:
return None
return f"Info for {user.name}"
@api.field
def admin_info(self, context: GraphQLContext) -> Optional[str]:
"""Only available to admin users."""
user = getattr(context, 'current_user', None)
if not user or not user.is_admin:
return None
return "Admin-only information"
@api.field(mutable=True)
def delete_data(self, context: GraphQLContext, id: str) -> bool:
"""Dangerous operation with strict access control."""
user = getattr(context, 'current_user', None)
if not user or not user.has_permission('delete'):
raise PermissionError("Delete permission required")
return TrueAdvanced Filtering Patterns#
Role-Based Field Access#
Implement role-based access control that filters fields based on user roles:
def role_based_filter_middleware(next_, root, info, **args):
"""Filter fields based on user roles."""
user = getattr(info.context, 'current_user', None)
field_name = info.field_name
# Define role requirements for fields
role_requirements = {
'admin_data': ['admin'],
'moderator_tools': ['admin', 'moderator'],
'user_secrets': ['user', 'admin', 'moderator'],
'system_info': ['admin', 'system']
}
required_roles = role_requirements.get(field_name, [])
if required_roles:
if not user:
raise GraphQLError("Authentication required")
user_roles = getattr(user, 'roles', [])
if not any(role in user_roles for role in required_roles):
raise GraphQLError(f"Insufficient permissions for {field_name}")
return next_(root, info, **args)
@api.type(is_root_type=True)
class Root:
@api.field
def public_data(self) -> str:
return "Available to everyone"
@api.field
def admin_data(self) -> str:
return "Admin only data"
@api.field
def moderator_tools(self) -> str:
return "Moderator tools"
@api.field
def user_secrets(self) -> str:
return "User-specific secrets"
api = GraphQLAPI(middleware=[role_based_filter_middleware])Dynamic Field Filtering#
Filter fields dynamically based on request context:
def dynamic_field_filter_middleware(next_, root, info, **args):
"""Dynamically filter fields based on various factors."""
field_name = info.field_name
context = info.context
# Filter expensive operations during peak hours
if field_name in ['expensive_calculation', 'heavy_query']:
current_hour = datetime.now().hour
if 9 <= current_hour <= 17: # Business hours
peak_hours = getattr(context, 'allow_peak_operations', False)
if not peak_hours:
raise GraphQLError("Operation not available during peak hours")
# Filter based on API key tier
api_key_tier = getattr(context, 'api_key_tier', 'free')
premium_fields = ['advanced_analytics', 'bulk_operations', 'priority_support']
if field_name in premium_fields and api_key_tier != 'premium':
raise GraphQLError(f"Premium subscription required for {field_name}")
# Filter based on feature flags
feature_flags = getattr(context, 'feature_flags', {})
beta_fields = ['beta_feature', 'experimental_api']
if field_name in beta_fields:
if not feature_flags.get(f'{field_name}_enabled', False):
raise GraphQLError(f"Feature {field_name} is not enabled")
return next_(root, info, **args)Schema-Level Filtering#
Filter entire types or sets of fields based on configuration:
class FilteredSchema:
def __init__(self, enabled_features=None):
self.enabled_features = enabled_features or set()
def is_feature_enabled(self, feature: str) -> bool:
return feature in self.enabled_features
schema_config = FilteredSchema(enabled_features={
'user_management',
'reporting',
# 'admin_tools', # Disabled
# 'beta_features' # Disabled
})
def schema_filter_middleware(next_, root, info, **args):
"""Filter based on enabled schema features."""
field_name = info.field_name
# Map fields to features
feature_map = {
'create_user': 'user_management',
'delete_user': 'user_management',
'generate_report': 'reporting',
'admin_panel': 'admin_tools',
'beta_endpoint': 'beta_features'
}
required_feature = feature_map.get(field_name)
if required_feature and not schema_config.is_feature_enabled(required_feature):
raise GraphQLError(f"Feature {required_feature} is not available")
return next_(root, info, **args)
@api.type(is_root_type=True)
class Root:
@api.field(mutable=True)
def create_user(self, name: str) -> str:
return f"Created user: {name}"
@api.field
def generate_report(self) -> str:
return "Generated report"
@api.field
def admin_panel(self) -> str:
return "Admin panel access"
@api.field
def beta_endpoint(self) -> str:
return "Beta feature"Conditional Field Inclusion#
Include or exclude fields based on runtime conditions:
@api.type
class User:
@api.field
def id(self) -> str:
return self._id
@api.field
def name(self) -> str:
return self._name
@api.field
def email(self, context: GraphQLContext) -> Optional[str]:
"""Email only available to user themselves or admins."""
current_user = getattr(context, 'current_user', None)
if not current_user:
return None
# Users can see their own email, admins can see any email
if current_user.id == self._id or current_user.is_admin:
return self._email
return None
@api.field
def phone(self, context: GraphQLContext) -> Optional[str]:
"""Phone number with privacy controls."""
current_user = getattr(context, 'current_user', None)
if not current_user:
return None
# Only show to user themselves
if current_user.id == self._id:
return self._phone
return None
@api.field
def internal_notes(self, context: GraphQLContext) -> Optional[str]:
"""Internal notes only for staff."""
current_user = getattr(context, 'current_user', None)
if not current_user or not current_user.is_staff:
return None
return self._internal_notesBest Practices#
Use clear field separation:
# ✅ Good: Clear separation of concerns
@api.field
def get_user(self, id: str) -> User:
return fetch_user(id)
@api.field(mutable=True)
def update_user(self, id: str, data: UpdateUserInput) -> User:
return update_user_data(id, data)
# ❌ Avoid: Mixing query and mutation logic
@api.field
def user_operation(self, id: str, action: str, data: Optional[dict] = None):
if action == "get":
return fetch_user(id)
elif action == "update":
return update_user_data(id, data)Implement graceful degradation:
@api.field
def premium_feature(self, context: GraphQLContext) -> Optional[str]:
"""Gracefully handle unavailable features."""
user = getattr(context, 'current_user', None)
if not user:
return None # Not an error, just not available
if not user.has_premium:
return None # Graceful degradation
return "Premium content here"Use middleware for consistent filtering:
# Apply filtering logic consistently across all fields
api = GraphQLAPI(middleware=[
authentication_middleware, # Add user context
permission_middleware, # Check permissions
feature_flag_middleware, # Check feature availability
rate_limiting_middleware, # Apply rate limits
])Document filtering behavior:
@api.field
def sensitive_data(self, context: GraphQLContext) -> Optional[str]:
"""
Returns sensitive data.
Access Control:
- Requires authentication
- Only available to users with 'view_sensitive' permission
- Returns None if access is denied (no error thrown)
"""
user = getattr(context, 'current_user', None)
if not user or not user.has_permission('view_sensitive'):
return None
return "Sensitive information"Schema filtering ensures your GraphQL API maintains proper structure and security while providing flexible access control mechanisms that scale with your application’s needs.