tango-app-api-audio-analytics 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +0 -1
- package/package.json +3 -1
- package/src/controllers/audioAnalytics.controller.js +76 -0
- package/src/controllers/cohortAnalytics.controller.js +61 -0
- package/src/controllers/conversationAnalytics.controller.js +138 -13
- package/src/dtos/audioAnalytics.dtos.js +74 -0
- package/src/routes/audioAnalytics.routes.js +15 -1
- package/src/services/conversation.service.js +243 -0
- package/src/utils/csvExport.util.js +160 -0
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tango-app-api-audio-analytics",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "audioAnalytics",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
"author": "praveenraj",
|
|
14
14
|
"license": "ISC",
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"@aws-sdk/client-secrets-manager": "^3.1003.0",
|
|
16
17
|
"aws-sdk": "^2.1693.0",
|
|
17
18
|
"body-parser": "^2.2.2",
|
|
18
19
|
"cors": "^2.8.6",
|
|
19
20
|
"dotenv": "^17.3.1",
|
|
20
21
|
"express": "^5.2.1",
|
|
22
|
+
"json2csv": "^6.0.0-alpha.2",
|
|
21
23
|
"mongodb": "^6.21.0",
|
|
22
24
|
"nodemon": "^3.1.14",
|
|
23
25
|
"tango-api-schema": "^2.5.62",
|
|
@@ -224,3 +224,79 @@ export const callExternalStreamAPI = async ( req, res ) => {
|
|
|
224
224
|
} );
|
|
225
225
|
}
|
|
226
226
|
};
|
|
227
|
+
|
|
228
|
+
// ======================= CHAT STREAM API =======================
|
|
229
|
+
|
|
230
|
+
const CHAT_STREAM_API = 'http://13.232.38.210:8000/api/chat/stream';
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Chat Stream API - Streams response from external AI chat API
|
|
234
|
+
* Accepts user data and configuration, returns streaming response
|
|
235
|
+
* @param {Object} req - Express request object
|
|
236
|
+
* @param {Object} res - Express response object
|
|
237
|
+
*/
|
|
238
|
+
export async function chatStream( req, res ) {
|
|
239
|
+
try {
|
|
240
|
+
/* eslint-disable camelcase */
|
|
241
|
+
const {
|
|
242
|
+
userId,
|
|
243
|
+
sessionId,
|
|
244
|
+
sessionDate,
|
|
245
|
+
sessionTimezone,
|
|
246
|
+
message,
|
|
247
|
+
config,
|
|
248
|
+
} = req.body;
|
|
249
|
+
|
|
250
|
+
logger.info( { message: 'Received chat stream request', userId, sessionId, message } );
|
|
251
|
+
|
|
252
|
+
// Build payload for external API
|
|
253
|
+
const payload = {
|
|
254
|
+
userId,
|
|
255
|
+
sessionId,
|
|
256
|
+
sessionDate,
|
|
257
|
+
sessionTimezone,
|
|
258
|
+
message,
|
|
259
|
+
config,
|
|
260
|
+
};
|
|
261
|
+
/* eslint-enable camelcase */
|
|
262
|
+
|
|
263
|
+
logger.info( { message: 'Calling external chat stream API', payload } );
|
|
264
|
+
|
|
265
|
+
// Set SSE headers
|
|
266
|
+
res.setHeader( 'Content-Type', 'text/event-stream' );
|
|
267
|
+
res.setHeader( 'Cache-Control', 'no-cache' );
|
|
268
|
+
res.setHeader( 'Connection', 'keep-alive' );
|
|
269
|
+
res.setHeader( 'X-Accel-Buffering', 'no' );
|
|
270
|
+
|
|
271
|
+
// Make the external API call with streaming
|
|
272
|
+
const response = await fetch( CHAT_STREAM_API, {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: {
|
|
275
|
+
'Content-Type': 'application/json',
|
|
276
|
+
'Accept': 'text/event-stream',
|
|
277
|
+
},
|
|
278
|
+
body: JSON.stringify( payload ),
|
|
279
|
+
} );
|
|
280
|
+
|
|
281
|
+
if ( !response.ok ) {
|
|
282
|
+
logger.error( `External Chat Stream API error: ${response.status} ${response.statusText}` );
|
|
283
|
+
res.write( `data: ${JSON.stringify( { error: `External API error: ${response.statusText}` } )}\n\n` );
|
|
284
|
+
res.end();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Stream the response from external API to client
|
|
289
|
+
for await ( const chunk of response.body ) {
|
|
290
|
+
const text = new TextDecoder().decode( chunk );
|
|
291
|
+
res.write( text );
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
res.end();
|
|
295
|
+
logger.info( { message: 'Chat stream completed successfully' } );
|
|
296
|
+
} catch ( error ) {
|
|
297
|
+
logger.error( { error: error, function: 'chatStream' } );
|
|
298
|
+
res.write( `data: ${JSON.stringify( { error: error.message } )}\n\n` );
|
|
299
|
+
res.end();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
// import { sampleCohortData } from '../models/cohortAnalysis.model.js';
|
|
2
2
|
import { logger } from 'tango-app-api-middleware';
|
|
3
|
+
import { getCohortAnalysisSummaryCard } from '../services/conversation.service.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get Cohort Analysis Summary Card
|
|
7
|
+
* POST /cohort-analysis-card
|
|
8
|
+
* @param {Object} req - Express request object
|
|
9
|
+
* @param {Object} req.body - Request body with analysis parameters
|
|
10
|
+
* @param {string} req.body.startDate - Start date for analysis (YYYY-MM-DD)
|
|
11
|
+
* @param {string} req.body.endDate - End date for analysis (YYYY-MM-DD)
|
|
12
|
+
* @param {string[]} req.body.storeId - Array of store IDs for filtering
|
|
13
|
+
* @param {string[]} req.body.cohortType - Array of cohort types to filter
|
|
14
|
+
* @param {string[]} req.body.clientId - Array of client IDs
|
|
15
|
+
* @param {Object} res - Express response object
|
|
16
|
+
* @return {void} Returns JSON response with cohort analysis summary card data
|
|
17
|
+
*/
|
|
18
|
+
export const getCohortAnalysisCard = async ( req, res ) => {
|
|
19
|
+
try {
|
|
20
|
+
const { startDate, endDate, storeId, cohortType, clientId } = req.body;
|
|
21
|
+
|
|
22
|
+
// Validation
|
|
23
|
+
if ( !startDate || !endDate || !storeId || !cohortType || !clientId ) {
|
|
24
|
+
return res.status( 400 ).json( {
|
|
25
|
+
status: 'error',
|
|
26
|
+
message: 'Missing required parameters: startDate, endDate, storeId, cohortType, clientId',
|
|
27
|
+
code: 'MISSING_PARAMETERS',
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
} );
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logger.info( {
|
|
33
|
+
message: 'Fetching cohort analysis summary card',
|
|
34
|
+
startDate,
|
|
35
|
+
endDate,
|
|
36
|
+
storeId,
|
|
37
|
+
cohortType,
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
// Get summary card data from Lambda
|
|
41
|
+
const summaryCard = await getCohortAnalysisSummaryCard( {
|
|
42
|
+
startDate,
|
|
43
|
+
endDate,
|
|
44
|
+
storeId,
|
|
45
|
+
cohortType,
|
|
46
|
+
clientId,
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
return res.status( 200 ).json( {
|
|
50
|
+
status: 'success',
|
|
51
|
+
data: summaryCard,
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
} );
|
|
54
|
+
} catch ( error ) {
|
|
55
|
+
logger.error( { error, message: 'Error fetching cohort analysis summary card', body: req.body } );
|
|
56
|
+
return res.status( 500 ).json( {
|
|
57
|
+
status: 'error',
|
|
58
|
+
message: error.message || 'Internal server error',
|
|
59
|
+
code: 'INTERNAL_ERROR',
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
} );
|
|
62
|
+
}
|
|
63
|
+
};
|
|
3
64
|
|
|
4
65
|
/**
|
|
5
66
|
* Get Complete Cohort Analysis
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// import { sampleConversationData, sampleConversationsList } from '../models/conversationAnalysis.model.js';
|
|
2
2
|
import { logger } from 'tango-app-api-middleware';
|
|
3
|
+
import {
|
|
4
|
+
getConversationsListFromLambda,
|
|
5
|
+
exportConversationsFromLambda,
|
|
6
|
+
getConversationDetailsFromLambda,
|
|
7
|
+
exportConversationsToCSV,
|
|
8
|
+
filterConversationsBySearch,
|
|
9
|
+
sortConversations,
|
|
10
|
+
} from '../services/conversation.service.js';
|
|
3
11
|
|
|
4
12
|
/**
|
|
5
13
|
* Get Conversation Analysis List
|
|
@@ -8,13 +16,31 @@ import { logger } from 'tango-app-api-middleware';
|
|
|
8
16
|
* @param {Object} req.body - Request body with query parameters
|
|
9
17
|
* @param {string} req.body.startDate - Start date for filtering
|
|
10
18
|
* @param {string} req.body.endDate - End date for filtering
|
|
11
|
-
* @param {string} req.body.storeId -
|
|
19
|
+
* @param {string[]} req.body.storeId - Array of store IDs for filtering
|
|
20
|
+
* @param {string[]} req.body.clientId - Optional array of client IDs
|
|
21
|
+
* @param {boolean} req.body.isAI - Filter for AI conversations
|
|
22
|
+
* @param {string} req.body.analyticsType - Type of analytics (audio, text, all)
|
|
23
|
+
* @param {string} req.body.searchValue - Search term for filtering
|
|
24
|
+
* @param {number} req.body.limit - Pagination limit
|
|
25
|
+
* @param {number} req.body.offset - Pagination offset
|
|
26
|
+
* @param {boolean} req.body.isExport - Flag to export as CSV
|
|
12
27
|
* @param {Object} res - Express response object
|
|
13
|
-
* @return {void} Returns JSON response with conversations list
|
|
28
|
+
* @return {void} Returns JSON response with conversations list or CSV file
|
|
14
29
|
*/
|
|
15
30
|
export const getConversationsList = async ( req, res ) => {
|
|
16
31
|
try {
|
|
17
|
-
const {
|
|
32
|
+
const {
|
|
33
|
+
startDate,
|
|
34
|
+
endDate,
|
|
35
|
+
storeId,
|
|
36
|
+
clientId,
|
|
37
|
+
isAI,
|
|
38
|
+
analyticsType,
|
|
39
|
+
searchValue,
|
|
40
|
+
limit,
|
|
41
|
+
offset,
|
|
42
|
+
isExport,
|
|
43
|
+
} = req.body;
|
|
18
44
|
|
|
19
45
|
// Validation
|
|
20
46
|
if ( !startDate || !endDate || !storeId ) {
|
|
@@ -26,21 +52,101 @@ export const getConversationsList = async ( req, res ) => {
|
|
|
26
52
|
} );
|
|
27
53
|
}
|
|
28
54
|
|
|
29
|
-
logger.info(
|
|
55
|
+
logger.info( {
|
|
56
|
+
message: 'Fetching conversations list',
|
|
57
|
+
startDate,
|
|
58
|
+
endDate,
|
|
59
|
+
storeId,
|
|
60
|
+
isExport,
|
|
61
|
+
} );
|
|
62
|
+
|
|
63
|
+
let conversations;
|
|
64
|
+
let totalCount;
|
|
65
|
+
|
|
66
|
+
// If export is requested, call the export Lambda function
|
|
67
|
+
if ( isExport ) {
|
|
68
|
+
try {
|
|
69
|
+
const exportResponse = await exportConversationsFromLambda( {
|
|
70
|
+
startDate,
|
|
71
|
+
endDate,
|
|
72
|
+
storeId,
|
|
73
|
+
clientId,
|
|
74
|
+
isAI,
|
|
75
|
+
analyticsType,
|
|
76
|
+
searchValue,
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
conversations = exportResponse.conversations || [];
|
|
80
|
+
totalCount = exportResponse.totalCount || conversations.length;
|
|
81
|
+
|
|
82
|
+
logger.info( {
|
|
83
|
+
message: 'Export Lambda response received',
|
|
84
|
+
recordCount: conversations.length,
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
// Apply search filter if provided
|
|
88
|
+
if ( searchValue ) {
|
|
89
|
+
conversations = filterConversationsBySearch( conversations, searchValue );
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Apply sorting
|
|
93
|
+
conversations = sortConversations( conversations, 'date', 'desc' );
|
|
94
|
+
|
|
95
|
+
// Convert to CSV
|
|
96
|
+
const csv = await exportConversationsToCSV( conversations );
|
|
97
|
+
res.setHeader( 'Content-Type', 'text/csv' );
|
|
98
|
+
res.setHeader( 'Content-Disposition', 'attachment; filename=conversations.csv' );
|
|
99
|
+
return res.send( csv );
|
|
100
|
+
} catch ( error ) {
|
|
101
|
+
logger.error( { error, message: 'Error in export Lambda call', body: req.body } );
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Call Lambda to get conversations (non-export)
|
|
107
|
+
const lambdaResponse = await getConversationsListFromLambda( {
|
|
108
|
+
startDate,
|
|
109
|
+
endDate,
|
|
110
|
+
storeId,
|
|
111
|
+
clientId,
|
|
112
|
+
isAI,
|
|
113
|
+
analyticsType,
|
|
114
|
+
searchValue,
|
|
115
|
+
limit,
|
|
116
|
+
offset,
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
conversations = lambdaResponse.conversations || [];
|
|
120
|
+
totalCount = lambdaResponse.totalCount || conversations.length;
|
|
121
|
+
|
|
122
|
+
// Apply search filter if provided
|
|
123
|
+
if ( searchValue ) {
|
|
124
|
+
conversations = filterConversationsBySearch( conversations, searchValue );
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Apply sorting
|
|
128
|
+
conversations = sortConversations( conversations, 'date', 'desc' );
|
|
30
129
|
|
|
130
|
+
// Return paginated JSON response
|
|
31
131
|
return res.status( 200 ).json( {
|
|
32
132
|
status: 'success',
|
|
33
133
|
data: {
|
|
34
|
-
totalConversations:
|
|
35
|
-
conversations
|
|
134
|
+
totalConversations: totalCount,
|
|
135
|
+
conversations,
|
|
136
|
+
pagination: {
|
|
137
|
+
limit,
|
|
138
|
+
offset,
|
|
139
|
+
total: totalCount,
|
|
140
|
+
hasMore: ( offset + limit ) < totalCount,
|
|
141
|
+
},
|
|
36
142
|
},
|
|
37
143
|
timestamp: new Date().toISOString(),
|
|
38
144
|
} );
|
|
39
145
|
} catch ( error ) {
|
|
40
|
-
logger.error(
|
|
146
|
+
logger.error( { error, message: 'Error fetching conversations list', body: req.body } );
|
|
41
147
|
return res.status( 500 ).json( {
|
|
42
148
|
status: 'error',
|
|
43
|
-
message: 'Internal server error',
|
|
149
|
+
message: error.message || 'Internal server error',
|
|
44
150
|
code: 'INTERNAL_ERROR',
|
|
45
151
|
timestamp: new Date().toISOString(),
|
|
46
152
|
} );
|
|
@@ -66,24 +172,43 @@ export const getConversationDetails = async ( req, res ) => {
|
|
|
66
172
|
if ( !conversationId || !storeId ) {
|
|
67
173
|
return res.status( 400 ).json( {
|
|
68
174
|
status: 'error',
|
|
69
|
-
message: 'Missing required parameters',
|
|
175
|
+
message: 'Missing required parameters: conversationId, storeId',
|
|
70
176
|
code: 'MISSING_PARAMETERS',
|
|
71
177
|
timestamp: new Date().toISOString(),
|
|
72
178
|
} );
|
|
73
179
|
}
|
|
74
180
|
|
|
75
|
-
logger.info(
|
|
181
|
+
logger.info( {
|
|
182
|
+
message: 'Fetching conversation details',
|
|
183
|
+
conversationId,
|
|
184
|
+
storeId,
|
|
185
|
+
} );
|
|
186
|
+
|
|
187
|
+
// Call Lambda to get conversation details
|
|
188
|
+
const conversationData = await getConversationDetailsFromLambda( {
|
|
189
|
+
conversationId,
|
|
190
|
+
storeId,
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
if ( !conversationData ) {
|
|
194
|
+
return res.status( 404 ).json( {
|
|
195
|
+
status: 'error',
|
|
196
|
+
message: 'Conversation not found',
|
|
197
|
+
code: 'NOT_FOUND',
|
|
198
|
+
timestamp: new Date().toISOString(),
|
|
199
|
+
} );
|
|
200
|
+
}
|
|
76
201
|
|
|
77
202
|
return res.status( 200 ).json( {
|
|
78
203
|
status: 'success',
|
|
79
|
-
data:
|
|
204
|
+
data: conversationData,
|
|
80
205
|
timestamp: new Date().toISOString(),
|
|
81
206
|
} );
|
|
82
207
|
} catch ( error ) {
|
|
83
|
-
logger.error(
|
|
208
|
+
logger.error( { error, message: 'Error fetching conversation details', params: req.params } );
|
|
84
209
|
return res.status( 500 ).json( {
|
|
85
210
|
status: 'error',
|
|
86
|
-
message: 'Internal server error',
|
|
211
|
+
message: error.message || 'Internal server error',
|
|
87
212
|
code: 'INTERNAL_ERROR',
|
|
88
213
|
timestamp: new Date().toISOString(),
|
|
89
214
|
} );
|
|
@@ -497,6 +497,53 @@ export class ExternalStreamAPIRequestDTO {
|
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
499
|
|
|
500
|
+
// ======================= JOI VALIDATION SCHEMAS =======================
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Cohort Analysis Card Schema
|
|
504
|
+
*/
|
|
505
|
+
export const cohortAnalysisCardValid = joi.object( {
|
|
506
|
+
startDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Start date in YYYY-MM-DD format' ),
|
|
507
|
+
endDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'End date in YYYY-MM-DD format' ),
|
|
508
|
+
storeId: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of store IDs' ),
|
|
509
|
+
cohortType: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of cohort types' ),
|
|
510
|
+
clientId: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of client IDs' ),
|
|
511
|
+
} ).strict();
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Conversations List Schema
|
|
515
|
+
*/
|
|
516
|
+
export const conversationsListValid = joi.object( {
|
|
517
|
+
startDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Start date in YYYY-MM-DD format' ),
|
|
518
|
+
endDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'End date in YYYY-MM-DD format' ),
|
|
519
|
+
storeId: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of store IDs' ),
|
|
520
|
+
clientId: joi.array().items( joi.string() ).optional().description( 'Array of client IDs' ),
|
|
521
|
+
isAI: joi.boolean().optional().description( 'Filter for AI-generated conversations' ),
|
|
522
|
+
analyticsType: joi.string().optional().valid( 'audio', 'text', 'all' ).description( 'Type of analytics' ),
|
|
523
|
+
searchValue: joi.string().optional().description( 'Search term' ),
|
|
524
|
+
isExport: joi.boolean().optional().description( 'Flag to export as CSV' ),
|
|
525
|
+
limit: joi.number().integer().min( 1 ).max( 1000 ).optional().default( 10 ).description( 'Pagination limit' ),
|
|
526
|
+
offset: joi.number().integer().min( 0 ).optional().default( 0 ).description( 'Pagination offset' ),
|
|
527
|
+
} ).strict();
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Conversation Details Schema
|
|
531
|
+
*/
|
|
532
|
+
export const conversationDetailsValid = joi.object( {
|
|
533
|
+
storeId: joi.string().required().description( 'Store ID' ),
|
|
534
|
+
} ).strict();
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* CSV Export Schema
|
|
538
|
+
*/
|
|
539
|
+
export const csvExportValid = joi.object( {
|
|
540
|
+
startDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Start date in YYYY-MM-DD format' ),
|
|
541
|
+
endDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'End date in YYYY-MM-DD format' ),
|
|
542
|
+
storeId: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of store IDs' ),
|
|
543
|
+
clientId: joi.array().items( joi.string() ).optional().description( 'Array of client IDs' ),
|
|
544
|
+
columns: joi.array().items( joi.string() ).optional().description( 'Columns to include in export' ),
|
|
545
|
+
} ).strict();
|
|
546
|
+
|
|
500
547
|
|
|
501
548
|
const cohortContextNonNumericSchema = joi.object( {
|
|
502
549
|
contextName: joi.string().required(),
|
|
@@ -586,3 +633,30 @@ const listCohortsByClientSchema = joi.object( {
|
|
|
586
633
|
export const listCohortsByClientValid = {
|
|
587
634
|
query: listCohortsByClientSchema,
|
|
588
635
|
};
|
|
636
|
+
|
|
637
|
+
// ======================= CHAT STREAM API SCHEMA =======================
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Chat Stream Request Schema
|
|
641
|
+
* For streaming AI chat API
|
|
642
|
+
*/
|
|
643
|
+
const chatStreamSchema = joi.object( {
|
|
644
|
+
|
|
645
|
+
userId: joi.string().required().description( 'User ID' ),
|
|
646
|
+
sessionId: joi.string().required().description( 'Session ID' ),
|
|
647
|
+
sessionDate: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Session date in YYYY-MM-DD format' ),
|
|
648
|
+
sessionTimezone: joi.string().required().description( 'Session timezone (e.g., Asia/Kolkata)' ),
|
|
649
|
+
message: joi.string().required().min( 1 ).description( 'User message/prompt' ),
|
|
650
|
+
config: joi.object( {
|
|
651
|
+
selectedStores: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of selected store IDs' ),
|
|
652
|
+
dateRange: joi.object( {
|
|
653
|
+
start: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'Start date in YYYY-MM-DD format' ),
|
|
654
|
+
end: joi.string().required().pattern( /^\d{4}-\d{2}-\d{2}$/ ).description( 'End date in YYYY-MM-DD format' ),
|
|
655
|
+
} ).required().description( 'Date range object' ),
|
|
656
|
+
selectedProducts: joi.array().items( joi.string() ).required().min( 1 ).description( 'Array of selected products' ),
|
|
657
|
+
} ).required().description( 'Configuration object' ),
|
|
658
|
+
} ).strict();
|
|
659
|
+
|
|
660
|
+
export const chatStreamValid = {
|
|
661
|
+
body: chatStreamSchema,
|
|
662
|
+
};
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
import express from 'express';
|
|
3
3
|
import { validate } from 'tango-app-api-middleware';
|
|
4
4
|
import { createCohortValid, updateCohortValid, getCohortValid, listCohortsByClientValid } from '../dtos/audioAnalytics.dtos.js';
|
|
5
|
-
import {
|
|
5
|
+
import { cohortAnalysisCardValid, conversationsListValid, conversationDetailsValid, chatStreamValid } from '../dtos/audioAnalytics.dtos.js';
|
|
6
|
+
import { createCohort, updateCohort, getCohort, listCohortsByClient, chatStream } from '../controllers/audioAnalytics.controller.js';
|
|
7
|
+
import { getCohortAnalysisCard } from '../controllers/cohortAnalytics.controller.js';
|
|
8
|
+
import { getConversationsList, getConversationDetails } from '../controllers/conversationAnalytics.controller.js';
|
|
6
9
|
|
|
7
10
|
export const audioAnalyticsrouter = express.Router(); ;
|
|
8
11
|
|
|
@@ -12,10 +15,21 @@ audioAnalyticsrouter.get( '/test', ( req, res ) => {
|
|
|
12
15
|
return res.json( 'Hello, world!' );
|
|
13
16
|
} );
|
|
14
17
|
|
|
18
|
+
// Cohort Management Routes
|
|
15
19
|
audioAnalyticsrouter.post( '/create-cohort', validate( createCohortValid ), createCohort );
|
|
16
20
|
audioAnalyticsrouter.post( '/update-cohort', validate( updateCohortValid ), updateCohort );
|
|
17
21
|
audioAnalyticsrouter.get( '/get-cohort/:cohortId', validate( getCohortValid ), getCohort );
|
|
18
22
|
audioAnalyticsrouter.get( '/list-cohorts', validate( listCohortsByClientValid ), listCohortsByClient );
|
|
19
23
|
|
|
24
|
+
// Cohort Analytics Routes
|
|
25
|
+
audioAnalyticsrouter.post( '/cohort-analysis-card', validate( cohortAnalysisCardValid ), getCohortAnalysisCard );
|
|
26
|
+
|
|
27
|
+
// Conversation Analytics Routes
|
|
28
|
+
audioAnalyticsrouter.post( '/conversations/list', validate( conversationsListValid ), getConversationsList );
|
|
29
|
+
audioAnalyticsrouter.post( '/conversations/:conversationId', validate( conversationDetailsValid ), getConversationDetails );
|
|
30
|
+
|
|
31
|
+
// Chat Stream API Route
|
|
32
|
+
audioAnalyticsrouter.post( '/chat/stream', validate( chatStreamValid ), chatStream );
|
|
33
|
+
|
|
20
34
|
|
|
21
35
|
export default audioAnalyticsrouter;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { logger } from 'tango-app-api-middleware';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { Parser } from 'json2csv';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Call the Lambda function to get cohort analysis
|
|
8
|
+
* @param {Object} params - Request parameters
|
|
9
|
+
* @param {string} params.startDate - Start date (YYYY-MM-DD)
|
|
10
|
+
* @param {string} params.endDate - End date (YYYY-MM-DD)
|
|
11
|
+
* @param {string[]} params.storeId - Array of store IDs
|
|
12
|
+
* @param {string[]} params.cohortType - Array of cohort types
|
|
13
|
+
* @param {string[]} params.clientId - Array of client IDs
|
|
14
|
+
* @return {Promise<Object>} Response from Lambda
|
|
15
|
+
*/
|
|
16
|
+
export async function getCohortAnalysisFromLambda( params ) {
|
|
17
|
+
try {
|
|
18
|
+
const LAMBDA_ENDPOINT = JSON.parse( process.env.URL ) || 'http://lambda-api:8000';
|
|
19
|
+
logger.info( { message: 'Calling Lambda for cohort analysis', params } );
|
|
20
|
+
|
|
21
|
+
const response = await axios.post( `${LAMBDA_ENDPOINT.cohortAnalysisCard}/cohort-analysis`, {
|
|
22
|
+
startDate: params.startDate,
|
|
23
|
+
endDate: params.endDate,
|
|
24
|
+
storeId: params.storeId,
|
|
25
|
+
cohortType: params.cohortType,
|
|
26
|
+
clientId: params.clientId,
|
|
27
|
+
}, {
|
|
28
|
+
timeout: 30000,
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
logger.info( { message: 'Lambda response received', data: response.data } );
|
|
32
|
+
return response.data;
|
|
33
|
+
} catch ( error ) {
|
|
34
|
+
logger.error( { error, message: 'Error calling Lambda for cohort analysis', params } );
|
|
35
|
+
throw new Error( `Failed to fetch cohort analysis: ${error.message}` );
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Call Lambda to get conversations list
|
|
41
|
+
* @param {Object} params - Request parameters
|
|
42
|
+
* @param {string} params.startDate - Start date (YYYY-MM-DD)
|
|
43
|
+
* @param {string} params.endDate - End date (YYYY-MM-DD)
|
|
44
|
+
* @param {string[]} params.storeId - Array of store IDs
|
|
45
|
+
* @param {string[]} params.clientId - Array of client IDs
|
|
46
|
+
* @param {boolean} params.isAI - Filter for AI conversations
|
|
47
|
+
* @param {string} params.analyticsType - Type of analytics
|
|
48
|
+
* @param {string} params.searchValue - Search term
|
|
49
|
+
* @param {number} params.limit - Pagination limit
|
|
50
|
+
* @param {number} params.offset - Pagination offset
|
|
51
|
+
* @return {Promise<Object>} Response from Lambda
|
|
52
|
+
*/
|
|
53
|
+
export async function getConversationsListFromLambda( params ) {
|
|
54
|
+
try {
|
|
55
|
+
const LAMBDA_ENDPOINT = JSON.parse( process.env.URL ) || 'http://lambda-api:8000';
|
|
56
|
+
logger.info( { message: 'Calling Lambda for conversations list', params } );
|
|
57
|
+
|
|
58
|
+
const response = await axios.post( `${LAMBDA_ENDPOINT.cohortConversationList}/conversations/list`, {
|
|
59
|
+
startDate: params.startDate,
|
|
60
|
+
endDate: params.endDate,
|
|
61
|
+
storeId: params.storeId,
|
|
62
|
+
clientId: params.clientId,
|
|
63
|
+
isAI: params.isAI,
|
|
64
|
+
analyticsType: params.analyticsType,
|
|
65
|
+
searchValue: params.searchValue,
|
|
66
|
+
limit: params.limit,
|
|
67
|
+
offset: params.offset,
|
|
68
|
+
}, {
|
|
69
|
+
timeout: 30000,
|
|
70
|
+
} );
|
|
71
|
+
|
|
72
|
+
logger.info( { message: 'Lambda response received for conversations list', totalCount: response.data?.totalCount } );
|
|
73
|
+
return response.data;
|
|
74
|
+
} catch ( error ) {
|
|
75
|
+
logger.error( { error, message: 'Error calling Lambda for conversations list', params } );
|
|
76
|
+
throw new Error( `Failed to fetch conversations list: ${error.message}` );
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Call Lambda to get conversations for export
|
|
82
|
+
* @param {Object} params - Request parameters
|
|
83
|
+
* @param {string} params.startDate - Start date (YYYY-MM-DD)
|
|
84
|
+
* @param {string} params.endDate - End date (YYYY-MM-DD)
|
|
85
|
+
* @param {string[]} params.storeId - Array of store IDs
|
|
86
|
+
* @param {string[]} params.clientId - Array of client IDs
|
|
87
|
+
* @param {boolean} params.isAI - Filter for AI conversations
|
|
88
|
+
* @param {string} params.analyticsType - Type of analytics
|
|
89
|
+
* @param {string} params.searchValue - Search term
|
|
90
|
+
* @return {Promise<Object>} Response from Lambda with export data
|
|
91
|
+
*/
|
|
92
|
+
export async function exportConversationsFromLambda( params ) {
|
|
93
|
+
try {
|
|
94
|
+
const LAMBDA_ENDPOINT = JSON.parse( process.env.URL ) || 'http://lambda-api:8000';
|
|
95
|
+
logger.info( { message: 'Calling Lambda for conversations export', params } );
|
|
96
|
+
|
|
97
|
+
const response = await axios.post( `${LAMBDA_ENDPOINT.cohortConversationExport}/conversations/export`, {
|
|
98
|
+
startDate: params.startDate,
|
|
99
|
+
endDate: params.endDate,
|
|
100
|
+
storeId: params.storeId,
|
|
101
|
+
clientId: params.clientId,
|
|
102
|
+
isAI: params.isAI,
|
|
103
|
+
analyticsType: params.analyticsType,
|
|
104
|
+
searchValue: params.searchValue,
|
|
105
|
+
}, {
|
|
106
|
+
timeout: 30000,
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
logger.info( { message: 'Lambda response received for conversations export', recordCount: response.data?.recordCount } );
|
|
110
|
+
return response.data;
|
|
111
|
+
} catch ( error ) {
|
|
112
|
+
logger.error( { error, message: 'Error calling Lambda for conversations export', params } );
|
|
113
|
+
throw new Error( `Failed to export conversations: ${error.message}` );
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Call Lambda to get conversation details
|
|
119
|
+
* @param {Object} params - Request parameters
|
|
120
|
+
* @param {string} params.conversationId - Conversation ID
|
|
121
|
+
* @param {string} params.storeId - Store ID
|
|
122
|
+
* @return {Promise<Object>} Response from Lambda
|
|
123
|
+
*/
|
|
124
|
+
export async function getConversationDetailsFromLambda( params ) {
|
|
125
|
+
try {
|
|
126
|
+
const LAMBDA_ENDPOINT = JSON.parse( process.env.URL ) || 'http://lambda-api:8000';
|
|
127
|
+
logger.info( { message: 'Calling Lambda for conversation details', params } );
|
|
128
|
+
|
|
129
|
+
const response = await axios.post( `${LAMBDA_ENDPOINT.cohortConversationDetail}/conversations/${params.conversationId}`, {
|
|
130
|
+
storeId: params.storeId,
|
|
131
|
+
}, {
|
|
132
|
+
timeout: 30000,
|
|
133
|
+
} );
|
|
134
|
+
|
|
135
|
+
logger.info( { message: 'Lambda response received for conversation details', conversationId: params.conversationId } );
|
|
136
|
+
return response.data;
|
|
137
|
+
} catch ( error ) {
|
|
138
|
+
logger.error( { error, message: 'Error calling Lambda for conversation details', params } );
|
|
139
|
+
throw new Error( `Failed to fetch conversation details: ${error.message}` );
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Export conversations list to CSV
|
|
145
|
+
* @param {Array} conversations - Array of conversation objects
|
|
146
|
+
* @param {Array} columns - Columns to include in export
|
|
147
|
+
* @return {Promise<string>} CSV string
|
|
148
|
+
*/
|
|
149
|
+
export async function exportConversationsToCSV( conversations, columns = null ) {
|
|
150
|
+
try {
|
|
151
|
+
if ( !conversations || conversations.length === 0 ) {
|
|
152
|
+
logger.warn( 'No conversations to export' );
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// If no columns specified, use all keys from first object
|
|
157
|
+
const fields = columns || Object.keys( conversations[0] );
|
|
158
|
+
|
|
159
|
+
const parser = new Parser( { fields } );
|
|
160
|
+
const csv = parser.parse( conversations );
|
|
161
|
+
|
|
162
|
+
logger.info( { message: 'Conversations exported to CSV', recordCount: conversations.length } );
|
|
163
|
+
return csv;
|
|
164
|
+
} catch ( error ) {
|
|
165
|
+
logger.error( { error, message: 'Error exporting conversations to CSV' } );
|
|
166
|
+
throw new Error( `Failed to export to CSV: ${error.message}` );
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get cohort analysis summary card data
|
|
172
|
+
* @param {Object} params - Request parameters
|
|
173
|
+
* @return {Promise<Object>} Summary card data from Lambda
|
|
174
|
+
*/
|
|
175
|
+
export async function getCohortAnalysisSummaryCard( params ) {
|
|
176
|
+
try {
|
|
177
|
+
const lambdaResponse = await getCohortAnalysisFromLambda( params );
|
|
178
|
+
|
|
179
|
+
// Process response to create summary card
|
|
180
|
+
const summaryCard = {
|
|
181
|
+
totalConversations: lambdaResponse.totalConversations || 0,
|
|
182
|
+
totalDuration: lambdaResponse.totalDuration || 0,
|
|
183
|
+
averageRating: lambdaResponse.averageRating || 0,
|
|
184
|
+
topCohorts: lambdaResponse.topCohorts || [],
|
|
185
|
+
trendData: lambdaResponse.trendData || [],
|
|
186
|
+
keyInsights: lambdaResponse.keyInsights || [],
|
|
187
|
+
timestamp: new Date().toISOString(),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return summaryCard;
|
|
191
|
+
} catch ( error ) {
|
|
192
|
+
logger.error( { error, message: 'Error getting cohort analysis summary card' } );
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Process and filter conversations based on search criteria
|
|
199
|
+
* @param {Array} conversations - Array of conversation objects
|
|
200
|
+
* @param {string} searchValue - Search term
|
|
201
|
+
* @return {Array} Filtered conversations
|
|
202
|
+
*/
|
|
203
|
+
export function filterConversationsBySearch( conversations, searchValue ) {
|
|
204
|
+
if ( !searchValue || searchValue.trim() === '' ) {
|
|
205
|
+
return conversations;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const searchLower = searchValue.toLowerCase();
|
|
209
|
+
return conversations.filter( ( conv ) => {
|
|
210
|
+
return (
|
|
211
|
+
( conv.storeName && conv.storeName.toLowerCase().includes( searchLower ) ) ||
|
|
212
|
+
( conv.storeId && conv.storeId.toLowerCase().includes( searchLower ) ) ||
|
|
213
|
+
( conv.summary && conv.summary.toLowerCase().includes( searchLower ) ) ||
|
|
214
|
+
( conv.audioDescription && conv.audioDescription.toLowerCase().includes( searchLower ) )
|
|
215
|
+
);
|
|
216
|
+
} );
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Sort conversations by specified field
|
|
221
|
+
* @param {Array} conversations - Array of conversation objects
|
|
222
|
+
* @param {string} sortBy - Field to sort by
|
|
223
|
+
* @param {string} order - Sort order ('asc' or 'desc')
|
|
224
|
+
* @return {Array} Sorted conversations
|
|
225
|
+
*/
|
|
226
|
+
export function sortConversations( conversations, sortBy = 'date', order = 'desc' ) {
|
|
227
|
+
const sorted = [ ...conversations ];
|
|
228
|
+
sorted.sort( ( a, b ) => {
|
|
229
|
+
const aVal = a[sortBy];
|
|
230
|
+
const bVal = b[sortBy];
|
|
231
|
+
|
|
232
|
+
if ( aVal === undefined || aVal === null ) return 1;
|
|
233
|
+
if ( bVal === undefined || bVal === null ) return -1;
|
|
234
|
+
|
|
235
|
+
if ( typeof aVal === 'string' ) {
|
|
236
|
+
return order === 'desc' ? bVal.localeCompare( aVal ) : aVal.localeCompare( bVal );
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return order === 'desc' ? bVal - aVal : aVal - bVal;
|
|
240
|
+
} );
|
|
241
|
+
|
|
242
|
+
return sorted;
|
|
243
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Parser } from 'json2csv';
|
|
2
|
+
import { logger } from 'tango-app-api-middleware';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert data array to CSV format
|
|
6
|
+
* @param {Array} data - Array of objects to convert to CSV
|
|
7
|
+
* @param {Array} fields - Fields to include in CSV (if null, uses all fields from first object)
|
|
8
|
+
* @param {Object} options - Additional options for Parser
|
|
9
|
+
* @return {Promise<string>} CSV string
|
|
10
|
+
*/
|
|
11
|
+
export async function convertToCSV( data, fields = null, options = {} ) {
|
|
12
|
+
try {
|
|
13
|
+
if ( !data || data.length === 0 ) {
|
|
14
|
+
logger.warn( 'No data provided for CSV export' );
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const parserOptions = {
|
|
19
|
+
fields: fields || Object.keys( data[0] ),
|
|
20
|
+
...options,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const parser = new Parser( parserOptions );
|
|
24
|
+
const csv = parser.parse( data );
|
|
25
|
+
|
|
26
|
+
logger.info( { message: 'Data converted to CSV', recordCount: data.length } );
|
|
27
|
+
return csv;
|
|
28
|
+
} catch ( error ) {
|
|
29
|
+
logger.error( { error, message: 'Error converting data to CSV' } );
|
|
30
|
+
throw new Error( `Failed to convert to CSV: ${error.message}` );
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Export conversations to CSV
|
|
36
|
+
* @param {Array} conversations - Array of conversation objects
|
|
37
|
+
* @param {Array} columns - Columns to include in export
|
|
38
|
+
* @return {Promise<string>} CSV string
|
|
39
|
+
*/
|
|
40
|
+
export async function exportConversationsToCSV( conversations, columns = null ) {
|
|
41
|
+
const defaultFields = columns || [
|
|
42
|
+
'conversationId',
|
|
43
|
+
'storeId',
|
|
44
|
+
'storeName',
|
|
45
|
+
'date',
|
|
46
|
+
'time',
|
|
47
|
+
'duration',
|
|
48
|
+
'audioDescription',
|
|
49
|
+
'summary',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
return convertToCSV( conversations, defaultFields, {
|
|
53
|
+
header: true,
|
|
54
|
+
flatten: false,
|
|
55
|
+
} );
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Export cohort analysis data to CSV
|
|
60
|
+
* @param {Array} data - Array of cohort analysis objects
|
|
61
|
+
* @param {Array} columns - Columns to include in export
|
|
62
|
+
* @return {Promise<string>} CSV string
|
|
63
|
+
*/
|
|
64
|
+
export async function exportCohortAnalysisToCSV( data, columns = null ) {
|
|
65
|
+
const defaultFields = columns || [
|
|
66
|
+
'cohortId',
|
|
67
|
+
'cohortName',
|
|
68
|
+
'cohortType',
|
|
69
|
+
'storeId',
|
|
70
|
+
'totalConversations',
|
|
71
|
+
'averageRating',
|
|
72
|
+
'conversionRate',
|
|
73
|
+
'date',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
return convertToCSV( data, defaultFields, {
|
|
77
|
+
header: true,
|
|
78
|
+
flatten: false,
|
|
79
|
+
} );
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Export dashboard metrics to CSV
|
|
84
|
+
* @param {Array} metrics - Array of metric objects
|
|
85
|
+
* @param {Array} columns - Columns to include in export
|
|
86
|
+
* @return {Promise<string>} CSV string
|
|
87
|
+
*/
|
|
88
|
+
export async function exportMetricsToCSV( metrics, columns = null ) {
|
|
89
|
+
const defaultFields = columns || [
|
|
90
|
+
'metricName',
|
|
91
|
+
'value',
|
|
92
|
+
'target',
|
|
93
|
+
'variance',
|
|
94
|
+
'status',
|
|
95
|
+
'date',
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
return convertToCSV( metrics, defaultFields, {
|
|
99
|
+
header: true,
|
|
100
|
+
flatten: false,
|
|
101
|
+
} );
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate CSV file blob
|
|
106
|
+
* @param {string} csvString - CSV string content
|
|
107
|
+
* @param {string} filename - Name of the file
|
|
108
|
+
* @return {Blob} File blob for download
|
|
109
|
+
*/
|
|
110
|
+
export function generateCSVBlob( csvString, filename = 'export.csv' ) {
|
|
111
|
+
const blob = new Blob( [ csvString ], { type: 'text/csv;charset=utf-8;' } );
|
|
112
|
+
const link = document.createElement( 'a' );
|
|
113
|
+
const url = URL.createObjectURL( blob );
|
|
114
|
+
|
|
115
|
+
link.setAttribute( 'href', url );
|
|
116
|
+
link.setAttribute( 'download', filename );
|
|
117
|
+
link.style.visibility = 'hidden';
|
|
118
|
+
|
|
119
|
+
document.body.appendChild( link );
|
|
120
|
+
link.click();
|
|
121
|
+
document.body.removeChild( link );
|
|
122
|
+
|
|
123
|
+
return blob;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Flatten nested objects for CSV export
|
|
128
|
+
* @param {Object} obj - Object to flatten
|
|
129
|
+
* @param {string} prefix - Prefix for flattened keys
|
|
130
|
+
* @return {Object} Flattened object
|
|
131
|
+
*/
|
|
132
|
+
export function flattenObject( obj, prefix = '' ) {
|
|
133
|
+
const flattened = {};
|
|
134
|
+
|
|
135
|
+
for ( const key in obj ) {
|
|
136
|
+
if ( Object.prototype.hasOwnProperty.call( obj, key ) ) {
|
|
137
|
+
const value = obj[key];
|
|
138
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
139
|
+
|
|
140
|
+
if ( value !== null && typeof value === 'object' && !Array.isArray( value ) ) {
|
|
141
|
+
Object.assign( flattened, flattenObject( value, newKey ) );
|
|
142
|
+
} else if ( Array.isArray( value ) ) {
|
|
143
|
+
flattened[newKey] = JSON.stringify( value );
|
|
144
|
+
} else {
|
|
145
|
+
flattened[newKey] = value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return flattened;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Flatten array of objects, handling nested data
|
|
155
|
+
* @param {Array} data - Array of objects to flatten
|
|
156
|
+
* @return {Array} Array of flattened objects
|
|
157
|
+
*/
|
|
158
|
+
export function flattenDataForCSV( data ) {
|
|
159
|
+
return data.map( ( item ) => flattenObject( item ) );
|
|
160
|
+
}
|