tango-app-api-trax 3.9.33 → 3.9.35

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.
Files changed (51) hide show
  1. package/index.js +2 -1
  2. package/package.json +1 -1
  3. package/src/controllers/internalTrax.controller.js +134 -90
  4. package/src/controllers/mobileTrax.controller.js +69 -29
  5. package/src/controllers/trax.controller.js +7 -1
  6. package/src/controllers/traxDashboard.controllers.js +63 -54
  7. package/src/hbs/flag.hbs +1 -1
  8. package/src/hbs/login-otp.hbs +943 -943
  9. package/src/hbs/template.hbs +7 -0
  10. package/src/hbs/visit-checklist.hbs +49 -87
  11. package/src/logging/activityLogFlusher.js +59 -0
  12. package/src/logging/activityLogMiddleware.js +45 -0
  13. package/src/logging/activityLogStore.js +91 -0
  14. package/src/logging/compressBatches.js +83 -0
  15. package/src/logging/config.js +24 -0
  16. package/src/logging/createLoggableService.js +46 -0
  17. package/src/logging/logExternalCall.js +37 -0
  18. package/src/services/app.service.js +15 -9
  19. package/src/services/approver.service.js +23 -15
  20. package/src/services/authentication.service.js +9 -3
  21. package/src/services/camera.service.js +19 -13
  22. package/src/services/checklist.service.js +35 -27
  23. package/src/services/checklistAssign.service.js +43 -38
  24. package/src/services/checklistQuestion.service.js +39 -34
  25. package/src/services/checklistlog.service.js +39 -34
  26. package/src/services/clientRequest.service.js +9 -2
  27. package/src/services/clients.services.js +23 -18
  28. package/src/services/cluster.service.js +31 -23
  29. package/src/services/domain.service.js +23 -18
  30. package/src/services/download.services.js +35 -25
  31. package/src/services/group.service.js +23 -17
  32. package/src/services/lenskartEmployeeMapping.service.js +15 -10
  33. package/src/services/locus.service.js +35 -28
  34. package/src/services/notification.service.js +35 -26
  35. package/src/services/otp.service.js +20 -13
  36. package/src/services/planogram.service.js +9 -2
  37. package/src/services/processedTaskConfig.service.js +35 -27
  38. package/src/services/processedTaskList.service.js +32 -26
  39. package/src/services/processedchecklist.services.js +55 -47
  40. package/src/services/processedchecklistconfig.services.js +39 -34
  41. package/src/services/recurringFlagTracker.service.js +39 -32
  42. package/src/services/runAIFeatures.services.js +32 -27
  43. package/src/services/runAIRequest.services.js +43 -38
  44. package/src/services/store.service.js +32 -27
  45. package/src/services/tagging.service.js +9 -2
  46. package/src/services/taskConfig.service.js +35 -27
  47. package/src/services/teams.service.js +35 -24
  48. package/src/services/ticket.service.js +15 -10
  49. package/src/services/user.service.js +27 -20
  50. package/src/services/userAssignedstores.service.js +12 -5
  51. package/src/utils/visitChecklistPdf.utils.js +192 -16
@@ -267,6 +267,13 @@
267
267
  <img src="{{this}}" alt="test" width="200" height="180">
268
268
  {{/each}}
269
269
  </div>
270
+ {{#if validationVideo.length}}
271
+ <div class="Reference"><span>Uploaded Video</span><br>
272
+ {{#each validationVideo}}
273
+ <a href="{{this}}" target="_blank" style="text-decoration: underline;color:#0085D2">{{this}}</a><br>
274
+ {{/each}}
275
+ </div>
276
+ {{/if}}
270
277
  {{/eq}}
271
278
  </td>
272
279
  </tr>
@@ -72,9 +72,21 @@
72
72
  .q-answer-media img,.q-answer-media video,.q-answer-item td img{display:block;width:200px;height:180px;object-fit:cover;border-radius:6px;margin-bottom:6px}
73
73
  .img-grid{display:flex;flex-wrap:wrap;gap:8px}
74
74
  .img-grid img{margin-bottom:0}
75
+ .answer-media-grid{display:flex;flex-wrap:wrap;gap:12px;margin-top:8px}
76
+ .answer-media-cell{width:calc(50% - 6px)}
77
+ .answer-media-cell .q-answer-caption{margin-bottom:4px}
78
+ .answer-media-cell img{display:block;width:100%;height:200px;object-fit:cover;border-radius:6px;margin-bottom:0}
75
79
  .q-answer-link{font-size:12px;color:#0085D2;text-decoration:underline;word-break:break-all}
76
80
  .q-answer-caption{font-size:11px;color:#666;margin-bottom:4px}
77
81
  .q-answer-remarks{font-size:11px;color:#666;margin-top:6px;white-space:pre-line}
82
+ /* User verification (Submitted By) */
83
+ .user-verify{margin-top:28px;padding-top:18px;border-top:1px solid #d9d9d9;break-inside:avoid}
84
+ .user-verify-title{font-size:15px;font-weight:700;color:#1a1a1a;margin-bottom:14px}
85
+ .user-verify-photo{display:block;width:200px;height:240px;object-fit:cover;border-radius:8px;margin-bottom:10px}
86
+ .user-verify-name{font-size:14px;color:#1a1a1a;font-weight:600}
87
+ .user-verify-sign-label{font-size:12px;color:#666;margin-top:16px;margin-bottom:6px;font-weight:600}
88
+ .user-verify-signature{display:block;width:220px;height:auto;max-height:110px;object-fit:contain;border:1px solid #eee;border-radius:6px;padding:6px;background:#fff}
89
+ .user-verify-signature-text{display:inline-block;min-width:180px;font-size:15px;font-weight:600;color:#1a1a1a;padding-bottom:6px;border-bottom:1px solid #d9d9d9;word-break:break-word}
78
90
  /* Footer */
79
91
  .page-footer{position:absolute;bottom:20px;left:40px;right:40px;display:flex;justify-content:space-between;align-items:center;font-size:11px;color:#999;border-top:1px solid #d9d9d9;padding-top:10px}
80
92
  .footer-brand{display:flex;align-items:center;gap:8px;font-weight:600;color:#0066CC;font-size:11px}
@@ -112,8 +124,12 @@
112
124
 
113
125
  <div class="cover-summary">
114
126
  <div class="cover-sum-row"><span class="cover-sum-label">No. of questions</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{numQuestions}}</span></div>
115
- <div class="cover-sum-row"><span class="cover-sum-label">No. of flags</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{numFlags}}</span></div>
116
- {{!-- <div class="cover-sum-row"><span class="cover-sum-label">AI Breached</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{aiBreached}}</span></div> --}}
127
+ {{#if showFlags}}
128
+ <div class="cover-sum-row"><span class="cover-sum-label">Question flags</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{numFlags}}</span></div>
129
+ {{/if}}
130
+ {{#if showRunAIFlag}}
131
+ <div class="cover-sum-row"><span class="cover-sum-label">Run AI flags</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{runAIFlag}}</span></div>
132
+ {{/if}}
117
133
  <div class="cover-sum-row"><span class="cover-sum-label">Submitted By</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{submittedBy}}</span></div>
118
134
  <div class="cover-sum-row"><span class="cover-sum-label">Country</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{country}}</span></div>
119
135
  </div>
@@ -227,93 +243,28 @@
227
243
  {{/if}}
228
244
  {{/eq}}
229
245
 
230
- <table style="width:100%;margin-top:8px;table-layout:fixed"><tr>
231
- <td style="width:50%;vertical-align:top;padding-right:8px">
232
- {{#neq ../answerType 'image/video'}}
233
- {{#neq ../answerType 'multipleImage'}}
234
- {{#if this.multiReferenceImage.length}}
235
- <div class="q-answer-media">
236
- <div class="q-answer-caption">Reference Images</div>
237
- {{#each this.multiReferenceImage}}
238
- <img src="{{this}}" alt="Reference Image" />
239
- {{/each}}
240
- </div>
241
- {{else}}
242
- {{#if this.referenceImage}}
243
- <div class="q-answer-media">
244
- <div class="q-answer-caption">Reference Image</div>
245
- <img src="{{this.referenceImage}}" alt="Reference Image" />
246
- </div>
247
- {{else}}
248
- {{#eq this.answerType 'image'}}
249
- {{#if this.answer}}
250
- <div class="q-answer-media">
251
- <div class="q-answer-caption">Uploaded Image</div>
252
- <img src="{{this.answer}}" alt="Uploaded Image" />
253
- </div>
254
- {{/if}}
255
- {{/eq}}
256
- {{/if}}
257
- {{/if}}
258
- {{else}}
259
- {{#eq this.answerType 'image'}}
260
- {{#if this.answer}}
261
- <div class="q-answer-media">
262
- <div class="q-answer-caption">Uploaded Image</div>
263
- <img src="{{this.answer}}" alt="Uploaded Image" />
264
- </div>
265
- {{/if}}
266
- {{/eq}}
267
- {{/neq}}
268
- {{else}}
269
- {{#eq this.answerType 'image'}}
270
- {{#if this.answer}}
271
- <div class="q-answer-media">
272
- <div class="q-answer-caption">Uploaded Image</div>
273
- <img src="{{this.answer}}" alt="Uploaded Image" />
274
- </div>
275
- {{/if}}
276
- {{/eq}}
277
- {{/neq}}
278
- </td>
279
- <td style="width:50%;vertical-align:top;padding-left:8px">
280
- {{#eq this.answerType 'image'}}
281
- {{#if this.answer}}
282
- {{#neq ../answerType 'image/video'}}
283
- {{#neq ../answerType 'multipleImage'}}
284
- {{#if this.multiReferenceImage.length}}
285
- <div class="q-answer-caption">Uploaded Image</div>
286
- <img src="{{this.answer}}" alt="Uploaded Image" />
287
- {{else}}
288
- {{#if this.referenceImage}}
289
- <div class="q-answer-caption">Uploaded Image</div>
290
- <img src="{{this.answer}}" alt="Uploaded Image" />
291
- {{/if}}
292
- {{/if}}
293
- {{/neq}}
294
- {{/neq}}
295
- {{/if}}
296
- {{/eq}}
297
- {{#if this.validation}}
298
- {{#eq this.validationDisplayType 'image'}}
299
- {{#if this.validationAnswer}}
300
- <div class="q-answer-caption">Validation Image</div>
301
- <img src="{{this.validationAnswer}}" alt="Validation Image" />
302
- {{/if}}
303
- {{/eq}}
304
- {{#eq this.validationDisplayType 'multiImage'}}
305
- {{#if this.validationImage.length}}
306
- <div class="q-answer-caption">Validation Image</div>
307
- {{#each this.validationImage}}
308
- <img src="{{this}}" alt="Validation Image" />
309
- {{/each}}
310
- {{/if}}
311
- {{/eq}}
312
- {{/if}}
313
- </td>
314
- </tr></table>
246
+ {{#eq this.validationDisplayType 'multiImage'}}
247
+ {{#if this.validationVideo.length}}
248
+ <div class="q-answer-media">
249
+ <div class="q-answer-caption">Validation Video</div>
250
+ {{#each this.validationVideo}}
251
+ <a class="q-answer-link" href="{{this}}" target="_blank">{{this}}</a>
252
+ {{/each}}
253
+ </div>
254
+ {{/if}}
255
+ {{/eq}}
315
256
  </div>
316
257
  {{/each}}
258
+ {{#if this.mediaItems.length}}
259
+ <div class="answer-media-grid">
260
+ {{#each this.mediaItems}}
261
+ <div class="answer-media-cell">
262
+ <div class="q-answer-caption">{{this.label}}</div>
263
+ <img src="{{this.url}}" alt="{{this.label}}" />
264
+ </div>
265
+ {{/each}}
266
+ </div>
267
+ {{/if}}
317
268
  </div>
318
269
  {{/if}}
319
270
  {{#if this.remarks}}
@@ -324,6 +275,17 @@
324
275
  {{/each}}
325
276
  </div>
326
277
  {{/each}}
278
+ {{#if showUserVerification}}
279
+ <div class="user-verify">
280
+ <div class="user-verify-title">Submitted By</div>
281
+ {{#if userImage}}
282
+ <img class="user-verify-photo" src="{{userImage}}" alt="Submitted by photo" />
283
+ {{/if}}
284
+ {{#if userSignature}}
285
+ <div class="user-verify-signature-text">{{userSignature}}</div>
286
+ {{/if}}
287
+ </div>
288
+ {{/if}}
327
289
  </div>
328
290
 
329
291
  </body>
@@ -0,0 +1,59 @@
1
+ import { insertOpenSearchData, logger } from 'tango-app-api-middleware';
2
+ import { loggingConfig } from './config.js';
3
+ import { compressBatches } from './compressBatches.js';
4
+
5
+ export async function flushActivityLog( ctx, status ) {
6
+ if ( !loggingConfig.enabled ) return;
7
+ if ( !ctx ) return;
8
+
9
+ if ( ctx.steps.length === 0 && !ctx.error ) return;
10
+
11
+ const compressedSteps = safeCompress( ctx.steps );
12
+
13
+ const doc = {
14
+ version: loggingConfig.version,
15
+ requestId: ctx.requestId,
16
+ timestamp: new Date( ctx.startTime ).toISOString(),
17
+ duration: Date.now() - ctx.startTime,
18
+ user: ctx.user,
19
+ api: {
20
+ method: ctx.api.method,
21
+ path: ctx.api.path,
22
+ action: ctx.api.action,
23
+ body: stringifyBody( ctx.api.body ),
24
+ },
25
+ response: {
26
+ code: ctx.response?.code ?? null,
27
+ body: stringifyBody( ctx.response?.body ),
28
+ },
29
+ status,
30
+ steps: compressedSteps,
31
+ error: ctx.error,
32
+ };
33
+
34
+ try {
35
+ await insertOpenSearchData( loggingConfig.index, doc );
36
+ } catch ( e ) {
37
+ logger.error( { functionName: 'flushActivityLog', error: e } );
38
+ }
39
+ }
40
+
41
+ function safeCompress( steps ) {
42
+ try {
43
+ return compressBatches( steps );
44
+ } catch ( _e ) {
45
+ // Never let compression failure block log delivery — fall back to raw steps.
46
+ return steps;
47
+ }
48
+ }
49
+
50
+ function stringifyBody( body ) {
51
+ if ( body === null || body === undefined ) return null;
52
+ if ( typeof body === 'string' ) return body;
53
+ if ( body instanceof Error ) return body.message || String( body );
54
+ try {
55
+ return JSON.stringify( body );
56
+ } catch ( _e ) {
57
+ return '[unserializable]';
58
+ }
59
+ }
@@ -0,0 +1,45 @@
1
+ import { activityLogStore, createLogContext } from './activityLogStore.js';
2
+ import { flushActivityLog } from './activityLogFlusher.js';
3
+ import { loggingConfig } from './config.js';
4
+
5
+ function refreshUser( ctx, req ) {
6
+ if ( req?.user ) {
7
+ ctx.user = {
8
+ _id: req.user._id,
9
+ userName: req.user.userName,
10
+ email: req.user.email,
11
+ };
12
+ }
13
+ }
14
+
15
+ export default function traxActivityLogMiddleware( req, res, next ) {
16
+ if ( !loggingConfig.enabled ) return next();
17
+
18
+ const WRITE_METHODS = [ 'POST', 'PUT', 'PATCH', 'DELETE' ];
19
+ if ( !WRITE_METHODS.includes( req.method ) ) return next();
20
+
21
+ const ctx = createLogContext( req );
22
+
23
+ activityLogStore.run( ctx, () => {
24
+ const originalSendSuccess = res.sendSuccess;
25
+ const originalSendError = res.sendError;
26
+
27
+ res.sendSuccess = ( data ) => {
28
+ refreshUser( ctx, req );
29
+ ctx.response.code = 200;
30
+ ctx.response.body = data;
31
+ originalSendSuccess( data );
32
+ setImmediate( () => flushActivityLog( ctx, 'success' ).catch( () => {} ) );
33
+ };
34
+
35
+ res.sendError = ( message, code ) => {
36
+ refreshUser( ctx, req );
37
+ ctx.response.code = code ?? 500;
38
+ ctx.response.body = message;
39
+ originalSendError( message, code );
40
+ setImmediate( () => flushActivityLog( ctx, 'failed' ).catch( () => {} ) );
41
+ };
42
+
43
+ next();
44
+ } );
45
+ }
@@ -0,0 +1,91 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ import { randomUUID } from 'crypto';
3
+
4
+ export const activityLogStore = new AsyncLocalStorage();
5
+
6
+ const SENSITIVE_KEYS = [ 'password', 'token', 'secret', 'authorization' ];
7
+
8
+ export function createLogContext( req ) {
9
+ return {
10
+ requestId: randomUUID(),
11
+ startTime: Date.now(),
12
+ user: req.user ? {
13
+ _id: req.user._id,
14
+ userName: req.user.userName,
15
+ email: req.user.email,
16
+ } : null,
17
+ api: {
18
+ method: req.method,
19
+ path: req.path,
20
+ action: `${ req.method } ${ req.path }`,
21
+ body: sanitizeBody( req.body ),
22
+ },
23
+ response: {
24
+ code: null,
25
+ body: null,
26
+ },
27
+ steps: [],
28
+ stepCounter: 0,
29
+ error: null,
30
+ };
31
+ }
32
+
33
+ export function getLogContext() {
34
+ return activityLogStore.getStore();
35
+ }
36
+
37
+ export function pushStep( entry ) {
38
+ const ctx = getLogContext();
39
+ if ( !ctx ) return;
40
+
41
+ const last = ctx.steps[ctx.steps.length - 1];
42
+ if (
43
+ last &&
44
+ last.name === entry.name &&
45
+ last.type === entry.type &&
46
+ last.status === 'success' &&
47
+ entry.status === 'success'
48
+ ) {
49
+ last.count = ( last.count || 1 ) + 1;
50
+ last.ms += entry.ms;
51
+ return;
52
+ }
53
+
54
+ ctx.stepCounter++;
55
+ ctx.steps.push( {
56
+ stepIndex: ctx.stepCounter,
57
+ name: entry.name,
58
+ type: entry.type,
59
+ status: entry.status,
60
+ ms: entry.ms,
61
+ } );
62
+ }
63
+
64
+ export function pushStepFailure( entry, error ) {
65
+ const ctx = getLogContext();
66
+ if ( !ctx ) return;
67
+ ctx.stepCounter++;
68
+ const message = error?.message || String( error );
69
+ ctx.steps.push( {
70
+ stepIndex: ctx.stepCounter,
71
+ name: entry.name,
72
+ type: entry.type,
73
+ status: 'failed',
74
+ ms: entry.ms,
75
+ error: message,
76
+ } );
77
+ ctx.error = {
78
+ message,
79
+ failedAtStepIndex: ctx.stepCounter,
80
+ failedStepName: entry.name,
81
+ };
82
+ }
83
+
84
+ function sanitizeBody( body ) {
85
+ if ( !body || typeof body !== 'object' ) return body;
86
+ const cleaned = { ...body };
87
+ for ( const key of SENSITIVE_KEYS ) {
88
+ if ( cleaned[key] !== undefined ) cleaned[key] = '[REDACTED]';
89
+ }
90
+ return cleaned;
91
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Post-process a steps array to compress repeated adjacent cycles into a single
3
+ * "batch" entry. Complements the push-time collapse of identical adjacent
4
+ * steps (which merges A,A,A into A×3) by merging repeated *sequences*
5
+ * (A,B,C,A,B,C,A,B,C → batch[A→B→C]×3).
6
+ *
7
+ * Rules:
8
+ * - A cycle is detected only if the same ordered sequence of step names/types
9
+ * appears ≥ 2 times back-to-back.
10
+ * - Failed steps are never included in cycles — each failure stands alone so
11
+ * you can see exactly which iteration broke.
12
+ * - Greedy: smallest cycle length (≥ 2) wins. This gives the most compact,
13
+ * most readable output.
14
+ * - Cycle length is bounded to keep the O(n·k) scan cheap.
15
+ */
16
+
17
+ const MIN_CYCLE_LEN = 2;
18
+ const MAX_CYCLE_LEN = 10;
19
+
20
+ export function compressBatches( rawSteps ) {
21
+ if ( !Array.isArray( rawSteps ) || rawSteps.length < MIN_CYCLE_LEN * 2 ) return rawSteps;
22
+
23
+ const out = [];
24
+ let i = 0;
25
+ while ( i < rawSteps.length ) {
26
+ let absorbed = false;
27
+ const maxK = Math.min( MAX_CYCLE_LEN, Math.floor( ( rawSteps.length - i ) / 2 ) );
28
+
29
+ for ( let k = MIN_CYCLE_LEN; k <= maxK; k++ ) {
30
+ if ( !cycleEquals( rawSteps, i, k, i + k ) ) continue;
31
+
32
+ // At least 2 repeats confirmed. Count forward.
33
+ let repeats = 2;
34
+ let j = i + 2 * k;
35
+ while ( cycleEquals( rawSteps, i, k, j ) ) {
36
+ repeats++;
37
+ j += k;
38
+ }
39
+
40
+ const patternSteps = rawSteps.slice( i, i + k );
41
+ const totalMs = rawSteps.slice( i, j ).reduce( ( sum, s ) => sum + ( s.ms || 0 ), 0 );
42
+
43
+ out.push( {
44
+ stepIndex: out.length + 1,
45
+ name: patternSteps.map( ( s ) => s.count ? `${ s.name }×${ s.count }` : s.name ).join( ' → ' ),
46
+ type: 'batch',
47
+ status: 'success',
48
+ count: repeats,
49
+ ms: totalMs,
50
+ pattern: patternSteps.map( ( s ) => ( {
51
+ name: s.name,
52
+ type: s.type,
53
+ count: s.count ?? 1,
54
+ } ) ),
55
+ } );
56
+
57
+ i = j;
58
+ absorbed = true;
59
+ break;
60
+ }
61
+
62
+ if ( !absorbed ) {
63
+ out.push( { ...rawSteps[i], stepIndex: out.length + 1 } );
64
+ i++;
65
+ }
66
+ }
67
+
68
+ return out;
69
+ }
70
+
71
+ function cycleEquals( steps, patternStart, patternLen, checkStart ) {
72
+ if ( checkStart + patternLen > steps.length ) return false;
73
+ for ( let i = 0; i < patternLen; i++ ) {
74
+ const a = steps[patternStart + i];
75
+ const b = steps[checkStart + i];
76
+ if ( !a || !b ) return false;
77
+ if ( a.status === 'failed' || b.status === 'failed' ) return false;
78
+ if ( a.name !== b.name ) return false;
79
+ if ( a.type !== b.type ) return false;
80
+ if ( a.status !== b.status ) return false;
81
+ }
82
+ return true;
83
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Self-contained configuration for the activity logging module.
3
+ *
4
+ * Read directly from process.env so this module has zero dependency on
5
+ * consumers' env.js layout. Getters (not cached values) so dotenv can load
6
+ * after this module is imported.
7
+ */
8
+
9
+ const DEFAULT_INDEX = 'trax-activity-logs-test';
10
+ const LOG_VERSION = 'v1';
11
+
12
+ export const loggingConfig = {
13
+ get enabled() {
14
+ return process.env.TRAX_ACTIVITY_LOGGING === 'true';
15
+ },
16
+ get index() {
17
+ try {
18
+ return JSON.parse( process.env.OPENSEARCH || '{}' )?.traxActivityLog || DEFAULT_INDEX;
19
+ } catch ( _e ) {
20
+ return DEFAULT_INDEX;
21
+ }
22
+ },
23
+ version: LOG_VERSION,
24
+ };
@@ -0,0 +1,46 @@
1
+ import { getLogContext, pushStep, pushStepFailure } from './activityLogStore.js';
2
+ import { loggingConfig } from './config.js';
3
+
4
+ const WRITE_OPS = [
5
+ 'create',
6
+ 'updateOne',
7
+ 'updateMany',
8
+ 'deleteOne',
9
+ 'deleteMany',
10
+ 'upsertOne',
11
+ 'insertMany',
12
+ 'bulkUpsert',
13
+ 'bulkWrite',
14
+ 'removeKeys',
15
+ 'findOneAndUpdate',
16
+ 'findOneAndUpdate2',
17
+ 'updateOneArrayItem',
18
+ 'update',
19
+ ];
20
+
21
+ export function createLoggableService( serviceModule, collectionName ) {
22
+ // Install the proxy unconditionally. The enabled-check happens at call time
23
+ // inside the `get` trap so env vars loaded after module import (via dotenv)
24
+ // are observed correctly.
25
+ return new Proxy( serviceModule, {
26
+ get( target, prop ) {
27
+ const original = target[prop];
28
+ if ( !WRITE_OPS.includes( prop ) || typeof original !== 'function' ) return original;
29
+ if ( !loggingConfig.enabled ) return original;
30
+
31
+ return async function( ...args ) {
32
+ const ctx = getLogContext();
33
+ const start = Date.now();
34
+ const stepName = `${ collectionName }.${ prop }`;
35
+ try {
36
+ const result = await original.apply( target, args );
37
+ if ( ctx ) pushStep( { name: stepName, type: 'db', status: 'success', ms: Date.now() - start } );
38
+ return result;
39
+ } catch ( e ) {
40
+ if ( ctx ) pushStepFailure( { name: stepName, type: 'db', ms: Date.now() - start }, e );
41
+ throw e;
42
+ }
43
+ };
44
+ },
45
+ } );
46
+ }
@@ -0,0 +1,37 @@
1
+ import { pushStep, pushStepFailure } from './activityLogStore.js';
2
+
3
+ export async function logExternalCall( serviceName, url, options = {} ) {
4
+ const start = Date.now();
5
+ const stepName = `external.${ serviceName }`;
6
+ try {
7
+ const res = await fetch( url, options );
8
+ pushStep( {
9
+ name: stepName,
10
+ type: 'external',
11
+ status: res.ok ? 'success' : 'failed',
12
+ ms: Date.now() - start,
13
+ } );
14
+ return res;
15
+ } catch ( e ) {
16
+ pushStepFailure( { name: stepName, type: 'external', ms: Date.now() - start }, e );
17
+ throw e;
18
+ }
19
+ }
20
+
21
+ export async function logQueueMessage( serviceName, queueUrl, messageBody, sendMessageToQueueFn ) {
22
+ const start = Date.now();
23
+ const stepName = `external.${ serviceName }`;
24
+ try {
25
+ const result = await sendMessageToQueueFn( queueUrl, messageBody );
26
+ pushStep( {
27
+ name: stepName,
28
+ type: 'external',
29
+ status: 'success',
30
+ ms: Date.now() - start,
31
+ } );
32
+ return result;
33
+ } catch ( e ) {
34
+ pushStepFailure( { name: stepName, type: 'external', ms: Date.now() - start }, e );
35
+ throw e;
36
+ }
37
+ }
@@ -1,14 +1,20 @@
1
1
  import model from 'tango-api-schema';
2
+ import { createLoggableService } from '../logging/createLoggableService.js';
2
3
 
3
- export const aggregate = async ( query ={} ) => {
4
- return model.appVersionModel.aggregate( query );
4
+ const _methods = {
5
+ async aggregate( query ={} ) {
6
+ return model.appVersionModel.aggregate( query );
7
+ },
8
+ async updateOne( query, record ) {
9
+ return model.appVersionModel.updateOne( query, { $set: record }, { upsert: true } );
10
+ },
11
+ async findOne( query ={}, field={} ) {
12
+ return model.appVersionModel.findOne( query, field );
13
+ },
5
14
  };
6
15
 
7
- export const updateOne = async ( query, record ) => {
8
- return model.appVersionModel.updateOne( query, { $set: record }, { upsert: true } );
9
- };
10
-
11
- export const findOne = async ( query ={}, field={} ) => {
12
- return model.appVersionModel.findOne( query, field );
13
- };
16
+ const _svc = createLoggableService( _methods, 'appversions' );
14
17
 
18
+ export const aggregate = ( ...args ) => _svc.aggregate( ...args );
19
+ export const updateOne = ( ...args ) => _svc.updateOne( ...args );
20
+ export const findOne = ( ...args ) => _svc.findOne( ...args );
@@ -1,20 +1,28 @@
1
1
  import model from 'tango-api-schema';
2
+ import { createLoggableService } from '../logging/createLoggableService.js';
2
3
 
3
- export const create = async ( data ) => {
4
- return model.traxApproverModel.create( data );
4
+ const _methods = {
5
+ async create( data ) {
6
+ return model.traxApproverModel.create( data );
7
+ },
8
+ async updateMany( query={}, record={} ) {
9
+ return model.traxApproverModel.updateMany( query, { $set: record } );
10
+ },
11
+ async insertMany( data = [] ) {
12
+ return model.traxApproverModel.insertMany( data );
13
+ },
14
+ async find( query ={}, field={} ) {
15
+ return model.traxApproverModel.find( query, field );
16
+ },
17
+ async findOne( query ={}, field={} ) {
18
+ return model.traxApproverModel.findOne( query, field );
19
+ },
5
20
  };
6
21
 
7
- export const updateMany = async ( query={}, record={} ) => {
8
- return model.traxApproverModel.updateMany( query, { $set: record } );
9
- };
10
-
11
- export const insertMany = async ( data = [] ) => {
12
- return model.traxApproverModel.insertMany( data );
13
- };
22
+ const _svc = createLoggableService( _methods, 'approvers' );
14
23
 
15
- export const find = async ( query ={}, field={} ) => {
16
- return model.traxApproverModel.find( query, field );
17
- };
18
- export const findOne = async ( query ={}, field={} ) => {
19
- return model.traxApproverModel.findOne( query, field );
20
- };
24
+ export const create = ( ...args ) => _svc.create( ...args );
25
+ export const updateMany = ( ...args ) => _svc.updateMany( ...args );
26
+ export const insertMany = ( ...args ) => _svc.insertMany( ...args );
27
+ export const find = ( ...args ) => _svc.find( ...args );
28
+ export const findOne = ( ...args ) => _svc.findOne( ...args );
@@ -1,6 +1,12 @@
1
1
  import authenticationModel from 'tango-api-schema/schema/authentication.model.js';
2
+ import { createLoggableService } from '../logging/createLoggableService.js';
2
3
 
3
- export async function create( data ) {
4
- return authenticationModel.create( data );
5
- }
4
+ const _methods = {
5
+ async create( data ) {
6
+ return authenticationModel.create( data );
7
+ },
8
+ };
6
9
 
10
+ const _svc = createLoggableService( _methods, 'authentications' );
11
+
12
+ export const create = ( ...args ) => _svc.create( ...args );