hierarchical-approval 2.3.0 → 2.5.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +27 -0
  3. package/dist/{ApprovalEngine-Ci9urTDI.d.ts → ApprovalEngine-BHWls8-z.d.ts} +59 -7
  4. package/dist/{ApprovalEngine-C1dUPnLM.d.cts → ApprovalEngine-CW7Uu1bV.d.cts} +59 -7
  5. package/dist/{IAuditAdapter-C4OASS6u.d.ts → IAuditAdapter-CMCirvY-.d.ts} +1 -1
  6. package/dist/{IAuditAdapter-BHVKMMv6.d.cts → IAuditAdapter-Dwx8ZP4J.d.cts} +1 -1
  7. package/dist/{IAuthorizationPolicy-CtLkwQ8I.d.cts → IAuthorizationPolicy-B3vaV69v.d.cts} +1 -1
  8. package/dist/{IAuthorizationPolicy-BA_m2Dg9.d.ts → IAuthorizationPolicy-BryQIKko.d.ts} +1 -1
  9. package/dist/{INotificationAdapter-JJAiMyMd.d.cts → INotificationAdapter-AwbhZEqz.d.cts} +1 -1
  10. package/dist/{INotificationAdapter-BdgfUrIn.d.ts → INotificationAdapter-BBq2czpO.d.ts} +1 -1
  11. package/dist/{IOperationMiddleware-D0ksiFM3.d.cts → IOperationMiddleware-3J-_NSGq.d.cts} +1 -1
  12. package/dist/{IOperationMiddleware-BGYrAqep.d.ts → IOperationMiddleware-CqzFklMn.d.ts} +1 -1
  13. package/dist/{IStorageAdapter-BU3sau5W.d.ts → IStorageAdapter-Dw2xXP9B.d.ts} +1 -1
  14. package/dist/{IStorageAdapter-DdHO4Rf1.d.cts → IStorageAdapter-Pq9J60xv.d.cts} +1 -1
  15. package/dist/adapters/MemoryAdapter.d.cts +2 -2
  16. package/dist/adapters/MemoryAdapter.d.ts +2 -2
  17. package/dist/adapters/PostgresAdapter.d.cts +2 -2
  18. package/dist/adapters/PostgresAdapter.d.ts +2 -2
  19. package/dist/index.cjs +117 -6
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +8 -8
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +117 -7
  24. package/dist/index.js.map +1 -1
  25. package/dist/{instance-DUJY_Axf.d.cts → instance-BO-i9-nq.d.cts} +13 -0
  26. package/dist/{instance-DUJY_Axf.d.ts → instance-BO-i9-nq.d.ts} +13 -0
  27. package/dist/nestjs.cjs +49 -6
  28. package/dist/nestjs.cjs.map +1 -1
  29. package/dist/nestjs.d.cts +7 -7
  30. package/dist/nestjs.d.ts +7 -7
  31. package/dist/nestjs.js +49 -6
  32. package/dist/nestjs.js.map +1 -1
  33. package/dist/plugins/audit.d.cts +2 -2
  34. package/dist/plugins/audit.d.ts +2 -2
  35. package/dist/plugins/notify.cjs +105 -9
  36. package/dist/plugins/notify.cjs.map +1 -1
  37. package/dist/plugins/notify.d.cts +91 -3
  38. package/dist/plugins/notify.d.ts +91 -3
  39. package/dist/plugins/notify.js +105 -10
  40. package/dist/plugins/notify.js.map +1 -1
  41. package/dist/plugins/resilience.d.cts +3 -3
  42. package/dist/plugins/resilience.d.ts +3 -3
  43. package/dist/plugins/tracing.d.cts +2 -2
  44. package/dist/plugins/tracing.d.ts +2 -2
  45. package/dist/plugins/webhook.d.cts +2 -2
  46. package/dist/plugins/webhook.d.ts +2 -2
  47. package/dist/testing.cjs +49 -6
  48. package/dist/testing.cjs.map +1 -1
  49. package/dist/testing.d.cts +7 -7
  50. package/dist/testing.d.ts +7 -7
  51. package/dist/testing.js +49 -6
  52. package/dist/testing.js.map +1 -1
  53. package/package.json +1 -1
@@ -59,6 +59,12 @@ interface ApprovalLevelConfig {
59
59
  */
60
60
  subWorkflow?: SubWorkflowConfig;
61
61
  escalationAfterDays?: number;
62
+ /**
63
+ * Escalate this level after this many **working hours**, for SLAs quoted in
64
+ * hours rather than days. Mutually exclusive with
65
+ * {@link escalationAfterDays}.
66
+ */
67
+ escalationAfterHours?: number;
62
68
  /**
63
69
  * Send a reminder to this level's pending approvers this many days after the
64
70
  * level opens. Escalation reassigns work; a reminder only nudges, which is
@@ -166,6 +172,11 @@ interface ApprovalTemplateConfig {
166
172
  escalation?: EscalationConfig;
167
173
  /** Overall SLA for the entire workflow in days. Emits approval:sla_breached when elapsed. */
168
174
  slaDeadlineDays?: number;
175
+ /**
176
+ * Overall SLA in **working hours**. Mutually exclusive with
177
+ * {@link slaDeadlineDays}.
178
+ */
179
+ slaDeadlineHours?: number;
169
180
  /** Allow emergency override (bypass remaining levels). Must be true to use engine.override(). */
170
181
  allowOverride?: boolean;
171
182
  }
@@ -227,6 +238,7 @@ interface ApprovalLevelInstance {
227
238
  weights?: Record<string, number>;
228
239
  escalationDueAt?: Date;
229
240
  escalationAfterDays?: number;
241
+ escalationAfterHours?: number;
230
242
  /** When the next reminder for this level is due; cleared when the level closes. */
231
243
  reminderDueAt?: Date;
232
244
  /** How many reminders have already been sent for this level. */
@@ -281,6 +293,7 @@ interface InfoRequest {
281
293
  interface TemplateSnapshot {
282
294
  escalation?: EscalationConfig;
283
295
  slaDeadlineDays?: number;
296
+ slaDeadlineHours?: number;
284
297
  allowOverride?: boolean;
285
298
  }
286
299
  interface ApprovalInstance {
@@ -59,6 +59,12 @@ interface ApprovalLevelConfig {
59
59
  */
60
60
  subWorkflow?: SubWorkflowConfig;
61
61
  escalationAfterDays?: number;
62
+ /**
63
+ * Escalate this level after this many **working hours**, for SLAs quoted in
64
+ * hours rather than days. Mutually exclusive with
65
+ * {@link escalationAfterDays}.
66
+ */
67
+ escalationAfterHours?: number;
62
68
  /**
63
69
  * Send a reminder to this level's pending approvers this many days after the
64
70
  * level opens. Escalation reassigns work; a reminder only nudges, which is
@@ -166,6 +172,11 @@ interface ApprovalTemplateConfig {
166
172
  escalation?: EscalationConfig;
167
173
  /** Overall SLA for the entire workflow in days. Emits approval:sla_breached when elapsed. */
168
174
  slaDeadlineDays?: number;
175
+ /**
176
+ * Overall SLA in **working hours**. Mutually exclusive with
177
+ * {@link slaDeadlineDays}.
178
+ */
179
+ slaDeadlineHours?: number;
169
180
  /** Allow emergency override (bypass remaining levels). Must be true to use engine.override(). */
170
181
  allowOverride?: boolean;
171
182
  }
@@ -227,6 +238,7 @@ interface ApprovalLevelInstance {
227
238
  weights?: Record<string, number>;
228
239
  escalationDueAt?: Date;
229
240
  escalationAfterDays?: number;
241
+ escalationAfterHours?: number;
230
242
  /** When the next reminder for this level is due; cleared when the level closes. */
231
243
  reminderDueAt?: Date;
232
244
  /** How many reminders have already been sent for this level. */
@@ -281,6 +293,7 @@ interface InfoRequest {
281
293
  interface TemplateSnapshot {
282
294
  escalation?: EscalationConfig;
283
295
  slaDeadlineDays?: number;
296
+ slaDeadlineHours?: number;
284
297
  allowOverride?: boolean;
285
298
  }
286
299
  interface ApprovalInstance {
package/dist/nestjs.cjs CHANGED
@@ -1072,6 +1072,18 @@ var ApprovalEngine = class _ApprovalEngine {
1072
1072
  message: `Level ${l.level} sets reminderEveryDays without reminderAfterDays, so no reminder would ever be sent.`
1073
1073
  });
1074
1074
  }
1075
+ if (l.escalationAfterHours !== void 0 && l.escalationAfterHours <= 0) {
1076
+ errors.push({
1077
+ field: `levels[${i}].escalationAfterHours`,
1078
+ message: `Level ${l.level} escalationAfterHours must be a positive number.`
1079
+ });
1080
+ }
1081
+ if (l.escalationAfterDays !== void 0 && l.escalationAfterHours !== void 0) {
1082
+ errors.push({
1083
+ field: `levels[${i}].escalationAfterHours`,
1084
+ message: `Level ${l.level} sets both escalationAfterDays and escalationAfterHours; pick one so the deadline is unambiguous.`
1085
+ });
1086
+ }
1075
1087
  if (l.escalationAfterDays !== void 0 && l.escalationAfterDays <= 0) {
1076
1088
  errors.push({
1077
1089
  field: `levels[${i}].escalationAfterDays`,
@@ -1136,6 +1148,18 @@ var ApprovalEngine = class _ApprovalEngine {
1136
1148
  }
1137
1149
  });
1138
1150
  }
1151
+ if (config.slaDeadlineDays !== void 0 && config.slaDeadlineHours !== void 0) {
1152
+ errors.push({
1153
+ field: "slaDeadlineHours",
1154
+ message: "Template sets both slaDeadlineDays and slaDeadlineHours; pick one so the SLA is unambiguous."
1155
+ });
1156
+ }
1157
+ if (config.slaDeadlineHours !== void 0 && config.slaDeadlineHours <= 0) {
1158
+ errors.push({
1159
+ field: "slaDeadlineHours",
1160
+ message: "slaDeadlineHours must be a positive number."
1161
+ });
1162
+ }
1139
1163
  if (config.conditions) {
1140
1164
  config.conditions.forEach((rule, ruleIdx) => {
1141
1165
  errors.push(...validateConditionExpression(rule.when, `conditions[${ruleIdx}].when`));
@@ -1249,7 +1273,8 @@ var ApprovalEngine = class _ApprovalEngine {
1249
1273
  threshold: cfg.threshold,
1250
1274
  weights: cfg.weights,
1251
1275
  escalationAfterDays: cfg.escalationAfterDays,
1252
- escalationDueAt: inFirstGroup && cfg.escalationAfterDays ? this.deadlineFrom(now, cfg.escalationAfterDays) : void 0,
1276
+ escalationAfterHours: cfg.escalationAfterHours,
1277
+ escalationDueAt: inFirstGroup ? this.levelEscalationDue(now, cfg) : void 0,
1253
1278
  subWorkflowTemplate: cfg.subWorkflow?.templateName,
1254
1279
  reminderAfterDays: cfg.reminderAfterDays,
1255
1280
  reminderEveryDays: cfg.reminderEveryDays,
@@ -1279,7 +1304,7 @@ var ApprovalEngine = class _ApprovalEngine {
1279
1304
  timestamp: now,
1280
1305
  ...auditCtx
1281
1306
  };
1282
- const slaDeadlineAt = template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
1307
+ const slaDeadlineAt = template.slaDeadlineHours ? this.deadlineFromHours(now, template.slaDeadlineHours) : template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
1283
1308
  const instance = {
1284
1309
  id: instanceId,
1285
1310
  tenantId: this.tenantId,
@@ -1307,6 +1332,7 @@ var ApprovalEngine = class _ApprovalEngine {
1307
1332
  templateSnapshot: {
1308
1333
  escalation: template.escalation,
1309
1334
  slaDeadlineDays: template.slaDeadlineDays,
1335
+ slaDeadlineHours: template.slaDeadlineHours,
1310
1336
  allowOverride: template.allowOverride
1311
1337
  }
1312
1338
  };
@@ -2578,7 +2604,7 @@ var ApprovalEngine = class _ApprovalEngine {
2578
2604
  reason: opts.reason,
2579
2605
  ...auditCtx
2580
2606
  };
2581
- const slaDeadlineAt = template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
2607
+ const slaDeadlineAt = template.slaDeadlineHours ? this.deadlineFromHours(now, template.slaDeadlineHours) : template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
2582
2608
  const newInstance = {
2583
2609
  id: newInstanceId,
2584
2610
  tenantId: this.tenantId,
@@ -2601,6 +2627,7 @@ var ApprovalEngine = class _ApprovalEngine {
2601
2627
  templateSnapshot: {
2602
2628
  escalation: template.escalation,
2603
2629
  slaDeadlineDays: template.slaDeadlineDays,
2630
+ slaDeadlineHours: template.slaDeadlineHours,
2604
2631
  allowOverride: template.allowOverride
2605
2632
  }
2606
2633
  };
@@ -3543,6 +3570,24 @@ var ApprovalEngine = class _ApprovalEngine {
3543
3570
  deadlineFrom(from, days) {
3544
3571
  return this.calendar ? this.calendar.addBusinessDays(from, days) : new Date(from.getTime() + days * 864e5);
3545
3572
  }
3573
+ /**
3574
+ * Resolve an hour-based deadline.
3575
+ *
3576
+ * A calendar that only knows whole days — `weekendCalendar`, say — has no
3577
+ * `addBusinessHours`, and this falls back to elapsed clock time rather than
3578
+ * quietly pretending the calendar was applied. Configure
3579
+ * `businessHoursCalendar` to have hours skip evenings and weekends.
3580
+ */
3581
+ deadlineFromHours(from, hours) {
3582
+ const addHours = this.calendar?.addBusinessHours?.bind(this.calendar);
3583
+ return addHours ? addHours(from, hours) : new Date(from.getTime() + hours * 36e5);
3584
+ }
3585
+ /** Level deadline from whichever of days/hours the template configured. */
3586
+ levelEscalationDue(from, level) {
3587
+ if (level.escalationAfterHours) return this.deadlineFromHours(from, level.escalationAfterHours);
3588
+ if (level.escalationAfterDays) return this.deadlineFrom(from, level.escalationAfterDays);
3589
+ return void 0;
3590
+ }
3546
3591
  async requireInstance(id) {
3547
3592
  const instance = await this.opts.adapter.getInstance(this.tenantId, id);
3548
3593
  if (!instance) throw new ApprovalNotFoundError("Instance", id);
@@ -3648,9 +3693,7 @@ var ApprovalEngine = class _ApprovalEngine {
3648
3693
  now
3649
3694
  );
3650
3695
  }
3651
- if (lvl.escalationAfterDays) {
3652
- lvl.escalationDueAt = this.deadlineFrom(now, lvl.escalationAfterDays);
3653
- }
3696
+ lvl.escalationDueAt = this.levelEscalationDue(now, lvl);
3654
3697
  this.scheduleReminder(lvl, now);
3655
3698
  lvl.status = "pending";
3656
3699
  }