Skip to content

Commit 1c1dd82

Browse files
ibrahim-abuznaidcursoragentkishanprmr
authored
fix(pieces/salesforce): fix Run Report returning empty rows and unusa… (activepieces#11226)
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Kishan Parmar <code.kishanparmar@gmail.com>
1 parent ce3fa13 commit 1c1dd82

2 files changed

Lines changed: 209 additions & 39 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"name": "@activepieces/piece-salesforce",
3-
"version": "0.3.4"
3+
"version": "0.4.0"
44
}

packages/pieces/community/salesforce/src/lib/action/run-report.ts

Lines changed: 208 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,214 @@ import { salesforceAuth } from '../..';
44
import { callSalesforceApi, salesforcesCommon } from '../common';
55

66
export const runReport = createAction({
7-
auth: salesforceAuth,
8-
name: 'run_report',
9-
displayName: 'Run Report',
10-
description: 'Execute a Salesforce analytics report.',
11-
props: {
12-
report_id: salesforcesCommon.report,
13-
filters: Property.Json({
14-
displayName: 'Filters',
15-
description: 'Apply dynamic filters to the report run.',
16-
required: false,
17-
defaultValue: [
18-
{
19-
"column": "ACCOUNT.NAME",
20-
"operator": "equals",
21-
"value": "Acme"
22-
}
23-
]
24-
})
25-
},
26-
async run(context) {
27-
const { report_id, filters } = context.propsValue;
28-
29-
let body = undefined;
30-
if (filters && Array.isArray(filters) && filters.length > 0) {
31-
body = {
32-
reportMetadata: {
33-
reportFilters: filters,
34-
},
35-
};
7+
auth: salesforceAuth,
8+
name: 'run_report',
9+
displayName: 'Run Report',
10+
description:
11+
'Execute a Salesforce analytics report and return the results as easy-to-use rows.',
12+
props: {
13+
report_id: salesforcesCommon.report,
14+
filters: Property.Json({
15+
displayName: 'Filters',
16+
description:
17+
"Apply dynamic filters to the report run. Leave empty to use the report's saved filters.",
18+
required: false,
19+
defaultValue: [],
20+
}),
21+
},
22+
async run(context) {
23+
const { report_id, filters } = context.propsValue;
24+
25+
let body = undefined;
26+
if (filters && Array.isArray(filters) && filters.length > 0) {
27+
body = {
28+
reportMetadata: {
29+
reportFilters: filters,
30+
},
31+
};
32+
}
33+
34+
const queryParam = '?includeDetails=true';
35+
36+
const response = await callSalesforceApi<SalesforceReportResponse>(
37+
HttpMethod.POST,
38+
context.auth,
39+
`/services/data/v56.0/analytics/reports/${report_id}${queryParam}`,
40+
body
41+
);
42+
43+
const reportData = response.body;
44+
45+
return transformReportToRows(reportData);
46+
},
47+
});
48+
49+
interface SalesforceReportResponse {
50+
attributes: {
51+
reportId: string;
52+
reportName: string;
53+
};
54+
reportMetadata: {
55+
detailColumns: string[];
56+
name: string;
57+
reportFormat: string;
58+
aggregates: string[];
59+
groupingsDown: {
60+
name: string;
61+
sortOrder: string;
62+
dateGranularity: string;
63+
column: string;
64+
}[];
65+
};
66+
reportExtendedMetadata: {
67+
detailColumnInfo: Record<string, { label: string; dataType: string }>;
68+
groupingColumnInfo: Record<string, { label: string; dataType: string }>;
69+
aggregateColumnInfo: Record<string, { label: string; dataType: string }>;
70+
};
71+
factMap: Record<
72+
string,
73+
{
74+
rows: { dataCells: { label: string; value: unknown }[] }[];
75+
aggregates: { label: string; value: unknown }[];
76+
}
77+
>;
78+
groupingsDown: {
79+
groupings: { key: string; label: string; value: unknown }[];
80+
};
81+
}
82+
83+
function transformReportToRows(report: SalesforceReportResponse): {
84+
reportName: string;
85+
reportId: string;
86+
totalRows: number;
87+
columns: string[];
88+
rows: Record<string, unknown>[];
89+
} {
90+
const detailColumns = report.reportMetadata?.detailColumns ?? [];
91+
const detailColumnInfo =
92+
report.reportExtendedMetadata?.detailColumnInfo ?? {};
93+
const groupingsDown = report.reportMetadata?.groupingsDown ?? [];
94+
const groupingColumnInfo =
95+
report.reportExtendedMetadata?.groupingColumnInfo ?? {};
96+
const factMap = report.factMap ?? {};
97+
98+
// Build ordered list of column labels for detail columns
99+
const columnLabels = detailColumns.map(
100+
(col) => detailColumnInfo[col]?.label ?? col
101+
);
102+
103+
// Build grouping column labels
104+
const groupingLabels = groupingsDown.map(
105+
(g) => groupingColumnInfo[g.name]?.label ?? g.name
106+
);
107+
108+
const allRows: Record<string, unknown>[] = [];
109+
110+
// Collect grouping labels from groupingsDown for grouped/summary reports
111+
const groupingValues = extractGroupingValues(
112+
report.groupingsDown?.groupings ?? []
113+
);
114+
115+
// Iterate over all factMap entries to collect rows
116+
// Keys: "T!T" (tabular/grand total), "0!T", "1!T" (summary groups), "0!0", "1!0" (matrix), etc.
117+
for (const [factMapKey, factMapEntry] of Object.entries(factMap)) {
118+
if (!factMapEntry?.rows) continue;
119+
120+
// Determine grouping context from the factMap key
121+
const groupContext = resolveGroupingContext(
122+
factMapKey,
123+
groupingValues,
124+
groupingLabels
125+
);
126+
127+
for (const row of factMapEntry.rows) {
128+
const rowObj: Record<string, unknown> = {};
129+
130+
// Add grouping columns if present
131+
for (const [key, value] of Object.entries(groupContext)) {
132+
rowObj[key] = value;
133+
}
134+
135+
// Map each data cell to its column label
136+
if (row.dataCells) {
137+
for (let i = 0; i < row.dataCells.length; i++) {
138+
const label = columnLabels[i] ?? `Column_${i}`;
139+
const cell = row.dataCells[i];
140+
let value = cell.label ?? cell.value;
141+
if (value === '-' || value === '--') {
142+
value = '';
143+
}
144+
rowObj[label] = value;
36145
}
146+
}
147+
148+
allRows.push(rowObj);
149+
}
150+
}
151+
152+
return {
153+
reportName:
154+
report.attributes?.reportName ??
155+
report.reportMetadata?.name ??
156+
'Unknown Report',
157+
reportId: report.attributes.reportId ?? 'Unknown Report',
158+
totalRows: allRows.length,
159+
columns: [...groupingLabels, ...columnLabels],
160+
rows: allRows,
161+
};
162+
}
163+
164+
function extractGroupingValues(
165+
groupings: {
166+
key: string;
167+
label: string;
168+
value: unknown;
169+
groupings?: { key: string; label: string; value: unknown }[];
170+
}[],
171+
depth = 0,
172+
result: Record<string, { label: string; depth: number }> = {}
173+
): Record<string, { label: string; depth: number }> {
174+
for (const grouping of groupings) {
175+
result[grouping.key] = { label: grouping.label, depth };
176+
if (grouping.groupings && grouping.groupings.length > 0) {
177+
extractGroupingValues(grouping.groupings, depth + 1, result);
178+
}
179+
}
180+
return result;
181+
}
182+
183+
function resolveGroupingContext(
184+
factMapKey: string,
185+
groupingValues: Record<string, { label: string; depth: number }>,
186+
groupingLabels: string[]
187+
): Record<string, string> {
188+
const context: Record<string, string> = {};
189+
190+
// factMap keys are like "0!T", "0_1!T", "T!T", etc.
191+
// The part before "!" represents row groupings, after "!" represents column groupings
192+
const [rowPart] = factMapKey.split('!');
193+
194+
if (rowPart === 'T' || !rowPart) {
195+
return context; // Grand total or no grouping
196+
}
197+
198+
// Row grouping keys can be like "0", "0_1" (nested groupings)
199+
const rowKeys = rowPart.split('_');
200+
201+
let currentKey = '';
202+
203+
for (let depth = 0; depth < rowKeys.length; depth++) {
204+
currentKey =
205+
depth === 0 ? rowKeys[depth] : `${currentKey}_${rowKeys[depth]}`;
206+
207+
const groupInfo = groupingValues[currentKey];
208+
209+
if (!groupInfo) continue;
210+
211+
const columnLabel = groupingLabels[depth] ?? `Group_${depth}`;
37212

38-
const response = await callSalesforceApi(
39-
HttpMethod.POST,
40-
context.auth,
41-
`/services/data/v56.0/analytics/reports/${report_id}`,
42-
body
43-
);
213+
context[columnLabel] = groupInfo.label;
214+
}
44215

45-
return response.body;
46-
},
47-
});
216+
return context;
217+
}

0 commit comments

Comments
 (0)