tango-app-api-audio-analytics 1.0.0

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.
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Cohort Service
3
+ * Handles all OpenSearch operations for cohort data
4
+ */
5
+
6
+ import { insertWithId, searchOpenSearchData, updateOpenSearchData } from 'tango-app-api-middleware';
7
+ import { logger } from 'tango-app-api-middleware';
8
+ import { v4 as uuidv4 } from 'uuid';
9
+
10
+ const OPENSEARCH_INDEX = 'tango-audio-cohort';
11
+
12
+ /**
13
+ * Create a new cohort in OpenSearch
14
+ * @param {Object} cohortData - Cohort data to insert
15
+ * @return {Promise<Object>} - Created cohort with ID
16
+ */
17
+ export const createCohort = async ( cohortData ) => {
18
+ try {
19
+ logger.info( `Creating cohort: ${cohortData.cohortId}` );
20
+
21
+ // Generate unique document ID
22
+ const documentId = uuidv4();
23
+
24
+ // Prepare cohort document
25
+ const cohortDocument = {
26
+ ...cohortData,
27
+ documentId,
28
+ createdAt: new Date().toISOString(),
29
+ updatedAt: new Date().toISOString(),
30
+ isActive: true,
31
+ version: '1.0',
32
+ };
33
+
34
+ // Insert into OpenSearch using the insert function with id
35
+ const result = await insertWithId(
36
+ OPENSEARCH_INDEX,
37
+ documentId,
38
+ cohortDocument,
39
+ );
40
+
41
+ logger.info( `Cohort created successfully: ${cohortData.cohortId}`, {
42
+ documentId,
43
+ cohortId: cohortData.cohortId,
44
+ result,
45
+ } );
46
+
47
+ return {
48
+ success: true,
49
+ data: {
50
+ documentId,
51
+ ...cohortDocument,
52
+ },
53
+ message: 'Cohort created successfully',
54
+ };
55
+ } catch ( error ) {
56
+ logger.error( `Error creating cohort: ${error.message}`, error );
57
+ return {
58
+ code: 'COHORT_CREATE_ERROR',
59
+ message: 'Failed to create cohort',
60
+ details: error.message,
61
+ };
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Get cohort by ID
67
+ * @param {string} cohortId - Cohort ID to retrieve
68
+ * @return {Promise<Object>} - Cohort data
69
+ */
70
+ export const getCohortById = async ( cohortId ) => {
71
+ try {
72
+ logger.info( `Fetching cohort: ${cohortId}` );
73
+
74
+ const query = {
75
+ query: {
76
+ match: {
77
+ cohortId: cohortId,
78
+ },
79
+ },
80
+ };
81
+
82
+ const result = await searchOpenSearchData( OPENSEARCH_INDEX, query );
83
+
84
+ if ( !result || result.hits.hits.length === 0 ) {
85
+ return new Error( `Cohort not found: ${cohortId}` );
86
+ }
87
+
88
+ const cohort = result.hits.hits[0]._source;
89
+ cohort.documentId = result.hits.hits[0]._id;
90
+
91
+ return cohort;
92
+ } catch ( error ) {
93
+ logger.error( `Error fetching cohort: ${error.message}`, error );
94
+ return {
95
+ code: 'COHORT_FETCH_ERROR',
96
+ message: 'Failed to fetch cohort',
97
+ details: error.message,
98
+ };
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Get cohorts by client ID
104
+ * @param {string} clientId - Client ID to filter by
105
+ * @param {number} limit - Results limit
106
+ * @param {number} offset - Pagination offset
107
+ * @return {Promise<Object>} - Cohorts and pagination
108
+ */
109
+ export const getCohortsByClientId = async ( clientId, limit = 10, offset = 0 ) => {
110
+ try {
111
+ logger.info( `Fetching cohorts for client: ${clientId}` );
112
+
113
+ const query = {
114
+ query: {
115
+ match: {
116
+ clientId: clientId,
117
+ },
118
+ },
119
+ size: limit,
120
+ from: offset,
121
+ sort: [ { createdAt: { order: 'desc' } } ],
122
+ };
123
+
124
+ const result = await search( OPENSEARCH_INDEX, query );
125
+
126
+ const cohorts = result.hits.hits.map( ( hit ) => ( {
127
+ documentId: hit._id,
128
+ ...hit._source,
129
+ } ) );
130
+
131
+ return {
132
+ cohorts,
133
+ pagination: {
134
+ total: result.hits.total.value,
135
+ limit,
136
+ offset,
137
+ hasMore: offset + limit < result.hits.total.value,
138
+ },
139
+ };
140
+ } catch ( error ) {
141
+ logger.error( `Error fetching cohorts by client: ${error.message}`, error );
142
+ return {
143
+ code: 'COHORT_SEARCH_ERROR',
144
+ message: 'Failed to fetch cohorts',
145
+ details: error.message,
146
+ };
147
+ }
148
+ };
149
+
150
+ /**
151
+ * Update cohort
152
+ * @param {string} documentId - Document ID in OpenSearch
153
+ * @param {Object} updateData - Data to update
154
+ * @return {Promise<Object>} - Updated cohort
155
+ */
156
+ export const updateCohort = async ( documentId, updateData ) => {
157
+ try {
158
+ logger.info( `Updating cohort document: ${documentId}` );
159
+
160
+ const updateDocument = {
161
+ ...updateData,
162
+ updatedAt: new Date().toISOString(),
163
+ };
164
+
165
+ const result = await updateOpenSearchData( OPENSEARCH_INDEX, documentId, updateDocument );
166
+
167
+ logger.info( `Cohort updated successfully: ${documentId}`, result );
168
+
169
+ return {
170
+ success: true,
171
+ data: {
172
+ documentId,
173
+ ...updateDocument,
174
+ },
175
+ message: 'Cohort updated successfully',
176
+ };
177
+ } catch ( error ) {
178
+ logger.error( `Error updating cohort: ${error.message}`, error );
179
+ return {
180
+ code: 'COHORT_UPDATE_ERROR',
181
+ message: 'Failed to update cohort',
182
+ details: error.message,
183
+ };
184
+ }
185
+ };
186
+
187
+ /**
188
+ * Delete cohort
189
+ * @param {string} documentId - Document ID in OpenSearch
190
+ * @return {Promise<Object>} - Deletion result
191
+ */
192
+ export const deleteCohort = async ( documentId ) => {
193
+ try {
194
+ logger.info( `Deleting cohort document: ${documentId}` );
195
+
196
+ // const result = await deleteDoc( OPENSEARCH_INDEX, documentId );
197
+
198
+ logger.info( `Cohort deleted successfully: ${documentId}` );
199
+
200
+ return {
201
+ success: true,
202
+ message: 'Cohort deleted successfully',
203
+ };
204
+ } catch ( error ) {
205
+ logger.error( `Error deleting cohort: ${error.message}`, error );
206
+ return {
207
+ code: 'COHORT_DELETE_ERROR',
208
+ message: 'Failed to delete cohort',
209
+ details: error.message,
210
+ };
211
+ }
212
+ };
213
+
214
+ /**
215
+ * Search cohorts by criteria
216
+ * @param {Object} searchCriteria - Search criteria (clientId, cohortName, etc.)
217
+ * @param {number} limit - Results limit
218
+ * @param {number} offset - Pagination offset
219
+ * @return {Promise<Object>} - Search results
220
+ */
221
+ export const searchCohorts = async ( searchCriteria, limit = 10, offset = 0 ) => {
222
+ try {
223
+ logger.info( `Searching cohorts with criteria:`, searchCriteria );
224
+
225
+ const mustClauses = [];
226
+
227
+ if ( searchCriteria.clientId ) {
228
+ mustClauses.push( { match: { clientId: searchCriteria.clientId } } );
229
+ }
230
+ if ( searchCriteria.cohortId ) {
231
+ mustClauses.push( { match: { cohortId: searchCriteria.cohortId } } );
232
+ }
233
+ if ( searchCriteria.cohortName ) {
234
+ mustClauses.push( {
235
+ match: {
236
+ cohortName: {
237
+ query: searchCriteria.cohortName,
238
+ fuzziness: 'AUTO',
239
+ },
240
+ },
241
+ } );
242
+ }
243
+
244
+ const query = {
245
+ query: {
246
+ bool: {
247
+ must: mustClauses.length > 0 ? mustClauses : [ { match_all: {} } ],
248
+ },
249
+ },
250
+ size: limit,
251
+ from: offset,
252
+ sort: [ { createdAt: { order: 'desc' } } ],
253
+ };
254
+
255
+ const result = await search( OPENSEARCH_INDEX, query );
256
+
257
+ const cohorts = result.hits.hits.map( ( hit ) => ( {
258
+ documentId: hit._id,
259
+ ...hit._source,
260
+ } ) );
261
+
262
+ return {
263
+ cohorts,
264
+ pagination: {
265
+ total: result.hits.total.value,
266
+ limit,
267
+ offset,
268
+ hasMore: offset + limit < result.hits.total.value,
269
+ },
270
+ };
271
+ } catch ( error ) {
272
+ logger.error( `Error searching cohorts: ${error.message}`, error );
273
+ return {
274
+ code: 'COHORT_SEARCH_ERROR',
275
+ message: 'Failed to search cohorts',
276
+ details: error.message,
277
+ };
278
+ }
279
+ };
280
+
281
+ /**
282
+ * Get cohort analytics/metrics summary
283
+ * @param {string} cohortId - Cohort ID
284
+ * @return {Promise<Object>} - Cohort with metrics summary
285
+ */
286
+ export const getCohortAnalytics = async ( cohortId ) => {
287
+ try {
288
+ logger.info( `Fetching analytics for cohort: ${cohortId}` );
289
+
290
+ const cohort = await getCohortById( cohortId );
291
+
292
+ // Build metrics summary
293
+ const metricsSummary = cohort.metrics.map( ( metric ) => ( {
294
+ metricId: metric.metricId,
295
+ metricName: metric.metricName,
296
+ isNumneric: metric.isNumneric,
297
+ contextCount: metric.contexts ? metric.contexts.length : 0,
298
+ contexts: metric.contexts || [],
299
+ } ) );
300
+
301
+ return {
302
+ cohortId: cohort.cohortId,
303
+ cohortName: cohort.cohortName,
304
+ clientId: cohort.clientId,
305
+ totalMetrics: cohort.metrics.length,
306
+ metrics: metricsSummary,
307
+ createdAt: cohort.createdAt,
308
+ updatedAt: cohort.updatedAt,
309
+ };
310
+ } catch ( error ) {
311
+ logger.error( `Error fetching cohort analytics: ${error.message}`, error );
312
+ return {
313
+ code: 'COHORT_ANALYTICS_ERROR',
314
+ message: 'Failed to fetch cohort analytics',
315
+ details: error.message,
316
+ };
317
+ }
318
+ };
319
+
320
+ /**
321
+ * Check if cohort exists
322
+ * @param {string} cohortId - Cohort ID to check
323
+ * @return {Promise<boolean>} - True if exists, false otherwise
324
+ */
325
+ export const cohortExists = async ( cohortId ) => {
326
+ try {
327
+ await getCohortById( cohortId );
328
+ return true;
329
+ } catch ( error ) {
330
+ return false;
331
+ }
332
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Cohort Validation Schema using Joi
3
+ * Validates cohort creation requests according to the schema
4
+ */
5
+
6
+ import Joi from 'joi';
7
+
8
+ /**
9
+ * Context validation schema (for metrics)
10
+ */
11
+ const contextSchema = Joi.object( {
12
+ contextName: Joi.string().required().description( 'Name of the context' ),
13
+ priority: Joi.number().min( 0 ).max( 2 ).required().description( 'Priority level 0-2' ),
14
+ description: Joi.string().required().description( 'Description of the context' ),
15
+ minValue: Joi.number().optional().description( 'Minimum value for numeric contexts' ),
16
+ maxValue: Joi.number().optional().description( 'Maximum value for numeric contexts' ),
17
+ } );
18
+
19
+ /**
20
+ * Metric validation schema
21
+ */
22
+ const metricSchema = Joi.object( {
23
+ metricId: Joi.string().required().description( 'Unique metric identifier' ),
24
+ metricName: Joi.string().required().description( 'Human-readable metric name' ),
25
+ metricDescription: Joi.string().required().description( 'Detailed metric description' ),
26
+ hasContext: Joi.boolean().required().description( 'Whether metric has context values' ),
27
+ isNumneric: Joi.boolean().required().description( 'Whether metric is numeric' ),
28
+ contexts: Joi.when( 'hasContext', {
29
+ is: true,
30
+ then: Joi.array().items( contextSchema ).required().min( 1 ),
31
+ otherwise: Joi.forbidden(),
32
+ } ).description( 'Array of context options for this metric' ),
33
+ } );
34
+
35
+ /**
36
+ * Main Cohort Creation Schema
37
+ */
38
+ export const cohortCreationSchema = Joi.object( {
39
+ clientId: Joi.string().required().description( 'Client identifier' ),
40
+ cohortId: Joi.string().required().pattern( /^[a-z0-9_-]+$/ ).description( 'Unique cohort identifier' ),
41
+ cohortName: Joi.string().required().min( 3 ).max( 100 ).description( 'Name of the cohort' ),
42
+ cohortDescription: Joi.string().required().min( 10 ).max( 500 ).description( 'Detailed cohort description' ),
43
+ metrics: Joi.array()
44
+ .items( metricSchema )
45
+ .required()
46
+ .min( 1 )
47
+ .max( 50 )
48
+ .description( 'Array of metrics defining the cohort' ),
49
+ } ).strict();
50
+
51
+ /**
52
+ * Cohort Update Schema (optional fields)
53
+ */
54
+ export const cohortUpdateSchema = Joi.object( {
55
+ cohortName: Joi.string().min( 3 ).max( 100 ).optional(),
56
+ cohortDescription: Joi.string().min( 10 ).max( 500 ).optional(),
57
+ metrics: Joi.array().items( metricSchema ).min( 1 ).max( 50 ).optional(),
58
+ } ).strict();
59
+
60
+ /**
61
+ * Cohort Query Schema
62
+ */
63
+ export const cohortQuerySchema = Joi.object( {
64
+ cohortId: Joi.string().optional(),
65
+ clientId: Joi.string().optional(),
66
+ cohortName: Joi.string().optional(),
67
+ limit: Joi.number().integer().min( 1 ).max( 100 ).default( 10 ),
68
+ offset: Joi.number().integer().min( 0 ).default( 0 ),
69
+ } ).strict();
70
+
71
+ /**
72
+ * Validate cohort data using Joi
73
+ * @param {Object} data - Data to validate
74
+ * @param {Joi.Schema} schema - Joi schema to validate against
75
+ * @return {Promise<{error: null, value: Object} | {error: Error, value: null}>}
76
+ */
77
+ export const validateCohortData = async ( data, schema ) => {
78
+ try {
79
+ const { error, value } = schema.validate( data, {
80
+ abortEarly: false,
81
+ stripUnknown: true,
82
+ convert: true,
83
+ } );
84
+
85
+ if ( error ) {
86
+ const details = error.details.map( ( detail ) => ( {
87
+ field: detail.path.join( '.' ),
88
+ message: detail.message,
89
+ type: detail.type,
90
+ } ) );
91
+ return {
92
+ error: {
93
+ message: 'Validation failed',
94
+ details,
95
+ },
96
+ value: null,
97
+ };
98
+ }
99
+
100
+ return {
101
+ error: null,
102
+ value,
103
+ };
104
+ } catch ( err ) {
105
+ return {
106
+ error: {
107
+ message: 'Validation error',
108
+ details: [ { message: err.message } ],
109
+ },
110
+ value: null,
111
+ };
112
+ }
113
+ };