Custom Resolvers in RESTHeart GraphQL
Custom resolvers provide advanced flexibility in how GraphQL fields are resolved in RESTHeart. This guide covers resolver types, implementation patterns, and best practices.
Understanding Resolvers
In RESTHeart GraphQL, resolvers are special functions that determine how to fetch or compute data for a specific field. While basic field mappings handle most cases, custom resolvers enable more complex scenarios.
Resolver Types
-
Type Resolvers
-
Determine concrete types for interfaces and unions
-
Handle polymorphic data structures
-
Map document fields to GraphQL types
-
-
Field Resolvers
-
Compute field values dynamically
-
Transform data during resolution
-
Handle complex data relationships
-
Type Resolvers
Interface Type Resolution
Type resolvers for interfaces determine which concrete type a document represents:
{
"mappings": {
"Content": {
"$typeResolver": {
"Article": "field-exists(wordCount)",
"Video": "field-exists(duration)",
"Podcast": "field-exists(audioUrl)"
}
}
}
}
Union Type Resolution
Similar to interfaces, but for union types:
{
"mappings": {
"SearchResult": {
"$typeResolver": {
"User": "field-exists(email)",
"Product": "field-exists(price)",
"Article": "field-exists(content)"
}
}
}
}
Type Resolution Rules
-
Evaluation Order
-
Rules are evaluated in order
-
First matching rule determines the type
-
Include a default case when possible
-
-
Available Functions
-
field-exists(fieldName)
: Checks if a field exists -
field-equals(fieldName, value)
: Compares field value -
field-type(fieldName, type)
: Checks field data type
-
{
"mappings": {
"Node": {
"$typeResolver": {
"Document": "field-type(content, string)",
"Image": "field-equals(type, 'image')",
"Default": "true"
}
}
}
}
Field Resolvers
Basic Field Resolution
Transform field values during resolution:
{
"mappings": {
"User": {
"fullName": {
"$resolver": "concat",
"fields": ["firstName", "lastName"],
"separator": " "
}
}
}
}
Computed Fields
Calculate values based on other fields:
{
"mappings": {
"Product": {
"discountedPrice": {
"$resolver": "multiply",
"field": "price",
"factor": 0.9
},
"profit": {
"$resolver": "subtract",
"field1": "price",
"field2": "cost"
}
}
}
}
Conditional Resolution
Resolve fields based on conditions:
{
"mappings": {
"Order": {
"status": {
"$resolver": "conditional",
"conditions": [
{
"if": "field-equals(paid, true)",
"then": "PAID"
},
{
"if": "field-exists(cancelledAt)",
"then": "CANCELLED"
},
{
"else": "PENDING"
}
]
}
}
}
}
Built-in Resolvers
RESTHeart provides several built-in resolvers:
Resolver | Description | Example Use Case |
---|---|---|
|
Concatenates multiple fields |
Combining name parts |
|
Multiplies a field by a factor |
Calculating discounts |
|
Divides a field by a divisor |
Computing rates |
|
Adds multiple fields |
Summing values |
|
Subtracts fields |
Computing differences |
|
Conditional field resolution |
Status determination |
|
Formats field values |
Date/number formatting |
Custom JavaScript Resolvers
For more complex resolution logic, you can implement custom JavaScript resolvers:
{
"mappings": {
"Order": {
"total": {
"$resolver": "javascript",
"code": "
function resolve(doc) {
return doc.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0);
}
"
}
}
}
}
JavaScript Resolver Context
JavaScript resolvers have access to:
-
Document Context
-
Current document as first argument
-
Parent document fields
-
Query arguments
-
-
Helper Functions
-
MongoDB ObjectId creation
-
Date manipulation
-
String formatting
-
Security Considerations
When using JavaScript resolvers:
-
Input Validation
-
Validate all inputs
-
Sanitize user data
-
Handle edge cases
-
-
Resource Usage
-
Keep functions simple
-
Avoid infinite loops
-
Limit recursion depth
-
-
Error Handling
-
Catch exceptions
-
Provide fallback values
-
Log errors appropriately
-
Performance Optimization
1. Caching
Cache computed values when possible:
{
"mappings": {
"Product": {
"stats": {
"$resolver": "javascript",
"code": "...",
"cache": {
"enabled": true,
"ttl": 300
}
}
}
}
}
2. Batching
Group related resolutions:
{
"mappings": {
"Order": {
"items": {
"$resolver": "batchLoad",
"maxBatchSize": 100,
"batchBy": "orderId"
}
}
}
}
Best Practices
-
Keep Resolvers Simple
-
One responsibility per resolver
-
Clear, maintainable logic
-
Document complex resolvers
-
-
Error Handling
-
Provide fallback values
-
Validate inputs
-
Log errors appropriately
-
-
Performance
-
Use caching when possible
-
Batch related operations
-
Monitor resolver performance
-
-
Security
-
Validate all inputs
-
Sanitize user data
-
Follow least privilege principle
-
Next Steps
-
Explore Performance Optimization
-
Learn about Best Practices
-
Check out Complex App Example