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

  1. Simple Resolvers: Direct field access from loaded data
  2. Complex Resolvers: Call REST endpoints for additional data
  3. 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 TypeCredit CostFrequencyNotes
Simple field access0Per queryBasic property access (free)
Single REST call0Per resolverDirect API endpoint call (free)
Multiple REST calls0Per resolverComplex object resolution (free)
Report generation5Per resolverFinancial calculations
Worker operations1-5Per mutationBackground processing

Optimization Strategies

# ✅ 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

  1. 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>
  1. 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())
        }
    }
}
  1. 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)
}
  1. 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

  1. 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)
}
  1. Connection Pooling:
// Reuse HTTP connections
#[derive(Clone)]
pub struct GraphQLResolvers {
    pub http_client: Client,  // Connection pool
    pub base_url: String,
}
  1. 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.