Summary and Aggregation

Data Aggregation and Summary System

Overview

The Finale GraphQL API provides a powerful aggregation system that enables you to calculate metrics and perform dimensional analysis across your data, similar to SQL's GROUP BY with aggregate functions like SUM, AVG, MIN, MAX, and COUNT.

Key Capabilities

The aggregation system is available on all 24 ViewConnection types in the API and enables you to:

  • Calculate Metrics: Aggregate data using operators like sum, mean (average), min, and max
  • Group by Dimensions: Break down metrics by categories, time periods, suppliers, customers, and other dimensions
  • Build Dashboards: Create analytics dashboards and reports with KPIs
  • Analyze Trends: Track performance over time with date-based grouping
  • Generate Reports: Create summary reports without fetching individual records

How It Works

The aggregation system adds a summary field to any ViewConnection query. This field returns calculated metrics and dimensional groupings separately from the main edges data, allowing you to:

  1. Get aggregated results without fetching individual items
  2. Apply filters at the query level to narrow down what's aggregated
  3. Request multiple metrics and groupings in a single query
  4. Handle aggregation errors independently from the main query

Important Concept: Parallel Arrays

Aggregation results use aligned arrays where each index position corresponds across all metrics and groupBy dimensions:

{
  groupBy: {
    category: ["Electronics", "Clothing", "Home & Garden"]
  },
  metrics: {
    totalStock: [15230, 28450, 12890],
    totalSales: [8934, 15678, 7234]
  }
}

In this example, groupBy.category[0] ("Electronics") corresponds to metrics.totalStock[0] (15230) and metrics.totalSales[0] (8934).

Supported Collections

All 24 Collections Support Metrics:

buildViewConnection, consolidatedItemViewConnection, facilityViewConnection,
generalLedgerAccountViewConnection, generalLedgerViewConnection, invoiceItemViewConnection,
invoiceViewConnection, journalEntryItemViewConnection, journalEntryViewConnection,
orderItemViewConnection, orderViewConnection, partyViewConnection, paymentViewConnection,
productLookupViewConnection, productStoreViewConnection, productViewConnection,
quickTransferViewConnection, returnItemViewConnection, returnViewConnection,
shipmentViewConnection, stockHistoryViewConnection, stockViewConnection,
userLoginViewConnection, varianceViewConnection

9 Collections Support GroupBy (Dimensional Analysis):

invoice, invoiceItem, journalEntry, journalEntryItem, order, orderItem,
payment, product, returnItem

Collections without GroupBy support can still calculate metrics, but cannot break down results by dimensions.


Basic Usage

Query Structure

To use aggregation, add the summary field to any ViewConnection query:

{collectionName}ViewConnection(filters...) {
  summary {
    errorCode
    errorMessage
    metrics { ... }
    groupBy { ... }  # Only available on 9 collections
  }
}

Field Descriptions:

  • errorCode: Error code if aggregation fails (null on success)
  • errorMessage: Human-readable error message (null on success)
  • metrics: The aggregated values you requested
  • groupBy: The dimensional groupings (only on supported collections)

Important Notes:

  1. Aggregations can fail independently of the main query - always check errorCode before using results
  2. Even when only using summary, ViewConnection queries still require a pagination parameter like first: 1
  3. Filters applied to the ViewConnection affect what data is aggregated

Simple Example: Counting Products

query {
  productViewConnection(first: 1, category: ["Electronics"]) {
    summary {
      errorCode
      errorMessage
      metrics {
        totalProducts: count
      }
    }
  }
}

Result:

{
  "data": {
    "productViewConnection": {
      "summary": {
        "errorCode": null,
        "errorMessage": null,
        "metrics": {
          "totalProducts": [1523]
        }
      }
    }
  }
}

Note that count returns an array with a single element [1523] even though there's no grouping. All metrics return arrays.


Metrics: Aggregating Data

Metrics are calculated values derived from your data using aggregation operators.

Universal Metric: Count

Every collection supports the count metric, which returns the number of items matching your filters:

metrics {
  itemCount: count
}

Return Type

All metrics return [Float] (array of floats). When there's no grouping, the array contains one element. With grouping, there's one element per group.

Aggregation Operators

Most metrics accept an operator parameter to specify how values should be aggregated:

OperatorDescriptionExample Use Case
sumSum all valuesTotal sales revenue, total quantity
meanCalculate averageAverage order value, average price
minFind minimum valueEarliest order date, lowest price
maxFind maximum valueLatest shipment date, highest quantity

Transform Parameter

Some metrics also accept a transform parameter:

TransformDescriptionUse Case
absApply absolute valueAggregate magnitudes ignoring positive/negative

Using GraphQL Aliases

GraphQL aliases let you request the same metric multiple times with different operators:

metrics {
  totalStock: stock(operator: sum)
  avgStock: stock(operator: mean)
  maxStock: stock(operator: max)
  minStock: stock(operator: min)
  productCount: count
}

Field-Specific Parameters

Many metrics accept additional parameters to filter or configure what's aggregated. Common parameters include:

  • dateRange: { start: "2024-01-01", end: "2024-12-31" } - Filter metrics by date range
  • facilityUrlList: ["facility1", "facility2"] - Filter by specific facilities
  • productUrlList: [...] - Filter by specific products
  • Various enum parameters for aggregation methods and options

The available parameters vary by collection and metric. See the Discovering Available Metrics section to explore what's available.

Example: Multiple Metrics Without Grouping

query {
  productViewConnection(
    first: 1
    category: ["Electronics"]
  ) {
    summary {
      errorCode
      errorMessage
      metrics {
        totalProducts: count
        totalStock: stock(operator: sum, facilityUrlList: ["warehouse1"])
        avgPrice: averageGrossSalesPerUnit(
          operator: mean
          dateRange: { start: "2024-01-01", end: "2024-12-31" }
        )
        minPrice: averageGrossSalesPerUnit(operator: min)
        maxPrice: averageGrossSalesPerUnit(operator: max)
      }
    }
  }
}

Result:

{
  "data": {
    "productViewConnection": {
      "summary": {
        "errorCode": null,
        "errorMessage": null,
        "metrics": {
          "totalProducts": [1523],
          "totalStock": [45230.5],
          "avgPrice": [29.99],
          "minPrice": [5.99],
          "maxPrice": [299.99]
        }
      }
    }
  }
}

Example: Order Metrics

query {
  orderViewConnection(
    first: 1
    type: ["SALES_ORDER"]
    orderDate: { start: "2024-01-01", end: "2024-12-31" }
  ) {
    summary {
      errorCode
      errorMessage
      metrics {
        orderCount: count
        totalRevenue: total(operator: sum)
        avgOrderValue: total(operator: mean)
        totalUnits: totalUnits(operator: sum)
        earliestOrder: orderDate(operator: min)
        latestOrder: orderDate(operator: max)
      }
    }
  }
}

Result:

{
  "data": {
    "orderViewConnection": {
      "summary": {
        "errorCode": null,
        "errorMessage": null,
        "metrics": {
          "orderCount": [2847],
          "totalRevenue": [482950.75],
          "avgOrderValue": [169.68],
          "totalUnits": [8934],
          "earliestOrder": ["2024-01-02T08:15:00Z"],
          "latestOrder": ["2024-12-30T16:45:00Z"]
        }
      }
    }
  }
}

Common Metrics by Collection

Product Metrics (24 total):

count, stock, stockOnHand, stockReserved, stockOnOrder, stockAvailable, stockAllocated,
stockInboundQuantity, reorderQuantityToOrder, replenishmentQuantityToOrder, unitSales,
grossSales, cogsSales, marginSales, countSales, averageGrossSalesPerUnit, averageUnitsPerSale,
neqPerUnit, lastPurchaseLandedCostPerUnit, lastPurchasePrice, reorderVariance,
unitSalesForSalesVelocity, reorderQuantityToOrderEditable, replenishmentQuantityToOrderEditable

Order Metrics (12 total):

count, orderDate, subtotal, taxableSubtotal, total, netSales, totalUnits,
totalTaxDiscountsAndFees, totalAverageCost, grossIncome, grossMargin, shipmentPendingValue

Invoice Metrics (4 total):

count, total, subtotal, balance

GroupBy: Dimensional Analysis

GroupBy enables you to break down metrics by dimensions (categories, time periods, suppliers, etc.), similar to SQL's GROUP BY clause.

Availability

Only 9 of 24 collections support GroupBy:

  • invoice
  • invoiceItem
  • journalEntry
  • journalEntryItem
  • order
  • orderItem
  • payment
  • product
  • returnItem

Other collections support metrics-only aggregation.

Return Type

All groupBy fields return [String] (array of strings), with one element per unique group value.

Array Alignment

The most critical concept: groupBy arrays and metrics arrays align by index position:

{
  groupBy: {
    category: ["Electronics", "Clothing", "Home & Garden"]
  },
  metrics: {
    productCount: [523, 892, 654],
    totalStock: [15230, 28450, 12890]
  }
}

This means:

  • "Electronics" has 523 products with 15,230 total stock
  • "Clothing" has 892 products with 28,450 total stock
  • "Home & Garden" has 654 products with 12,890 total stock

Formatter Parameter

GroupBy fields accept an optional formatter parameter to control how values are displayed:

FormatterDescription
"html"HTML formatted with links
"none"Raw value without formatting
"abbreviated"Shortened version
"blank-zero"Empty string for zero values
groupBy {
  category: category(formatter: "none")
}

Single Dimension Grouping

Group by one dimension to see metrics broken down by that dimension:

query {
  productViewConnection(first: 1) {
    summary {
      errorCode
      errorMessage
      groupBy {
        category: category
      }
      metrics {
        productCount: count
        totalStock: stock(operator: sum)
        totalSales: unitSales(
          operator: sum
          dateRange: { start: "2024-01-01", end: "2024-12-31" }
        )
      }
    }
  }
}

Result:

{
  "data": {
    "productViewConnection": {
      "summary": {
        "errorCode": null,
        "errorMessage": null,
        "groupBy": {
          "category": ["Electronics", "Clothing", "Home & Garden"]
        },
        "metrics": {
          "productCount": [523, 892, 654],
          "totalStock": [15230, 28450, 12890],
          "totalSales": [8934, 15678, 7234]
        }
      }
    }
  }
}

Multiple Dimension Grouping

Request multiple dimensions to create more detailed breakdowns:

query {
  productViewConnection(first: 1, category: ["Electronics"]) {
    summary {
      groupBy {
        category: category
        supplier: supplier1
      }
      metrics {
        productCount: count
        totalStock: stock(operator: sum)
      }
    }
  }
}

Result:

{
  "data": {
    "productViewConnection": {
      "summary": {
        "groupBy": {
          "category": ["Electronics", "Electronics", "Electronics"],
          "supplier": ["Supplier A", "Supplier B", "Supplier C"]
        },
        "metrics": {
          "productCount": [150, 250, 123],
          "totalStock": [5200, 7800, 2230]
        }
      }
    }
  }
}

This shows three groups: Electronics from Supplier A (150 products), Electronics from Supplier B (250 products), and Electronics from Supplier C (123 products).

Hierarchical Grouping

For hierarchical analysis (like category → subcategory), you can use aliases like group and subgroup:

groupBy {
  group: category
  subgroup: supplier1
}

This is a naming convention used in Finale's dashboard UI for two-level hierarchical grouping, but you can use any alias names you prefer.

Example: Orders by Date and Customer

query {
  orderViewConnection(
    first: 1
    type: ["SALES_ORDER"]
    orderDate: { start: "2024-01-01", end: "2024-12-31" }
  ) {
    summary {
      groupBy {
        orderMonth: orderDate
        customer: partyUrl
      }
      metrics {
        orderCount: count
        revenue: total(operator: sum)
        avgOrderValue: total(operator: mean)
        units: totalUnits(operator: sum)
      }
    }
  }
}

Common GroupBy Dimensions

Product Dimensions (4 total):

category, supplier1, supplier2, supplier3

Order Dimensions: Available dimensions include order date, customer (party), location, and other order attributes. Check your schema for the complete list.

Invoice Dimensions: Available dimensions include invoice date, customer, and other invoice attributes.


Advanced Patterns

Pattern 1: Combining Filters with Summaries

Filters applied to the ViewConnection narrow down what's aggregated:

query {
  orderViewConnection(
    first: 1
    type: ["SALES_ORDER"]
    status: ["COMPLETED"]
    orderDate: { start: "2024-01-01", end: "2024-12-31" }
  ) {
    summary {
      # Only aggregates completed sales orders from 2024
      metrics {
        count: count
        revenue: total(operator: sum)
      }
    }
  }
}

This pattern is essential for focused analytics: apply broad filters at the query level, then use groupBy to break down the filtered dataset.

Pattern 2: Aggregations Without Fetching Items

When you only need aggregated data, fetch minimal items to reduce data transfer:

query {
  productViewConnection(first: 1) {
    edges {
      node {
        productId  # Minimal field
      }
    }
    summary {
      # Get complete aggregations without fetching all products
      groupBy {
        category: category
      }
      metrics {
        productCount: count
        totalStock: stock(operator: sum)
        avgPrice: averageGrossSalesPerUnit(operator: mean)
      }
    }
  }
}

This pattern is useful for dashboards and reports where you don't need individual item details.

Pattern 3: Date-Based Grouping

Date dimensions can be grouped by different time periods using operators:

query {
  orderViewConnection(
    first: 1
    type: ["SALES_ORDER"]
    orderDate: { start: "2024-01-01", end: "2024-12-31" }
  ) {
    summary {
      groupBy {
        week: orderDate  # Can use operators like DAY, WEEK, MONTH, QUARTER, YEAR
      }
      metrics {
        orders: count
        revenue: total(operator: sum)
        units: totalUnits(operator: sum)
      }
    }
  }
}

The specific date operators available depend on the field. Check your schema or use introspection to discover what's available.

Pattern 4: Handling Null and Empty Results

Always check for errors and handle empty arrays:

const summary = data?.productViewConnection?.summary

if (summary?.errorCode) {
  console.error('Aggregation failed:', summary.errorMessage)
  return
}

const productCount = summary?.metrics?.totalProducts?.[0] ?? 0
const categories = summary?.groupBy?.category ?? []

if (categories.length === 0) {
  console.log('No categories found')
}

Pattern 5: Multiple Metrics with Different Parameters

Use aliases to request the same metric with different filters:

query {
  productViewConnection(first: 1) {
    summary {
      metrics {
        totalStock: stock(operator: sum)
        warehouse1Stock: stock(operator: sum, facilityUrlList: ["warehouse1"])
        warehouse2Stock: stock(operator: sum, facilityUrlList: ["warehouse2"])
        q1Sales: unitSales(
          operator: sum
          dateRange: { start: "2024-01-01", end: "2024-03-31" }
        )
        q2Sales: unitSales(
          operator: sum
          dateRange: { start: "2024-04-01", end: "2024-06-30" }
        )
      }
    }
  }
}

Pattern 6: Processing Results into Tables

Transform parallel arrays into row-based data for display:

// Based on Finale dashboard code
function transposeToTable(summary) {
  const { groupBy, metrics } = summary

  // Get array length from first groupBy or metric
  const length = Object.values(groupBy)[0]?.length ?? Object.values(metrics)[0]?.length ?? 0

  // Create row for each index
  const rows = []
  for (let i = 0; i < length; i++) {
    const row = {}

    // Add groupBy values
    Object.keys(groupBy).forEach(key => {
      row[key] = groupBy[key][i]
    })

    // Add metric values
    Object.keys(metrics).forEach(key => {
      row[key] = metrics[key][i]
    })

    rows.push(row)
  }

  return rows
}

// Usage
const rows = transposeToTable(data.productViewConnection.summary)
// Result: [
//   { category: "Electronics", productCount: 523, totalStock: 15230 },
//   { category: "Clothing", productCount: 892, totalStock: 28450 },
//   { category: "Home & Garden", productCount: 654, totalStock: 12890 }
// ]

Complete Real-World Examples

Example 1: Product Analytics Dashboard

This example shows how to build a comprehensive product analytics view with multiple perspectives:

query ProductAnalytics {
  # Analysis by Category
  byCategory: productViewConnection(first: 1) {
    summary {
      errorCode
      errorMessage
      groupBy {
        category: category
      }
      metrics {
        products: count
        totalStock: stock(operator: sum)
        avgStock: stock(operator: mean)
        lowStock: stock(operator: min)
        highStock: stock(operator: max)
        totalValue: neqPerUnit(operator: sum)
      }
    }
  }

  # Analysis by Supplier
  bySupplier: productViewConnection(first: 1) {
    summary {
      errorCode
      errorMessage
      groupBy {
        supplier: supplier1
      }
      metrics {
        products: count
        reorderNeeded: reorderQuantityToOrder(operator: sum)
        totalCost: lastPurchaseLandedCostPerUnit(operator: sum)
        avgLeadTime: stock(operator: mean)
      }
    }
  }

  # Overall Totals
  totals: productViewConnection(first: 1) {
    summary {
      errorCode
      errorMessage
      metrics {
        totalProducts: count
        totalStock: stock(operator: sum)
        totalValue: neqPerUnit(operator: sum)
        avgValue: neqPerUnit(operator: mean)
      }
    }
  }

  # Low Stock Alert
  lowStock: productViewConnection(
    first: 10
    stockStatus: ["LOW"]
  ) {
    summary {
      errorCode
      errorMessage
      metrics {
        lowStockCount: count
        reorderNeeded: reorderQuantityToOrder(operator: sum)
      }
    }
  }
}

Example 2: Sales Analysis by Time Period

Track sales performance over time with multiple metrics:

query SalesAnalysis {
  orderViewConnection(
    first: 1
    type: ["SALES_ORDER"]
    status: ["COMPLETED"]
    orderDate: { start: "2024-01-01", end: "2024-12-31" }
  ) {
    summary {
      errorCode
      errorMessage
      groupBy {
        month: orderDate  # Groups by month
      }
      metrics {
        orders: count
        revenue: total(operator: sum)
        avgOrderValue: total(operator: mean)
        minOrderValue: total(operator: min)
        maxOrderValue: total(operator: max)
        units: totalUnits(operator: sum)
        avgUnitsPerOrder: totalUnits(operator: mean)
        grossMargin: grossMargin(operator: sum)
        avgMargin: grossMargin(operator: mean)
        netSales: netSales(operator: sum)
      }
    }
  }
}

Processing the results:

const { summary } = data.orderViewConnection

if (summary.errorCode) {
  console.error('Failed to load sales data:', summary.errorMessage)
  return
}

// Transform into chart-friendly format
const chartData = summary.groupBy.month.map((month, i) => ({
  month,
  orders: summary.metrics.orders[i],
  revenue: summary.metrics.revenue[i],
  avgOrderValue: summary.metrics.avgOrderValue[i],
  units: summary.metrics.units[i],
  margin: summary.metrics.grossMargin[i]
}))

// Calculate totals
const totals = {
  orders: summary.metrics.orders.reduce((a, b) => a + b, 0),
  revenue: summary.metrics.revenue.reduce((a, b) => a + b, 0),
  units: summary.metrics.units.reduce((a, b) => a + b, 0)
}

// Calculate year-over-year growth
const revenueByMonth = summary.metrics.revenue
const avgMonthlyRevenue = revenueByMonth.reduce((a, b) => a + b, 0) / revenueByMonth.length

Example 3: Product Sales Performance

Analyze which products are performing best:

query ProductSalesPerformance {
  orderItemViewConnection(
    first: 1
    orderDate: { start: "2024-01-01", end: "2024-12-31" }
    orderType: ["SALES_ORDER"]
  ) {
    summary {
      errorCode
      errorMessage
      groupBy {
        product: productUrl
      }
      metrics {
        quantitySold: quantity(operator: sum)
        revenue: total(operator: sum)
        avgPrice: price(operator: mean)
        orderCount: count
        avgQuantityPerOrder: quantity(operator: mean)
      }
    }
  }
}

Example 4: Customer Analysis

Segment customers by purchase behavior:

query CustomerAnalysis {
  orderViewConnection(
    first: 1
    type: ["SALES_ORDER"]
    status: ["COMPLETED"]
    orderDate: { start: "2024-01-01", end: "2024-12-31" }
  ) {
    summary {
      errorCode
      errorMessage
      groupBy {
        customer: partyUrl
      }
      metrics {
        orderCount: count
        totalSpent: total(operator: sum)
        avgOrderValue: total(operator: mean)
        totalUnits: totalUnits(operator: sum)
        firstOrder: orderDate(operator: min)
        lastOrder: orderDate(operator: max)
      }
    }
  }
}

Processing for customer segmentation:

const { summary } = data.orderViewConnection

if (!summary.errorCode) {
  const customers = summary.groupBy.customer.map((customer, i) => ({
    customer,
    orderCount: summary.metrics.orderCount[i],
    totalSpent: summary.metrics.totalSpent[i],
    avgOrderValue: summary.metrics.avgOrderValue[i],
    totalUnits: summary.metrics.totalUnits[i],
    firstOrder: summary.metrics.firstOrder[i],
    lastOrder: summary.metrics.lastOrder[i]
  }))

  // Segment customers
  const vipCustomers = customers.filter(c => c.totalSpent > 10000)
  const regularCustomers = customers.filter(c => c.totalSpent >= 1000 && c.totalSpent <= 10000)
  const newCustomers = customers.filter(c => c.orderCount === 1)

  console.log(`VIP Customers: ${vipCustomers.length}`)
  console.log(`Regular Customers: ${regularCustomers.length}`)
  console.log(`New Customers: ${newCustomers.length}`)
}

Example 5: Inventory Reorder Analysis

Identify products that need reordering:

query ReorderAnalysis {
  productViewConnection(first: 1) {
    summary {
      errorCode
      errorMessage
      groupBy {
        category: category
        supplier: supplier1
      }
      metrics {
        productsNeedingReorder: count
        totalReorderQuantity: reorderQuantityToOrder(operator: sum)
        estimatedCost: lastPurchaseLandedCostPerUnit(operator: sum)
        currentStock: stock(operator: sum)
        avgReorderPoint: reorderQuantityToOrder(operator: mean)
      }
    }
  }
}

Performance Considerations

Understanding Performance Impact

Aggregations can be computationally expensive, especially on large datasets. Consider these factors:

  1. Dataset Size: Larger datasets take longer to aggregate
  2. Number of Groups: More unique group values increase processing time
  3. Number of Metrics: Each metric requires calculation across all data
  4. Complex Parameters: Metrics with field-specific parameters (like date ranges) may require additional filtering

Optimization Strategies

1. Apply Filters at Query Level

Reduce the dataset before aggregation by applying filters:

# Less efficient - aggregating all orders
query {
  orderViewConnection(first: 1) {
    summary {
      metrics {
        revenue: total(operator: sum)
      }
    }
  }
}

# More efficient - aggregating filtered orders
query {
  orderViewConnection(
    first: 1
    type: ["SALES_ORDER"]
    status: ["COMPLETED"]
    orderDate: { start: "2024-01-01", end: "2024-12-31" }
  ) {
    summary {
      metrics {
        revenue: total(operator: sum)
      }
    }
  }
}

2. Use Minimal Pagination When Only Using Summary

When you only need aggregations, fetch minimal items:

query {
  productViewConnection(first: 1) {  # Fetch only 1 item
    edges {
      node { productId }  # Minimal fields
    }
    summary {
      # Complete aggregations
      metrics { ... }
    }
  }
}

3. Request Only Needed Metrics

Don't request metrics you won't use:

# Less efficient - requesting many unused metrics
metrics {
  metric1: field1(operator: sum)
  metric2: field2(operator: sum)
  metric3: field3(operator: sum)
  metric4: field4(operator: sum)
  # ... 10 more metrics you don't need
}

# More efficient - only what you need
metrics {
  revenue: total(operator: sum)
  orderCount: count
}

4. Consider Parallel Queries for Different Analyses

Instead of one complex query with many dimensions, split into multiple focused queries:

# Can run in parallel for better performance
query Analytics {
  byCategory: productViewConnection(first: 1) {
    summary {
      groupBy { category: category }
      metrics { count: count, stock: stock(operator: sum) }
    }
  }

  bySupplier: productViewConnection(first: 1) {
    summary {
      groupBy { supplier: supplier1 }
      metrics { count: count, stock: stock(operator: sum) }
    }
  }
}

Performance Best Practices Summary

  1. Always apply filters at the ViewConnection level to reduce dataset size
  2. Use first: 1 when only using summary data
  3. Request only the metrics and dimensions you need
  4. Consider date range filters to limit aggregation scope
  5. Test performance with realistic data volumes
  6. Monitor for timeouts on large datasets

Error Handling

Aggregations can fail independently of the main query, so proper error handling is essential.

Error Response Structure

When an aggregation fails, the response includes error information:

{
  "data": {
    "productViewConnection": {
      "summary": {
        "errorCode": "AGGREGATION_TIMEOUT",
        "errorMessage": "Aggregation exceeded maximum execution time",
        "metrics": null,
        "groupBy": null
      }
    }
  }
}

Note that the main query can succeed even when the aggregation fails.

Always Check ErrorCode

Always check errorCode before using aggregation results:

const summary = data?.productViewConnection?.summary

if (!summary) {
  console.error('No summary data returned')
  return
}

if (summary.errorCode) {
  console.error('Aggregation error:', {
    code: summary.errorCode,
    message: summary.errorMessage
  })
  // Show error to user or fall back to alternative approach
  return
}

// Safe to use metrics and groupBy
const productCount = summary.metrics.totalProducts[0]

Common Error Scenarios

1. Timeout on Large Datasets

if (summary.errorCode === 'AGGREGATION_TIMEOUT') {
  console.warn('Aggregation timed out - try adding more filters to reduce dataset size')
  // Suggest user applies date range or other filters
}

2. Invalid Operator for Field Type

# This might fail if the field doesn't support the operator
metrics {
  result: textField(operator: sum)  # Can't sum text values
}

3. Permission Issues

if (summary.errorCode === 'PERMISSION_DENIED') {
  console.error('User does not have permission to aggregate this data')
  // Hide or disable aggregation features
}

4. Invalid Parameter Combinations

# Some parameter combinations may be invalid
metrics {
  stock: stock(
    operator: sum
    facilityUrlList: ["invalid-facility"]
    # Invalid facility might cause an error
  )
}

Best Practices for Error Handling

1. Defensive Data Access

Use optional chaining and nullish coalescing:

const productCount = summary?.metrics?.totalProducts?.[0] ?? 0
const categories = summary?.groupBy?.category ?? []
const hasError = Boolean(summary?.errorCode)

2. Graceful Degradation

Provide fallback experiences when aggregations fail:

function renderProductStats(data) {
  const summary = data?.productViewConnection?.summary

  if (summary?.errorCode) {
    // Fall back to showing message
    return (
      <Alert type="warning">
        Unable to load statistics. {summary.errorMessage}
      </Alert>
    )
  }

  // Render full stats
  return (
    <StatsPanel>
      <Stat label="Total Products" value={summary.metrics.totalProducts[0]} />
      <Stat label="Total Stock" value={summary.metrics.totalStock[0]} />
    </StatsPanel>
  )
}

3. User-Friendly Error Messages

Translate error codes to helpful messages:

function getErrorMessage(errorCode, errorMessage) {
  const messages = {
    'AGGREGATION_TIMEOUT': 'This query is taking too long. Try narrowing your search with filters.',
    'PERMISSION_DENIED': 'You do not have permission to view these statistics.',
    'INVALID_OPERATOR': 'The selected calculation is not available for this field.',
    'INVALID_PARAMETERS': 'Invalid filter settings. Please check your selections.'
  }

  return messages[errorCode] || errorMessage || 'An error occurred loading statistics.'
}

4. Retry with Simpler Query

If a complex aggregation fails, try a simpler version:

async function fetchProductStats(filters) {
  try {
    // Try full aggregation with grouping
    const result = await client.query({
      query: PRODUCT_STATS_WITH_GROUPING,
      variables: filters
    })

    if (result.data?.productViewConnection?.summary?.errorCode) {
      // Fall back to metrics-only
      return fetchProductStatsSimple(filters)
    }

    return result
  } catch (error) {
    console.error('Failed to fetch product stats:', error)
    return null
  }
}

async function fetchProductStatsSimple(filters) {
  // Simpler query without groupBy
  return client.query({
    query: PRODUCT_STATS_NO_GROUPING,
    variables: filters
  })
}

5. Logging and Monitoring

Log aggregation errors for monitoring and debugging:

function logAggregationError(queryName, errorCode, errorMessage, filters) {
  console.error('Aggregation failed', {
    query: queryName,
    errorCode,
    errorMessage,
    filters,
    timestamp: new Date().toISOString()
  })

  // Send to monitoring service
  analytics.track('aggregation_error', {
    query: queryName,
    errorCode
  })
}

Discovering Available Metrics and Dimensions

Different collections support different metrics and groupBy dimensions. Here's how to discover what's available.

Collection-Specific Documentation

Each collection type has its own set of available metrics and dimensions. Refer to the collection-specific documentation pages for complete lists:

  • Product Collection: Lists all 24+ product metrics and 4 groupBy dimensions
  • Order Collection: Lists all 12+ order metrics and available groupBy dimensions
  • Invoice Collection: Lists invoice metrics and dimensions
  • And so on for all 24 collections

Using GraphQL Introspection

You can dynamically discover available metrics and their parameters using GraphQL introspection:

Discovering Metrics

query {
  __type(name: "productViewConnectionSummaryMetric") {
    fields {
      name
      description
      type {
        name
        kind
        ofType { name }
      }
      args {
        name
        description
        type {
          name
          kind
          ofType { name }
        }
      }
    }
  }
}

This returns all available metrics for the product collection, including:

  • Metric names
  • Return types (always [Float])
  • Available parameters (operator, transform, etc.)
  • Parameter types and whether they're required

Discovering GroupBy Dimensions

query {
  __type(name: "productViewConnectionSummaryGroupBy") {
    fields {
      name
      description
      type {
        name
        kind
        ofType { name }
      }
      args {
        name
        description
        type {
          name
          kind
          ofType { name }
        }
      }
    }
  }
}

This returns all available groupBy dimensions for the product collection.

Discovering Operator Enums

query {
  __type(name: "SummarizeOperator") {
    enumValues {
      name
      description
    }
  }
}

Returns: sum, mean, min, max

query {
  __type(name: "SummarizeTransform") {
    enumValues {
      name
      description
    }
  }
}

Returns: abs

Finding Collections with GroupBy Support

To determine which collections support groupBy, look for types ending in ViewConnectionSummaryGroupBy:

# Using schema file
jq -r '.data.__schema.types[] | select(.name | endswith("ViewConnectionSummaryGroupBy")) | .name | sub("ViewConnectionSummaryGroupBy"; "")' schema.json | sort

Returns:

invoice
invoiceItem
journalEntry
journalEntryItem
order
orderItem
payment
product
returnItem

Programmatic Discovery

Build tools to explore the schema programmatically:

async function getAvailableMetrics(collectionName) {
  const typeName = `${collectionName}ViewConnectionSummaryMetric`

  const result = await client.query({
    query: gql`
      query {
        __type(name: "${typeName}") {
          fields {
            name
            args {
              name
              type {
                name
                kind
                ofType { name kind }
              }
            }
          }
        }
      }
    `
  })

  return result.data.__type.fields
}

async function supportsGroupBy(collectionName) {
  const typeName = `${collectionName}ViewConnectionSummaryGroupBy`

  const result = await client.query({
    query: gql`
      query {
        __type(name: "${typeName}") {
          name
          fields { name }
        }
      }
    `
  })

  return result.data.__type !== null
}

// Usage
const productMetrics = await getAvailableMetrics('product')
console.log(`Product supports ${productMetrics.length} metrics`)

const hasGroupBy = await supportsGroupBy('product')
console.log(`Product supports groupBy: ${hasGroupBy}`)

GraphQL Playground/IDE

Most GraphQL development tools provide built-in schema exploration:

  1. GraphQL Playground: Docs tab shows all types and fields
  2. GraphiQL: Documentation Explorer panel
  3. Apollo Studio: Schema tab with search and filtering

These tools let you browse the schema interactively and see:

  • All available collections
  • Metrics for each collection
  • GroupBy dimensions for collections that support it
  • Parameters and their types
  • Descriptions and usage notes

Summary

The Finale GraphQL API's aggregation system provides powerful data analysis capabilities:

Key Takeaways

  1. Universal Availability: All 24 collections support metrics aggregation
  2. Selective GroupBy: 9 collections support dimensional grouping
  3. Parallel Arrays: GroupBy and metrics arrays align by index position
  4. Flexible Operators: Use sum, mean, min, max to aggregate data
  5. Filter First: Apply ViewConnection filters to reduce aggregation scope
  6. Error Handling: Always check errorCode before using results
  7. Performance Matters: Use filters, caching, and minimal pagination
  8. Discoverable: Use introspection to explore available metrics and dimensions

Common Use Cases

  • Dashboards: Real-time KPIs and performance metrics
  • Reports: Summary reports with dimensional breakdowns
  • Analytics: Trend analysis and pattern discovery
  • Inventory: Stock levels and reorder analysis
  • Sales: Revenue tracking and customer analysis
  • Financial: Invoice and payment summaries

Next Steps

  1. Review collection-specific documentation for complete metric and dimension lists
  2. Experiment with simple queries (count without grouping)
  3. Add groupBy for dimensional analysis
  4. Build complex analytics with multiple metrics
  5. Implement error handling and caching
  6. Optimize performance with filters and minimal pagination

The aggregation system is one of the most powerful features of the Finale GraphQL API, enabling sophisticated data analysis without complex client-side processing.