GraphQL Resolvers
This document provides a comprehensive reference of all GraphQL resolvers in Crane Ledger's API. Resolvers are functions that fetch data for GraphQL fields, with some being simple property access and others being complex operations that call REST endpoints.
Resolver Architecture
How Resolvers Work
Crane Ledger's GraphQL resolvers call REST API endpoints internally:
GraphQL Query → Resolver Function → REST API Call → Database → JSON Response → GraphQL Field
Resolver Types
- Simple Resolvers: Direct field access from loaded data
- Complex Resolvers: Call REST endpoints for additional data
- Async Resolvers: Worker-based operations for complex computations
Cost Implications
- Simple resolvers: 0 credits (free - basic field access)
- Complex resolvers: 0-1 credits (additional API calls)
- Report resolvers: 5 credits (financial calculations)
OrganizationNode Resolvers
accounts
Get all accounts for the organization.
type OrganizationNode {
accounts: [AccountNode!]!
}
Implementation: Calls GET /organizations/{org_id}/accounts
Credits: 0 (free)
Usage:
query GetOrganizationAccounts {
organization {
id
name
accounts {
id
code
name
accountType
balance {
formatted
}
}
}
}
recentTransactions
Get recent transactions with optional limit.
type OrganizationNode {
recentTransactions(limit: Int): [TransactionNode!]!
}
Parameters:
limit: Int- Maximum transactions to return (default: 10, max: 100)
Implementation: Calls GET /organizations/{org_id}/transactions?limit={limit}&sort=-created_at
Credits: 0 (free)
Usage:
query GetRecentActivity {
organization {
recentTransactions(limit: 20) {
id
description
amount
date
status
}
}
}
contacts
Get all contacts for the organization.
type OrganizationNode {
contacts: [ContactNode!]!
}
Implementation: Calls GET /organizations/{org_id}/contacts
Credits: 0 (free)
invoices
Get all invoices for the organization.
type OrganizationNode {
invoices: [InvoiceNode!]!
}
Implementation: Calls GET /organizations/{org_id}/invoices
Credits: 0 (free)
bills
Get all bills for the organization.
type OrganizationNode {
bills: [BillNode!]!
}
Implementation: Calls GET /organizations/{org_id}/bills
Credits: 0 (free)
trialBalance
Generate trial balance report.
type OrganizationNode {
trialBalance: TrialBalanceReport!
}
Implementation: Calls GET /organizations/{org_id}/reports/trial-balance
Credits: 5
Usage:
query GetTrialBalance {
organization {
trialBalance {
generatedAt
totalDebits
totalCredits
accounts {
accountCode
accountName
debitBalance
creditBalance
netBalance
}
}
}
}
balanceSheet
Generate balance sheet report.
type OrganizationNode {
balanceSheet: BalanceSheetReport!
}
Implementation: Calls GET /organizations/{org_id}/reports/balance-sheet
Credits: 5
incomeStatement
Generate income statement report.
type OrganizationNode {
incomeStatement: IncomeStatementReport!
}
Implementation: Calls GET /organizations/{org_id}/reports/income-statement
Credits: 5
creditBalance
Get detailed credit balance information.
type OrganizationNode {
creditBalance: CreditBalanceNode! # Requires @auth guard
}
Implementation: Calls GET /organizations/{org_id}/billing
Credits: 0 (free)
Authentication: Required
creditPackages
Get available credit packages.
type OrganizationNode {
creditPackages: [CreditPackageNode!]!
}
Implementation: Returns static package data
Credits: 0 (free)
AccountNode Resolvers
balance
Get current account balance.
type AccountNode {
balance: MoneyNode!
}
Implementation: Calls GET /organizations/{org_id}/accounts/{account_id}/balance
Credits: 0 (free)
Usage:
query GetAccountBalance {
account(id: "act_123") {
id
code
name
balance {
amount
formatted
currency {
code
symbol
}
}
}
}
transactions
Get transactions for this account with optional limit.
type AccountNode {
transactions(limit: Int): [TransactionNode!]!
}
Parameters:
limit: Int- Maximum transactions to return (default: 10, max: 50)
Implementation: Calls GET /organizations/{org_id}/accounts/{account_id}/transactions?limit={limit}
Credits: 0 (free)
ContactNode Resolvers
transactions
Get transactions involving this contact.
type ContactNode {
transactions(limit: Int): [TransactionNode!]!
}
Parameters:
limit: Int- Maximum transactions to return (default: 10)
Implementation: Calls GET /organizations/{org_id}/contacts/{contact_id}/transactions?limit={limit}
Credits: 0 (free)
invoices
Get all invoices for this contact.
type ContactNode {
invoices: [InvoiceNode!]!
}
Implementation: Calls GET /organizations/{org_id}/contacts/{contact_id}/invoices
Credits: 0 (free)
bills
Get all bills for this contact.
type ContactNode {
bills: [BillNode!]!
}
Implementation: Calls GET /organizations/{org_id}/contacts/{contact_id}/bills
Credits: 0 (free)
InvoiceNode Resolvers
payments
Get all payments for this invoice.
type InvoiceNode {
payments: [PaymentNode!]!
}
Implementation: Calls GET /organizations/{org_id}/invoices/{invoice_id}/payments
Credits: 0 (free)
pdfUrl
Get PDF download URL for this invoice.
type InvoiceNode {
pdfUrl: String!
}
Implementation: Calls GET /organizations/{org_id}/invoices/{invoice_id}/pdf
Credits: 0 (free)
BillNode Resolvers
payments
Get all payments for this bill.
type BillNode {
payments: [PaymentNode!]!
}
Implementation: Calls GET /organizations/{org_id}/bills/{bill_id}/payments
Credits: 0 (free)
pdfUrl
Get PDF download URL for this bill.
type BillNode {
pdfUrl: String!
}
Implementation: Calls GET /organizations/{org_id}/bills/{bill_id}/pdf
Credits: 0 (free)
Query Resolvers
organization
Get current organization from API key context.
type Query {
organization: OrganizationNode # Requires @auth guard
}
Implementation: Uses API key context to determine organization, then calls GET /organizations/{org_id}
Credits: 0 (free)
Authentication: Required
organizationById
Get organization by specific ID.
type Query {
organizationById(id: ID!): OrganizationNode # Requires @auth guard
}
Parameters:
id: ID!- Organization ID to retrieve
Implementation: Calls GET /organizations/{id} (with access control)
Credits: 0 (free)
Authentication: Required
account
Get account by ID.
type Query {
account(id: ID!): AccountNode # Requires @auth guard
}
Parameters:
id: ID!- Account ID to retrieve
Implementation: Calls GET /organizations/{org_id}/accounts/{account_id}
Credits: 0 (free)
Authentication: Required
transaction
Get transaction by ID.
type Query {
transaction(id: ID!): TransactionNode # Requires @auth guard
}
Parameters:
id: ID!- Transaction ID to retrieve
Implementation: Calls GET /organizations/{org_id}/transactions/{transaction_id}
Credits: 0 (free)
Authentication: Required
contact
Get contact by ID.
type Query {
contact(id: ID!): ContactNode # Requires @auth guard
}
Parameters:
id: ID!- Contact ID to retrieve
Implementation: Calls GET /organizations/{org_id}/contacts/{contact_id}
Credits: 0 (free)
Authentication: Required
invoice
Get invoice by ID.
type Query {
invoice(id: ID!): InvoiceNode # Requires @auth guard
}
Parameters:
id: ID!- Invoice ID to retrieve
Implementation: Calls GET /organizations/{org_id}/invoices/{invoice_id}
Credits: 0 (free)
Authentication: Required
bill
Get bill by ID.
type Query {
bill(id: ID!): BillNode # Requires @auth guard
}
Parameters:
id: ID!- Bill ID to retrieve
Implementation: Calls GET /organizations/{org_id}/bills/{bill_id}
Credits: 0 (free)
Authentication: Required
currencies
Get list of supported currencies.
type Query {
currencies: [CurrencyNode!]! # Requires @auth guard
}
Implementation: Calls GET /currencies
Credits: 0 (free)
Authentication: Required
Mutation Resolvers
createAccount
Create a new account.
type Mutation {
createAccount(
code: String!
name: String!
accountType: AccountType!
description: String
): AccountNode # Requires @auth guard
}
Implementation: Calls POST /organizations/{org_id}/accounts
Credits: 1
updateAccount
Update an existing account.
type Mutation {
updateAccount(
id: ID!
code: String
name: String
description: String
): AccountNode # Requires @auth guard
}
Implementation: Calls PUT /organizations/{org_id}/accounts/{account_id}
Credits: 1
deleteAccount
Delete an account.
type Mutation {
deleteAccount(id: ID!): Boolean # Requires @auth guard
}
Implementation: Calls DELETE /organizations/{org_id}/accounts/{account_id}
Credits: 1
createTransaction
Create a new transaction.
type Mutation {
createTransaction(
description: String!
date: DateTime!
entries: [LedgerEntryInput!]!
): TransactionNode # Requires @auth guard
}
Implementation: Calls POST /organizations/{org_id}/transactions
Credits: 2
createContact
Create a new contact.
type Mutation {
createContact(
name: String!
contactType: ContactType!
email: String
phone: String
taxId: String
): ContactNode # Requires @auth guard
}
Implementation: Calls POST /organizations/{org_id}/contacts (worker-queued)
Credits: 1
updateContact
Update an existing contact.
type Mutation {
updateContact(
id: ID!
name: String
email: String
phone: String
taxId: String
): ContactNode # Requires @auth guard
}
Implementation: Calls PUT /organizations/{org_id}/contacts/{contact_id} (worker-queued)
Credits: 1
deleteContact
Delete a contact.
type Mutation {
deleteContact(id: ID!): Boolean # Requires @auth guard
}
Implementation: Calls DELETE /organizations/{org_id}/contacts/{contact_id} (worker-queued)
Credits: 1
createInvoice
Create a new invoice.
type Mutation {
createInvoice(
contactId: ID!
invoiceDate: DateTime!
dueDate: DateTime!
items: [InvoiceItemInput!]!
notes: String
): InvoiceNode # Requires @auth guard
}
Implementation: Calls POST /organizations/{org_id}/invoices (worker-queued)
Credits: 5
createBill
Create a new bill.
type Mutation {
createBill(
contactId: ID!
billDate: DateTime!
dueDate: DateTime!
items: [BillItemInput!]!
notes: String
): BillNode # Requires @auth guard
}
Implementation: Calls POST /organizations/{org_id}/bills (worker-queued)
Credits: 5
recordInvoicePayment
Record a payment against an invoice.
type Mutation {
recordInvoicePayment(
invoiceId: ID!
amount: Decimal!
paymentDate: DateTime!
paymentMethod: String!
reference: String
notes: String
): PaymentNode # Requires @auth guard
}
Implementation: Calls POST /organizations/{org_id}/invoices/{invoice_id}/payments
Credits: 2
recordBillPayment
Record a payment against a bill.
type Mutation {
recordBillPayment(
billId: ID!
amount: Decimal!
paymentDate: DateTime!
paymentMethod: String!
reference: String
notes: String
): PaymentNode # Requires @auth guard
}
Implementation: Calls POST /organizations/{org_id}/bills/{bill_id}/payments (worker-queued)
Credits: 2
purchaseCredits
Purchase additional credits.
type Mutation {
purchaseCredits(
creditAmount: Int!
successUrl: String
cancelUrl: String
): CreditPurchaseNode # Requires @auth guard
}
Implementation: Calls credit purchase API with Stripe integration
Credits: 0 (billing operation)
Resolver Performance
Credit Cost Breakdown
| Resolver Type | Credit Cost | Frequency | Notes |
|---|---|---|---|
| Simple field access | 0 | Per query | Basic property access (free) |
| Single REST call | 0 | Per resolver | Direct API endpoint call (free) |
| Multiple REST calls | 0 | Per resolver | Complex object resolution (free) |
| Report generation | 5 | Per resolver | Financial calculations |
| Worker operations | 1-5 | Per mutation | Background processing |
Optimization Strategies
1. Limit Related Data
# ✅ Good - Limited resolver calls
query EfficientQuery {
organization {
accounts(limit: 10) {
id
name
# No transactions resolver call
}
}
}
# ❌ Bad - Excessive resolver calls
query InefficientQuery {
organization {
accounts {
id
name
transactions(limit: 100) { # Calls resolver for EACH account
id
description
entries { # Calls more resolvers
account {
name # Even more resolver calls
}
}
}
}
}
}
2. Use Query Fragments
fragment BasicAccount on AccountNode {
id
code
name
balance {
formatted
}
}
query GetAccountsEfficiently {
organization {
accounts {
...BasicAccount
# No transaction resolvers called
}
}
}
3. Batch Operations
# Instead of multiple queries
query GetMultipleEntities {
account1: account(id: "act_1") { ...AccountFields }
account2: account(id: "act_2") { ...AccountFields }
account3: account(id: "act_3") { ...AccountFields }
}
4. Cache Resolver Results
// Client-side caching
const cache = new Map();
async function cachedResolver(key, resolver) {
if (cache.has(key)) {
return cache.get(key);
}
const result = await resolver();
cache.set(key, result);
return result;
}
Error Handling in Resolvers
Resolver Error Types
// Authentication errors
#[error("Authentication required")]
Unauthorized,
// Permission errors
#[error("Access denied to organization")]
Forbidden,
// Not found errors
#[error("Account not found: {0}")]
AccountNotFound(String),
// Business logic errors
#[error("Transaction does not balance")]
TransactionUnbalanced,
// External API errors
#[error("REST API call failed: {0}")]
ApiCallError(String),
Error Propagation
// GraphQL errors are properly formatted
{
"errors": [
{
"message": "Account not found: act_invalid_id",
"locations": [{ "line": 3, "column": 5 }],
"path": ["account"],
"extensions": {
"code": "NOT_FOUND"
}
}
]
}
Resolver Testing
Unit Testing
#[test]
fn test_account_balance_resolver() {
let mock_api = MockApiClient::new();
let resolvers = GraphQLResolvers::new(mock_api);
// Test resolver logic
let result = resolvers.get_account_balance(ctx, "act_123").await;
assert!(result.is_ok());
}
Integration Testing
#[tokio::test]
async fn test_organization_accounts_resolver() {
let app_state = setup_test_state().await;
let resolvers = GraphQLResolvers::new(app_state);
// Test full resolver chain
let ctx = create_test_context("org_123");
let result = resolvers.get_accounts(ctx).await;
assert!(result.is_ok());
assert!(!result.unwrap().is_empty());
}
Resolver Monitoring
Performance Metrics
// Track resolver performance
#[derive(Debug)]
struct ResolverMetrics {
resolver_name: String,
execution_time: Duration,
credit_cost: Decimal,
success: bool,
error_type: Option<String>,
}
// Log resolver execution
async fn execute_resolver_with_metrics<F, T>(
resolver_name: &str,
credit_cost: Decimal,
resolver: F,
) -> Result<T>
where
F: Future<Output = Result<T>>,
{
let start = Instant::now();
let result = resolver.await;
let duration = start.elapsed();
log_resolver_metrics(ResolverMetrics {
resolver_name: resolver_name.to_string(),
execution_time: duration,
credit_cost,
success: result.is_ok(),
error_type: result.as_ref().err().map(|e| e.to_string()),
});
result
}
Credit Usage Tracking
// Track cumulative credit usage per resolver
#[derive(Default)]
struct CreditTracker {
total_credits_used: Decimal,
resolver_usage: HashMap<String, Decimal>,
}
impl CreditTracker {
fn record_usage(&mut self, resolver_name: &str, credits: Decimal) {
self.total_credits_used += credits;
*self.resolver_usage.entry(resolver_name.to_string()).or_insert(Decimal::ZERO) += credits;
}
fn get_usage_report(&self) -> Value {
json!({
"total_credits_used": self.total_credits_used,
"resolver_breakdown": self.resolver_usage
})
}
}
Best Practices
Resolver Design
- Keep Resolvers Focused:
// ✅ Good - Single responsibility
async fn get_account_balance(&self, account_id: &str) -> Result<MoneyNode>
// ❌ Bad - Multiple responsibilities
async fn get_account_with_balance_and_transactions(&self, account_id: &str) -> Result<ComplexObject>
- Handle Errors Gracefully:
async fn safe_resolver_call(&self, operation: &str) -> Result<T> {
match self.api_call(operation).await {
Ok(result) => Ok(result),
Err(err) => {
tracing::error!("Resolver {} failed: {}", operation, err);
Err(err.into())
}
}
}
- Implement Caching Where Appropriate:
// Cache static data
lazy_static! {
static ref CURRENCY_CACHE: Mutex<HashMap<String, CurrencyNode>> = Mutex::new(HashMap::new());
}
async fn get_cached_currency(&self, code: &str) -> Result<CurrencyNode> {
if let Some(currency) = CURRENCY_CACHE.lock().await.get(code) {
return Ok(currency.clone());
}
let currency = self.api_get(&format!("/currencies/{}", code)).await?;
CURRENCY_CACHE.lock().await.insert(code.to_string(), currency.clone());
Ok(currency)
}
- Validate Inputs:
fn validate_account_id(account_id: &str) -> Result<()> {
if !account_id.starts_with("act_") {
return Err(Error::new("Invalid account ID format"));
}
Ok(())
}
Performance Optimization
- Batch API Calls:
// Instead of multiple individual calls
async fn get_multiple_balances(&self, account_ids: &[String]) -> Result<Vec<MoneyNode>> {
let mut balances = Vec::new();
for chunk in account_ids.chunks(10) { // Batch in groups of 10
let batch_result = self.api_post("/accounts/batch-balance", json!({ "ids": chunk })).await?;
balances.extend(batch_result);
}
Ok(balances)
}
- Connection Pooling:
// Reuse HTTP connections
#[derive(Clone)]
pub struct GraphQLResolvers {
pub http_client: Client, // Connection pool
pub base_url: String,
}
- Async Processing:
// Don't block on slow operations
async fn get_complex_report(&self, org_id: &str) -> Result<Report> {
// Queue to background worker
let job_id = self.worker_queue.queue_job("generate_report", json!({
"organization_id": org_id
})).await?;
// Return job status immediately
Ok(JobResult {
job_id,
status: "queued",
estimated_completion: Utc::now() + Duration::minutes(5),
})
}
GraphQL resolvers in Crane Ledger provide efficient, cost-effective access to your accounting data. Each resolver is carefully designed to balance functionality with performance, ensuring your GraphQL operations are both powerful and economical.
Need help?
Create a free account to access our support portal. Once signed in, use the Support tab in your dashboard to submit a support ticket — our team typically responds within 24 hours.
- ✨ For LLMs/AI assistants: Read our structured API reference