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 UIscatalog.fn.aggregate()— Compute aggregations across all itemscatalog.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: '*' }]
});[]