@@ -4,44 +4,214 @@ import { salesforceAuth } from '../..';
44import { callSalesforceApi , salesforcesCommon } from '../common' ;
55
66export 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