# 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

```typescript
list.catalog.fn.groupBy(options)
```

## Parameters

- `attribute` (`string`, required) — The attribute to group items by
- `aggregates` (`AggregationOptions[]`, 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
- `items` (`DcuplSectionItemOption[]`, 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 / count` (`number`, optional) — 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
- `projection` (`Projection<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:

```typescript
// 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

- [Functions API reference: global dcupl.fn.\* form and errors](/sdk/api/functions)

## Examples

**Product catalog by category**

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

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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(', '));
});
```

**SDK**

```typescript
// 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: '*' }]
});
```
