.md

catalog.fn.groupBy()

Group items by an attribute value and compute aggregations for each group. Perfect for creating categorized lists, calculating group statistics, and building hierarchical data displays.

Syntax

list.catalog.fn.groupBy(options)

Parameters

attributestringRequired
The attribute to group items by
aggregatesAggregationOptions[]Optional
Aggregations to compute per group: any AggregationOptions, as in aggregate() (including distinct and group). A dotted attribute such as customer.rating resolves through the reference
itemsDcuplSectionItemOption[]Optional
Which groups include their items: { key: "*" } for all, or one entry per group key (compared with the raw value). Each entry takes start, count, sort and projection for that group's items
start / countnumberOptional
Page through the groups
sort{ attributes: string[], order: ("ASC" | "DESC")[] }Optional
Sort the items inside each group. When grouping by a reference, it also sorts the groups by the referenced model's attributes; for a plain attribute, the groups follow the order of their first item under this sort. Only properties can be sort attributes (references are ignored). Order values must be uppercase
projectionProjection<T>Optional
Projection for the group entries when grouping by a reference (for the items inside a group, set projection on the items entry). Without $: true, _meta is dropped from the groups; _aggregates and _items stay when you request them. Ignored when grouping by a plain property

Return type

Returns a GroupByResponse object:

// Can be imported from @dcupl/common
type GroupByResponse = {
  _meta: {
    currentSize: number;  // Number of groups (before start/count)
    initialSize: number;  // Number of groups (before start/count)
  };
  items: SectionItem[];   // Array of groups
}

type SectionItem = {
  key: string | number | boolean | null; // Group identifier: the raw attribute value
                                         // (declared as string in @dcupl/common)
  value?: any;            // The attribute value (when grouping by a reference:
                          // the referenced item's properties instead)
  _meta: { size: number }; // Number of items in this group
  _aggregates: Aggregation[]; // One per entry of options.aggregates, same order
  _items: ListItem[];     // Items of the group ([] unless requested via items)
}

Usage notes

  • Groups respect current query filters - group counts update as filters change
  • Use aggregates to compute statistics like sum, avg, min, max per group
  • Items are only included if explicitly requested via the items option
  • Supports deep queries for grouping by related model attributes (e.g., "category.parent")
  • Performance: Only request items when needed - use { key: "specific-key" } instead of { key: "*" }
  • Performance: Limit items per group with count parameter to avoid loading thousands of items
  • Performance: Use projection to return only needed properties for items
  • Items whose value for the grouping attribute is undefined are not part of any group (null values form a group with the key null), and a group without items never appears. When grouping by a reference, a key that does not resolve still forms its own group
  • There are no exclude* options: SDK 2.0 removed excludeZeros, excludeUndefineds and excludeUnresolved from DcuplGroupByOptions (groupBy() never read them), so passing one is a type error
  • Troubleshooting: If _items stays empty for a specific key, check the type: items[].key is compared with the raw value, so "5" does not match a numeric 5

Related methods

  • catalog.fn.facets() — Get unique values with counts for building filter UIs
  • catalog.fn.aggregate() — Compute aggregations across all items
  • catalog.fn.pivot() — Create multi-dimensional pivot tables

Related guides/docs

Examples

Product catalog by category

Group products by category with multiple aggregations and include actual items with pagination.

const productList = dcupl.lists.create({ modelKey: 'Product' });

const grouped = productList.catalog.fn.groupBy({
  attribute: 'category',
  aggregates: [
    { attribute: 'price', types: ['avg', 'min', 'max'] },
    { attribute: 'rating', types: ['avg'] },
  ],
  items: [{ key: '*', count: 5 }], // Include first 5 items per group
});

grouped.items.forEach((group) => {
  console.log(group.key + ':');
  console.log('  Products:', group._meta.size);
  console.log('  Avg Price:', group._aggregates[0].avg);
  console.log('  Price Range:', group._aggregates[0].min, '-', group._aggregates[0].max);
  console.log('  Avg Rating:', group._aggregates[1].avg);
  console.log('  Sample Products:', group._items.map(p => p.name).join(', '));
});
Sales by region with sorting

Group orders by region, calculate revenue metrics, and sort by aggregated values.

const orderList = dcupl.lists.create({ modelKey: 'Order' });

// Filter to this year
orderList.catalog.query.addCondition({
  operator: 'eq',
  attribute: 'year',
  value: 2024,
});

const grouped = orderList.catalog.fn.groupBy({
  attribute: 'region',
  aggregates: [
    { attribute: 'totalAmount', types: ['sum', 'avg', 'count'] },
    { attribute: 'customer', types: ['distinct'] },
  ],
});

// Sort by revenue (sum) descending
const sorted = grouped.items.sort(
  (a, b) => b._aggregates[0].sum! - a._aggregates[0].sum!
);

sorted.forEach((group) => {
  console.log(group.key + ':');
  console.log('  Revenue:', group._aggregates[0].sum);
  console.log('  Orders:', group._aggregates[0].count);
  console.log('  Avg Order:', group._aggregates[0].avg);
  console.log('  Unique Customers:', group._aggregates[1].distinct!.length);
});
Tasks by project with selective items

Group tasks by project and load items only for the projects you name in items.

const taskList = dcupl.lists.create({ modelKey: 'Task' });

const grouped = taskList.catalog.fn.groupBy({
  attribute: 'project',
  aggregates: [
    { attribute: 'estimatedHours', types: ['sum'] },
    { attribute: 'status', types: ['distinct'] },
  ],
  // Items are loaded only for these groups; each entry takes start, count, sort, projection
  items: [
    { key: 'Project Alpha', count: 10 },
    { key: 'Project Beta', count: 10 },
  ],
});

grouped.items.forEach((group) => {
  const statusBreakdown = group._aggregates[1].distinct
    ?.map(s => s.value + ': ' + s.count)
    .join(', ');

  console.log(group.key + ':');
  console.log('  Tasks:', group._meta.size);
  console.log('  Total Hours:', group._aggregates[0].sum);
  console.log('  Status:', statusBreakdown);

  if (group._items?.length > 0) {
    console.log('  First Tasks:', group._items.map(t => t.name).join(', '));
  }
});
Content by tag (multi-valued)

Group articles by tags (multi-valued attribute) with sorting and item inclusion.

const articleList = dcupl.lists.create({ modelKey: 'Article' });

const grouped = articleList.catalog.fn.groupBy({
  attribute: 'tags', // Multi-valued attribute
  aggregates: [
    { attribute: 'views', types: ['sum', 'avg'] },
  ],
  items: [{
    key: '*',
    count: 3,
    sort: { attributes: ['views'], order: ['DESC'] },
  }],
});

// Sort groups by total views
const sorted = grouped.items.sort(
  (a, b) => b._aggregates[0].sum! - a._aggregates[0].sum!
);

sorted.slice(0, 10).forEach((group) => {
  console.log('#' + group.key + ':');
  console.log('  Articles:', group._meta.size);
  console.log('  Total Views:', group._aggregates[0].sum);
  console.log('  Avg Views:', group._aggregates[0].avg);
  console.log('  Top Articles:', group._items.map(a => a.title).join(', '));
});
Orders by customer segment (deep property)

Group orders by customer segment using deep property access with dot notation.

const orderList = dcupl.lists.create({ modelKey: 'Order' });

const grouped = orderList.catalog.fn.groupBy({
  attribute: 'customer.segment', // Deep property
  aggregates: [
    { attribute: 'totalAmount', types: ['sum', 'avg'] },
    { attribute: 'customer', types: ['distinct'] },
  ],
});

grouped.items.forEach((group) => {
  const uniqueCustomers = group._aggregates[1].distinct!.length;
  const revenuePerCustomer = group._aggregates[0].sum! / uniqueCustomers;

  console.log('Segment: ' + group.key);
  console.log('  Revenue:', group._aggregates[0].sum);
  console.log('  Avg Order:', group._aggregates[0].avg);
  console.log('  Customers:', uniqueCustomers);
  console.log('  Revenue/Customer:', revenuePerCustomer.toFixed(2));
});
Campaigns by channel with KPIs

Group marketing campaigns by channel and calculate conversion metrics from aggregations.

const campaignList = dcupl.lists.create({ modelKey: 'Campaign' });

const grouped = campaignList.catalog.fn.groupBy({
  attribute: 'channel',
  aggregates: [
    { attribute: 'impressions', types: ['sum'] },
    { attribute: 'clicks', types: ['sum'] },
    { attribute: 'conversions', types: ['sum'] },
    { attribute: 'spend', types: ['sum'] },
  ],
});

grouped.items.forEach((group) => {
  const impressions = group._aggregates[0].sum!;
  const clicks = group._aggregates[1].sum!;
  const conversions = group._aggregates[2].sum!;
  const spend = group._aggregates[3].sum!;

  const ctr = (clicks / impressions) * 100;
  const conversionRate = (conversions / clicks) * 100;
  const cpc = spend / clicks;

  console.log(group.key + ':');
  console.log('  Campaigns:', group._meta.size);
  console.log('  CTR:', ctr.toFixed(2) + '%');
  console.log('  Conversion Rate:', conversionRate.toFixed(2) + '%');
  console.log('  CPC:', '$' + cpc.toFixed(2));
});
Feedback by rating with sample items

Group feedback by a numeric rating and show sample comments for low ratings. items[].key must equal the raw value, so a string key like "1" does not match a numeric rating; use "*" with a count instead.

const feedbackList = dcupl.lists.create({ modelKey: 'Feedback' });

const grouped = feedbackList.catalog.fn.groupBy({
  attribute: 'rating',
  items: [{ key: '*', count: 5 }], // Up to 5 items per rating
});

// Sort by rating value
const sorted = grouped.items.sort(
  (a, b) => Number(a.key) - Number(b.key)
);

sorted.forEach((group) => {
  console.log(group.key + ' Star:');
  console.log('  Count:', group._meta.size);

  if (Number(group.key) <= 2) {
    console.log('  Sample Comments:');
    group._items.forEach(item => {
      console.log('    -', item.comment.substring(0, 50) + '...');
    });
  }
});
Products low on stock by category

Pre-filter to low-stock products, then group by category to identify inventory issues.

const productList = dcupl.lists.create({ modelKey: 'Product' });

// Filter to low stock items
productList.catalog.query.addCondition({
  operator: 'lte',
  attribute: 'stock',
  value: 10,
});

const grouped = productList.catalog.fn.groupBy({
  attribute: 'category',
  aggregates: [
    { attribute: 'stock', types: ['sum'] },
    { attribute: 'price', types: ['sum'] }, // Value at risk
  ],
  items: [{ key: '*', count: 3 }],
});

console.log('Low Stock Alert:');
grouped.items.forEach((group) => {
  const valueAtRisk = group._aggregates[1].sum!;

  console.log(group.key + ':');
  console.log('  Products:', group._meta.size);
  console.log('  Total Stock:', group._aggregates[0].sum);
  console.log('  Value at Risk:', '$' + valueAtRisk.toFixed(2));
  console.log('  Items:', group._items.map(p => p.name + ' (' + p.stock + ')').join(', '));
});
// Group articles by category
const grouped = list.catalog.fn.groupBy({
  attribute: 'category'
});

// With aggregations per group
const groupedWithStats = list.catalog.fn.groupBy({
  attribute: 'category',
  aggregates: [
    { attribute: 'price', types: ['sum', 'avg'] }
  ]
});

// Include items in each group
const withItems = list.catalog.fn.groupBy({
  attribute: 'category',
  items: [{ key: '*' }]
});