@valentine-efagene/qshelter-common 2.0.69 → 2.0.71
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/dist/generated/client/enums.d.ts +6 -6
- package/dist/generated/client/enums.js +6 -6
- package/dist/generated/client/internal/class.js +1 -1
- package/dist/src/events/workflow-event.service.d.ts +58 -33
- package/dist/src/events/workflow-event.service.js +190 -168
- package/dist/src/events/workflow-types.d.ts +136 -87
- package/dist/src/events/workflow-types.js +9 -1
- package/package.json +1 -1
- package/prisma/migrations/20260106003757_business_friendly_handler_types/migration.sql +28 -0
- package/prisma/schema.prisma +7 -6
|
@@ -32,7 +32,19 @@
|
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
import { EventPublisher } from './event-publisher';
|
|
35
|
-
import {
|
|
35
|
+
import { NotificationChannel } from './notification-enums';
|
|
36
|
+
/**
|
|
37
|
+
* Simple in-memory automation registry
|
|
38
|
+
*/
|
|
39
|
+
class InMemoryAutomationRegistry {
|
|
40
|
+
automations = new Map();
|
|
41
|
+
get(automationName) {
|
|
42
|
+
return this.automations.get(automationName);
|
|
43
|
+
}
|
|
44
|
+
register(automationName, handler) {
|
|
45
|
+
this.automations.set(automationName, handler);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
36
48
|
/**
|
|
37
49
|
* Simple in-memory service registry
|
|
38
50
|
*/
|
|
@@ -47,21 +59,21 @@ class InMemoryServiceRegistry {
|
|
|
47
59
|
}
|
|
48
60
|
export class WorkflowEventService {
|
|
49
61
|
prisma;
|
|
50
|
-
|
|
62
|
+
automationRegistry;
|
|
51
63
|
eventPublisher;
|
|
52
|
-
constructor(prisma,
|
|
64
|
+
constructor(prisma, automationRegistry, eventPublisher) {
|
|
53
65
|
this.prisma = prisma;
|
|
54
|
-
this.
|
|
66
|
+
this.automationRegistry = automationRegistry || new InMemoryAutomationRegistry();
|
|
55
67
|
this.eventPublisher = eventPublisher || new EventPublisher('workflow-event-service');
|
|
56
68
|
}
|
|
57
69
|
/**
|
|
58
|
-
* Register
|
|
70
|
+
* Register an automation for RUN_AUTOMATION handlers
|
|
59
71
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
72
|
+
* Automations are business logic functions that can be triggered by events.
|
|
73
|
+
* Example: "calculateLateFee", "sendWelcomePackage", "archiveContract"
|
|
62
74
|
*/
|
|
63
|
-
|
|
64
|
-
this.
|
|
75
|
+
registerAutomation(name, handler) {
|
|
76
|
+
this.automationRegistry.register(name, handler);
|
|
65
77
|
}
|
|
66
78
|
/**
|
|
67
79
|
* Emit an event
|
|
@@ -350,53 +362,155 @@ export class WorkflowEventService {
|
|
|
350
362
|
// ==========================================
|
|
351
363
|
/**
|
|
352
364
|
* Execute a handler based on its type
|
|
365
|
+
*
|
|
366
|
+
* Handler types are business-friendly names that abstract the underlying implementation:
|
|
367
|
+
* - SEND_EMAIL: Send email via notification service (SNS → SQS → SES)
|
|
368
|
+
* - SEND_SMS: Send SMS via notification service
|
|
369
|
+
* - SEND_PUSH: Send push notification via notification service
|
|
370
|
+
* - CALL_WEBHOOK: Make HTTP request to external URL
|
|
371
|
+
* - ADVANCE_WORKFLOW: Move workflow steps forward/backward
|
|
372
|
+
* - RUN_AUTOMATION: Execute registered business logic automation
|
|
353
373
|
*/
|
|
354
374
|
async executeHandler(handlerType, config, payload, tenantId) {
|
|
355
375
|
switch (handlerType) {
|
|
356
|
-
case '
|
|
357
|
-
return this.
|
|
358
|
-
case '
|
|
359
|
-
return this.
|
|
360
|
-
case '
|
|
361
|
-
return this.
|
|
362
|
-
case '
|
|
363
|
-
return this.
|
|
364
|
-
case '
|
|
365
|
-
return this.
|
|
366
|
-
case '
|
|
367
|
-
|
|
368
|
-
throw new Error('Script handlers not yet implemented');
|
|
376
|
+
case 'SEND_EMAIL':
|
|
377
|
+
return this.executeSendEmailHandler(config, payload, tenantId);
|
|
378
|
+
case 'SEND_SMS':
|
|
379
|
+
return this.executeSendSmsHandler(config, payload, tenantId);
|
|
380
|
+
case 'SEND_PUSH':
|
|
381
|
+
return this.executeSendPushHandler(config, payload, tenantId);
|
|
382
|
+
case 'CALL_WEBHOOK':
|
|
383
|
+
return this.executeCallWebhookHandler(config, payload);
|
|
384
|
+
case 'ADVANCE_WORKFLOW':
|
|
385
|
+
return this.executeAdvanceWorkflowHandler(config, payload, tenantId);
|
|
386
|
+
case 'RUN_AUTOMATION':
|
|
387
|
+
return this.executeRunAutomationHandler(config, payload, tenantId);
|
|
369
388
|
default:
|
|
370
389
|
throw new Error(`Unknown handler type: ${handlerType}`);
|
|
371
390
|
}
|
|
372
391
|
}
|
|
373
392
|
/**
|
|
374
|
-
* Execute
|
|
393
|
+
* Execute SEND_EMAIL handler
|
|
394
|
+
*
|
|
395
|
+
* Sends an email via the notification service using SNS → SQS → SES.
|
|
396
|
+
* Business users configure: template, recipient, and template data.
|
|
375
397
|
*/
|
|
376
|
-
async
|
|
377
|
-
//
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
398
|
+
async executeSendEmailHandler(config, payload, tenantId) {
|
|
399
|
+
// Build the notification payload
|
|
400
|
+
const notificationPayload = this.buildNotificationPayload(config, payload);
|
|
401
|
+
// Resolve recipient email from the payload
|
|
402
|
+
if (config.recipientPath) {
|
|
403
|
+
const email = this.resolvePath(payload, config.recipientPath.replace(/^\$\./, ''));
|
|
404
|
+
if (email && typeof email === 'string') {
|
|
405
|
+
notificationPayload.to_email = email;
|
|
406
|
+
}
|
|
381
407
|
}
|
|
382
|
-
//
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
408
|
+
// Publish to SNS via EventPublisher
|
|
409
|
+
const messageId = await this.eventPublisher.publish(config.notificationType, NotificationChannel.EMAIL, notificationPayload, {
|
|
410
|
+
tenantId,
|
|
411
|
+
correlationId: payload.correlationId || undefined,
|
|
412
|
+
userId: payload.userId || payload.actorId || undefined,
|
|
413
|
+
});
|
|
414
|
+
return {
|
|
415
|
+
success: true,
|
|
416
|
+
messageId,
|
|
417
|
+
notificationType: config.notificationType,
|
|
418
|
+
channel: 'email',
|
|
419
|
+
payload: notificationPayload,
|
|
420
|
+
tenantId,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Execute SEND_SMS handler
|
|
425
|
+
*
|
|
426
|
+
* Sends an SMS via the notification service.
|
|
427
|
+
* Business users configure: template, recipient phone, and template data.
|
|
428
|
+
*/
|
|
429
|
+
async executeSendSmsHandler(config, payload, tenantId) {
|
|
430
|
+
// Build the notification payload
|
|
431
|
+
const notificationPayload = this.buildNotificationPayload(config, payload);
|
|
432
|
+
// Resolve recipient phone from the payload
|
|
433
|
+
if (config.recipientPath) {
|
|
434
|
+
const phone = this.resolvePath(payload, config.recipientPath.replace(/^\$\./, ''));
|
|
435
|
+
if (phone && typeof phone === 'string') {
|
|
436
|
+
notificationPayload.to_phone = phone;
|
|
437
|
+
}
|
|
386
438
|
}
|
|
387
|
-
//
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
: payload
|
|
391
|
-
|
|
392
|
-
|
|
439
|
+
// Publish to SNS via EventPublisher
|
|
440
|
+
const messageId = await this.eventPublisher.publish(config.notificationType, NotificationChannel.SMS, notificationPayload, {
|
|
441
|
+
tenantId,
|
|
442
|
+
correlationId: payload.correlationId || undefined,
|
|
443
|
+
userId: payload.userId || payload.actorId || undefined,
|
|
444
|
+
});
|
|
445
|
+
return {
|
|
446
|
+
success: true,
|
|
447
|
+
messageId,
|
|
448
|
+
notificationType: config.notificationType,
|
|
449
|
+
channel: 'sms',
|
|
450
|
+
payload: notificationPayload,
|
|
451
|
+
tenantId,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Execute SEND_PUSH handler
|
|
456
|
+
*
|
|
457
|
+
* Sends a push notification via the notification service.
|
|
458
|
+
* Business users configure: template, recipient user, and template data.
|
|
459
|
+
*/
|
|
460
|
+
async executeSendPushHandler(config, payload, tenantId) {
|
|
461
|
+
// Build the notification payload
|
|
462
|
+
const notificationPayload = this.buildNotificationPayload(config, payload);
|
|
463
|
+
// Resolve recipient user ID from the payload
|
|
464
|
+
if (config.recipientPath) {
|
|
465
|
+
const userId = this.resolvePath(payload, config.recipientPath.replace(/^\$\./, ''));
|
|
466
|
+
if (userId && typeof userId === 'string') {
|
|
467
|
+
notificationPayload.to_user_id = userId;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Publish to SNS via EventPublisher
|
|
471
|
+
const messageId = await this.eventPublisher.publish(config.notificationType, NotificationChannel.PUSH, notificationPayload, {
|
|
472
|
+
tenantId,
|
|
473
|
+
correlationId: payload.correlationId || undefined,
|
|
474
|
+
userId: payload.userId || payload.actorId || undefined,
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
success: true,
|
|
478
|
+
messageId,
|
|
479
|
+
notificationType: config.notificationType,
|
|
480
|
+
channel: 'push',
|
|
481
|
+
payload: notificationPayload,
|
|
482
|
+
tenantId,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Build notification payload from config and event payload
|
|
487
|
+
*/
|
|
488
|
+
buildNotificationPayload(config, payload) {
|
|
489
|
+
const result = {};
|
|
490
|
+
// Apply static template data first
|
|
491
|
+
if (config.staticData) {
|
|
492
|
+
Object.assign(result, config.staticData);
|
|
493
|
+
}
|
|
494
|
+
// Apply template data mapping (map from event payload to notification payload)
|
|
495
|
+
if (config.templateData) {
|
|
496
|
+
for (const [targetField, sourcePath] of Object.entries(config.templateData)) {
|
|
497
|
+
const value = this.resolvePath(payload, sourcePath.replace(/^\$\./, ''));
|
|
498
|
+
if (value !== undefined) {
|
|
499
|
+
result[targetField] = value;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return result;
|
|
393
504
|
}
|
|
394
505
|
/**
|
|
395
|
-
* Execute
|
|
506
|
+
* Execute CALL_WEBHOOK handler
|
|
507
|
+
*
|
|
508
|
+
* Makes an HTTP request to an external URL.
|
|
509
|
+
* Business users configure: URL, method, headers, and body mapping.
|
|
396
510
|
*/
|
|
397
|
-
async
|
|
398
|
-
const transformedPayload = config.
|
|
399
|
-
? this.transformPayload(payload, config.
|
|
511
|
+
async executeCallWebhookHandler(config, payload) {
|
|
512
|
+
const transformedPayload = config.bodyMapping
|
|
513
|
+
? this.transformPayload(payload, config.bodyMapping)
|
|
400
514
|
: payload;
|
|
401
515
|
const controller = new AbortController();
|
|
402
516
|
const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs || 30000);
|
|
@@ -428,106 +542,50 @@ export class WorkflowEventService {
|
|
|
428
542
|
}
|
|
429
543
|
}
|
|
430
544
|
/**
|
|
431
|
-
* Execute
|
|
545
|
+
* Execute ADVANCE_WORKFLOW handler
|
|
432
546
|
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
547
|
+
* Advances or modifies workflow state.
|
|
548
|
+
* Business users configure: action (approve/reject/skip), step path, and data.
|
|
435
549
|
*/
|
|
436
|
-
async
|
|
550
|
+
async executeAdvanceWorkflowHandler(config, payload, tenantId) {
|
|
551
|
+
// Resolve step ID from payload if path is provided
|
|
552
|
+
let stepId = config.stepId;
|
|
553
|
+
if (config.stepIdPath) {
|
|
554
|
+
const resolved = this.resolvePath(payload, config.stepIdPath.replace(/^\$\./, ''));
|
|
555
|
+
if (resolved && typeof resolved === 'string') {
|
|
556
|
+
stepId = resolved;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
437
559
|
// Return the workflow action data
|
|
438
|
-
// The workflow service should listen for
|
|
560
|
+
// The workflow service should listen for ADVANCE_WORKFLOW handler results
|
|
439
561
|
return {
|
|
440
562
|
action: config.action,
|
|
441
563
|
workflowId: config.workflowId,
|
|
442
564
|
phaseId: config.phaseId,
|
|
443
|
-
stepId
|
|
565
|
+
stepId,
|
|
444
566
|
data: { ...config.data, ...payload },
|
|
445
567
|
tenantId,
|
|
446
568
|
};
|
|
447
569
|
}
|
|
448
570
|
/**
|
|
449
|
-
* Execute
|
|
571
|
+
* Execute RUN_AUTOMATION handler
|
|
450
572
|
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
573
|
+
* Runs a registered business logic automation.
|
|
574
|
+
* Business users select from pre-defined automations like
|
|
575
|
+
* "Calculate Mortgage Payment", "Generate Contract", etc.
|
|
453
576
|
*/
|
|
454
|
-
async
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
channels: config.channels,
|
|
460
|
-
recipients: this.resolveRecipients(config.recipients, payload),
|
|
461
|
-
priority: config.priority || 'normal',
|
|
462
|
-
data: payload,
|
|
463
|
-
tenantId,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Execute an SNS handler
|
|
468
|
-
*
|
|
469
|
-
* Publishes to SNS topic using the EventPublisher, which triggers
|
|
470
|
-
* the notification-service via SNS->SQS subscription.
|
|
471
|
-
*
|
|
472
|
-
* The config specifies the notification type, channel, and how to
|
|
473
|
-
* map the event payload to the notification payload.
|
|
474
|
-
*/
|
|
475
|
-
async executeSnsHandler(config, payload, tenantId) {
|
|
476
|
-
// Build the notification payload from config
|
|
477
|
-
const notificationPayload = this.buildSnsPayload(config, payload);
|
|
478
|
-
// Map channel string to NotificationChannel enum
|
|
479
|
-
const channelMap = {
|
|
480
|
-
email: NotificationChannel.EMAIL,
|
|
481
|
-
sms: NotificationChannel.SMS,
|
|
482
|
-
push: NotificationChannel.PUSH,
|
|
483
|
-
};
|
|
484
|
-
const channel = channelMap[config.channel] || NotificationChannel.EMAIL;
|
|
485
|
-
// Validate notification type is a valid enum value
|
|
486
|
-
const notificationType = config.notificationType;
|
|
487
|
-
if (!Object.values(NotificationType).includes(notificationType)) {
|
|
488
|
-
throw new Error(`Invalid notification type: ${config.notificationType}`);
|
|
577
|
+
async executeRunAutomationHandler(config, payload, tenantId) {
|
|
578
|
+
// Get the automation function from the registry
|
|
579
|
+
const automationFn = this.automationRegistry.get(config.automation);
|
|
580
|
+
if (!automationFn) {
|
|
581
|
+
throw new Error(`Automation '${config.automation}' not found in registry`);
|
|
489
582
|
}
|
|
490
|
-
//
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
return {
|
|
497
|
-
success: true,
|
|
498
|
-
messageId,
|
|
499
|
-
notificationType: config.notificationType,
|
|
500
|
-
channel: config.channel,
|
|
501
|
-
payload: notificationPayload,
|
|
502
|
-
tenantId,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Build the notification payload for SNS from config and event payload
|
|
507
|
-
*/
|
|
508
|
-
buildSnsPayload(config, payload) {
|
|
509
|
-
const result = {};
|
|
510
|
-
// Apply static payload first
|
|
511
|
-
if (config.staticPayload) {
|
|
512
|
-
Object.assign(result, config.staticPayload);
|
|
513
|
-
}
|
|
514
|
-
// Apply payload mapping (map from event payload to notification payload)
|
|
515
|
-
if (config.payloadMapping) {
|
|
516
|
-
for (const [targetField, sourcePath] of Object.entries(config.payloadMapping)) {
|
|
517
|
-
const value = this.resolvePath(payload, sourcePath.replace(/^\$\./, ''));
|
|
518
|
-
if (value !== undefined) {
|
|
519
|
-
result[targetField] = value;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
// If recipientPath is specified, extract the recipient email
|
|
524
|
-
if (config.recipientPath) {
|
|
525
|
-
const email = this.resolvePath(payload, config.recipientPath.replace(/^\$\./, ''));
|
|
526
|
-
if (email && typeof email === 'string') {
|
|
527
|
-
result.to_email = email;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
return result;
|
|
583
|
+
// Transform payload if mapping is defined
|
|
584
|
+
const transformedPayload = config.inputMapping
|
|
585
|
+
? this.transformPayload(payload, config.inputMapping)
|
|
586
|
+
: payload;
|
|
587
|
+
// Call the automation function with inputs and tenantId
|
|
588
|
+
return automationFn(transformedPayload, tenantId);
|
|
531
589
|
}
|
|
532
590
|
// ==========================================
|
|
533
591
|
// UTILITY METHODS
|
|
@@ -612,49 +670,13 @@ export class WorkflowEventService {
|
|
|
612
670
|
}
|
|
613
671
|
return current;
|
|
614
672
|
}
|
|
615
|
-
/**
|
|
616
|
-
* Resolve recipients from config, potentially using payload variables
|
|
617
|
-
*/
|
|
618
|
-
resolveRecipients(recipients, payload) {
|
|
619
|
-
if (!recipients)
|
|
620
|
-
return undefined;
|
|
621
|
-
const resolved = {};
|
|
622
|
-
// Resolve email recipients
|
|
623
|
-
if (recipients.email) {
|
|
624
|
-
resolved.email = recipients.email.map((addr) => {
|
|
625
|
-
if (addr.startsWith('$.')) {
|
|
626
|
-
const value = this.resolvePath(payload, addr);
|
|
627
|
-
return typeof value === 'string' ? value : addr;
|
|
628
|
-
}
|
|
629
|
-
return addr;
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
// Resolve phone recipients
|
|
633
|
-
if (recipients.phone) {
|
|
634
|
-
resolved.phone = recipients.phone.map((phone) => {
|
|
635
|
-
if (phone.startsWith('$.')) {
|
|
636
|
-
const value = this.resolvePath(payload, phone);
|
|
637
|
-
return typeof value === 'string' ? value : phone;
|
|
638
|
-
}
|
|
639
|
-
return phone;
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
// Resolve userId recipients
|
|
643
|
-
if (recipients.userId) {
|
|
644
|
-
resolved.userId = recipients.userId.map((id) => {
|
|
645
|
-
if (id.startsWith('$.')) {
|
|
646
|
-
const value = this.resolvePath(payload, id);
|
|
647
|
-
return typeof value === 'string' ? value : id;
|
|
648
|
-
}
|
|
649
|
-
return id;
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
return resolved;
|
|
653
|
-
}
|
|
654
673
|
}
|
|
655
674
|
/**
|
|
656
675
|
* Create a workflow event service instance
|
|
676
|
+
*
|
|
677
|
+
* @param prisma - Prisma client for database access
|
|
678
|
+
* @param automationRegistry - Optional registry of business automations
|
|
657
679
|
*/
|
|
658
|
-
export function createWorkflowEventService(prisma,
|
|
659
|
-
return new WorkflowEventService(prisma,
|
|
680
|
+
export function createWorkflowEventService(prisma, automationRegistry) {
|
|
681
|
+
return new WorkflowEventService(prisma, automationRegistry);
|
|
660
682
|
}
|