hierarchical-approval 2.4.0 → 2.6.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 (49) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +54 -0
  3. package/dist/{ApprovalEngine-C1dUPnLM.d.cts → ApprovalEngine-BWrblNQ0.d.cts} +81 -7
  4. package/dist/{ApprovalEngine-Ci9urTDI.d.ts → ApprovalEngine-D4DdSx-M.d.ts} +81 -7
  5. package/dist/{IAuditAdapter-BHVKMMv6.d.cts → IAuditAdapter-2UU5edKG.d.cts} +1 -1
  6. package/dist/{IAuditAdapter-C4OASS6u.d.ts → IAuditAdapter-DSor54iY.d.ts} +1 -1
  7. package/dist/{IAuthorizationPolicy-BA_m2Dg9.d.ts → IAuthorizationPolicy-BOWg-3AH.d.ts} +1 -1
  8. package/dist/{IAuthorizationPolicy-CtLkwQ8I.d.cts → IAuthorizationPolicy-DPpiKzyh.d.cts} +1 -1
  9. package/dist/{INotificationAdapter-BdgfUrIn.d.ts → INotificationAdapter-BNGivLgU.d.ts} +1 -1
  10. package/dist/{INotificationAdapter-JJAiMyMd.d.cts → INotificationAdapter-C6HPrzh6.d.cts} +1 -1
  11. package/dist/{IOperationMiddleware-BGYrAqep.d.ts → IOperationMiddleware-CXtg2wzF.d.ts} +1 -1
  12. package/dist/{IOperationMiddleware-D0ksiFM3.d.cts → IOperationMiddleware-CxnPzpqL.d.cts} +1 -1
  13. package/dist/{IStorageAdapter-BU3sau5W.d.ts → IStorageAdapter-ChPT7ZDp.d.ts} +1 -1
  14. package/dist/{IStorageAdapter-DdHO4Rf1.d.cts → IStorageAdapter-yX9ERfQE.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 +179 -11
  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 +179 -12
  24. package/dist/index.js.map +1 -1
  25. package/dist/{instance-DUJY_Axf.d.cts → instance-weh2w7Ji.d.cts} +39 -1
  26. package/dist/{instance-DUJY_Axf.d.ts → instance-weh2w7Ji.d.ts} +39 -1
  27. package/dist/nestjs.cjs +111 -11
  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 +111 -11
  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.d.cts +2 -2
  36. package/dist/plugins/notify.d.ts +2 -2
  37. package/dist/plugins/resilience.d.cts +3 -3
  38. package/dist/plugins/resilience.d.ts +3 -3
  39. package/dist/plugins/tracing.d.cts +2 -2
  40. package/dist/plugins/tracing.d.ts +2 -2
  41. package/dist/plugins/webhook.d.cts +2 -2
  42. package/dist/plugins/webhook.d.ts +2 -2
  43. package/dist/testing.cjs +111 -11
  44. package/dist/testing.cjs.map +1 -1
  45. package/dist/testing.d.cts +7 -7
  46. package/dist/testing.d.ts +7 -7
  47. package/dist/testing.js +111 -11
  48. package/dist/testing.js.map +1 -1
  49. package/package.json +1 -1
package/dist/nestjs.d.cts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { OnModuleDestroy, DynamicModule, ModuleMetadata, InjectionToken, OptionalFactoryDependency, Inject } from '@nestjs/common';
2
- import { a as ApprovalEngine, b as ApprovalEngineOptions } from './ApprovalEngine-C1dUPnLM.cjs';
3
- import './IStorageAdapter-DdHO4Rf1.cjs';
4
- import './instance-DUJY_Axf.cjs';
5
- import './INotificationAdapter-JJAiMyMd.cjs';
2
+ import { a as ApprovalEngine, b as ApprovalEngineOptions } from './ApprovalEngine-BWrblNQ0.cjs';
3
+ import './IStorageAdapter-yX9ERfQE.cjs';
4
+ import './instance-weh2w7Ji.cjs';
5
+ import './INotificationAdapter-C6HPrzh6.cjs';
6
6
  import 'zod';
7
7
  import './Logger-BplhlU7l.cjs';
8
8
  import './Clock-3FnOczFJ.cjs';
9
- import './IOperationMiddleware-D0ksiFM3.cjs';
10
- import './IAuditAdapter-BHVKMMv6.cjs';
9
+ import './IOperationMiddleware-CxnPzpqL.cjs';
10
+ import './IAuditAdapter-2UU5edKG.cjs';
11
11
  import './IMetricsAdapter-DWq8IFaf.cjs';
12
12
  import './ISchedulerAdapter-DKv_QjVN.cjs';
13
- import './IAuthorizationPolicy-CtLkwQ8I.cjs';
13
+ import './IAuthorizationPolicy-DPpiKzyh.cjs';
14
14
 
15
15
  /** Options for {@link HierarchicalApprovalModule.forRoot}. */
16
16
  interface HierarchicalApprovalModuleOptions extends ApprovalEngineOptions {
package/dist/nestjs.d.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { OnModuleDestroy, DynamicModule, ModuleMetadata, InjectionToken, OptionalFactoryDependency, Inject } from '@nestjs/common';
2
- import { a as ApprovalEngine, b as ApprovalEngineOptions } from './ApprovalEngine-Ci9urTDI.js';
3
- import './IStorageAdapter-BU3sau5W.js';
4
- import './instance-DUJY_Axf.js';
5
- import './INotificationAdapter-BdgfUrIn.js';
2
+ import { a as ApprovalEngine, b as ApprovalEngineOptions } from './ApprovalEngine-D4DdSx-M.js';
3
+ import './IStorageAdapter-ChPT7ZDp.js';
4
+ import './instance-weh2w7Ji.js';
5
+ import './INotificationAdapter-BNGivLgU.js';
6
6
  import 'zod';
7
7
  import './Logger-BplhlU7l.js';
8
8
  import './Clock-3FnOczFJ.js';
9
- import './IOperationMiddleware-BGYrAqep.js';
10
- import './IAuditAdapter-C4OASS6u.js';
9
+ import './IOperationMiddleware-CXtg2wzF.js';
10
+ import './IAuditAdapter-DSor54iY.js';
11
11
  import './IMetricsAdapter-DWq8IFaf.js';
12
12
  import './ISchedulerAdapter-DKv_QjVN.js';
13
- import './IAuthorizationPolicy-BA_m2Dg9.js';
13
+ import './IAuthorizationPolicy-BOWg-3AH.js';
14
14
 
15
15
  /** Options for {@link HierarchicalApprovalModule.forRoot}. */
16
16
  interface HierarchicalApprovalModuleOptions extends ApprovalEngineOptions {
package/dist/nestjs.js CHANGED
@@ -1066,6 +1066,18 @@ var ApprovalEngine = class _ApprovalEngine {
1066
1066
  message: `Level ${l.level} sets reminderEveryDays without reminderAfterDays, so no reminder would ever be sent.`
1067
1067
  });
1068
1068
  }
1069
+ if (l.escalationAfterHours !== void 0 && l.escalationAfterHours <= 0) {
1070
+ errors.push({
1071
+ field: `levels[${i}].escalationAfterHours`,
1072
+ message: `Level ${l.level} escalationAfterHours must be a positive number.`
1073
+ });
1074
+ }
1075
+ if (l.escalationAfterDays !== void 0 && l.escalationAfterHours !== void 0) {
1076
+ errors.push({
1077
+ field: `levels[${i}].escalationAfterHours`,
1078
+ message: `Level ${l.level} sets both escalationAfterDays and escalationAfterHours; pick one so the deadline is unambiguous.`
1079
+ });
1080
+ }
1069
1081
  if (l.escalationAfterDays !== void 0 && l.escalationAfterDays <= 0) {
1070
1082
  errors.push({
1071
1083
  field: `levels[${i}].escalationAfterDays`,
@@ -1130,6 +1142,18 @@ var ApprovalEngine = class _ApprovalEngine {
1130
1142
  }
1131
1143
  });
1132
1144
  }
1145
+ if (config.slaDeadlineDays !== void 0 && config.slaDeadlineHours !== void 0) {
1146
+ errors.push({
1147
+ field: "slaDeadlineHours",
1148
+ message: "Template sets both slaDeadlineDays and slaDeadlineHours; pick one so the SLA is unambiguous."
1149
+ });
1150
+ }
1151
+ if (config.slaDeadlineHours !== void 0 && config.slaDeadlineHours <= 0) {
1152
+ errors.push({
1153
+ field: "slaDeadlineHours",
1154
+ message: "slaDeadlineHours must be a positive number."
1155
+ });
1156
+ }
1133
1157
  if (config.conditions) {
1134
1158
  config.conditions.forEach((rule, ruleIdx) => {
1135
1159
  errors.push(...validateConditionExpression(rule.when, `conditions[${ruleIdx}].when`));
@@ -1243,7 +1267,9 @@ var ApprovalEngine = class _ApprovalEngine {
1243
1267
  threshold: cfg.threshold,
1244
1268
  weights: cfg.weights,
1245
1269
  escalationAfterDays: cfg.escalationAfterDays,
1246
- escalationDueAt: inFirstGroup && cfg.escalationAfterDays ? this.deadlineFrom(now, cfg.escalationAfterDays) : void 0,
1270
+ escalationAfterHours: cfg.escalationAfterHours,
1271
+ escalationStep: 0,
1272
+ escalationDueAt: inFirstGroup ? this.levelEscalationDue(now, cfg, this.firstRungOf(template.escalationSteps)) : void 0,
1247
1273
  subWorkflowTemplate: cfg.subWorkflow?.templateName,
1248
1274
  reminderAfterDays: cfg.reminderAfterDays,
1249
1275
  reminderEveryDays: cfg.reminderEveryDays,
@@ -1273,7 +1299,7 @@ var ApprovalEngine = class _ApprovalEngine {
1273
1299
  timestamp: now,
1274
1300
  ...auditCtx
1275
1301
  };
1276
- const slaDeadlineAt = template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
1302
+ const slaDeadlineAt = template.slaDeadlineHours ? this.deadlineFromHours(now, template.slaDeadlineHours) : template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
1277
1303
  const instance = {
1278
1304
  id: instanceId,
1279
1305
  tenantId: this.tenantId,
@@ -1300,7 +1326,9 @@ var ApprovalEngine = class _ApprovalEngine {
1300
1326
  slaDeadlineAt,
1301
1327
  templateSnapshot: {
1302
1328
  escalation: template.escalation,
1329
+ escalationSteps: template.escalationSteps,
1303
1330
  slaDeadlineDays: template.slaDeadlineDays,
1331
+ slaDeadlineHours: template.slaDeadlineHours,
1304
1332
  allowOverride: template.allowOverride
1305
1333
  }
1306
1334
  };
@@ -2572,7 +2600,7 @@ var ApprovalEngine = class _ApprovalEngine {
2572
2600
  reason: opts.reason,
2573
2601
  ...auditCtx
2574
2602
  };
2575
- const slaDeadlineAt = template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
2603
+ const slaDeadlineAt = template.slaDeadlineHours ? this.deadlineFromHours(now, template.slaDeadlineHours) : template.slaDeadlineDays ? this.deadlineFrom(now, template.slaDeadlineDays) : void 0;
2576
2604
  const newInstance = {
2577
2605
  id: newInstanceId,
2578
2606
  tenantId: this.tenantId,
@@ -2594,7 +2622,9 @@ var ApprovalEngine = class _ApprovalEngine {
2594
2622
  slaDeadlineAt,
2595
2623
  templateSnapshot: {
2596
2624
  escalation: template.escalation,
2625
+ escalationSteps: template.escalationSteps,
2597
2626
  slaDeadlineDays: template.slaDeadlineDays,
2627
+ slaDeadlineHours: template.slaDeadlineHours,
2598
2628
  allowOverride: template.allowOverride
2599
2629
  }
2600
2630
  };
@@ -3341,10 +3371,14 @@ var ApprovalEngine = class _ApprovalEngine {
3341
3371
  async escalateInternal(instanceId, escalatedBy = "system", auditCtx, levelNumber) {
3342
3372
  return this.withOptimisticRetry(instanceId, async (instance) => {
3343
3373
  if (instance.status !== "pending") return instance;
3344
- const escalationConfig = instance.templateSnapshot?.escalation ?? (await this.registry.get(instance.templateName)).escalation;
3345
- if (!escalationConfig) return instance;
3374
+ const ladder = await this.escalationLadder(instance);
3375
+ const targetLevel = levelNumber === void 0 ? this.currentLevelInstance(instance) : instance.levels.find((l) => l.level === levelNumber) ?? this.currentLevelInstance(instance);
3376
+ const rungIndex = targetLevel.escalationStep ?? 0;
3377
+ const rung = ladder[rungIndex];
3378
+ const escalateTo = rung?.escalateTo ?? (instance.templateSnapshot?.escalation ?? (await this.registry.get(instance.templateName)).escalation)?.escalateTo;
3379
+ if (!escalateTo) return instance;
3346
3380
  const newApprovers = await this.resolver.resolveApprovers(
3347
- [escalationConfig.escalateTo],
3381
+ [escalateTo],
3348
3382
  instance.submittedBy,
3349
3383
  instance.data,
3350
3384
  this.opts.orgProvider,
@@ -3362,10 +3396,13 @@ var ApprovalEngine = class _ApprovalEngine {
3362
3396
  );
3363
3397
  return instance;
3364
3398
  }
3365
- const level = levelNumber === void 0 ? this.currentLevelInstance(instance) : instance.levels.find((l) => l.level === levelNumber) ?? this.currentLevelInstance(instance);
3399
+ const level = targetLevel;
3366
3400
  level.approverIds = [.../* @__PURE__ */ new Set([...level.approverIds, ...filteredApprovers])];
3367
- level.escalationDueAt = void 0;
3368
3401
  const now = this.clock.now();
3402
+ level.escalationStep = rungIndex + 1;
3403
+ const nextRung = ladder[rungIndex + 1];
3404
+ const levelOpenedAt = this.levelOpenedAt(instance, level, now);
3405
+ level.escalationDueAt = nextRung ? this.stepDueAt(levelOpenedAt, nextRung) : void 0;
3369
3406
  const escalatedTo = filteredApprovers[0] ?? "unknown";
3370
3407
  const auditEntry = {
3371
3408
  action: "escalated",
@@ -3537,6 +3574,69 @@ var ApprovalEngine = class _ApprovalEngine {
3537
3574
  deadlineFrom(from, days) {
3538
3575
  return this.calendar ? this.calendar.addBusinessDays(from, days) : new Date(from.getTime() + days * 864e5);
3539
3576
  }
3577
+ /**
3578
+ * Resolve an hour-based deadline.
3579
+ *
3580
+ * A calendar that only knows whole days — `weekendCalendar`, say — has no
3581
+ * `addBusinessHours`, and this falls back to elapsed clock time rather than
3582
+ * quietly pretending the calendar was applied. Configure
3583
+ * `businessHoursCalendar` to have hours skip evenings and weekends.
3584
+ */
3585
+ deadlineFromHours(from, hours) {
3586
+ const addHours = this.calendar?.addBusinessHours?.bind(this.calendar);
3587
+ return addHours ? addHours(from, hours) : new Date(from.getTime() + hours * 36e5);
3588
+ }
3589
+ /** Level deadline from whichever of days/hours the template configured. */
3590
+ levelEscalationDue(from, level, firstRung) {
3591
+ if (level.escalationAfterHours) return this.deadlineFromHours(from, level.escalationAfterHours);
3592
+ if (level.escalationAfterDays) return this.deadlineFrom(from, level.escalationAfterDays);
3593
+ return firstRung ? this.stepDueAt(from, firstRung) : void 0;
3594
+ }
3595
+ /** First rung of a ladder, sorted by delay, or undefined when there is none. */
3596
+ firstRungOf(steps) {
3597
+ if (!steps || steps.length === 0) return void 0;
3598
+ return [...steps].sort(
3599
+ (a, b) => (a.afterHours ?? (a.afterDays ?? 0) * 24) - (b.afterHours ?? (b.afterDays ?? 0) * 24)
3600
+ )[0];
3601
+ }
3602
+ /**
3603
+ * The escalation ladder for an instance, sorted by delay.
3604
+ *
3605
+ * Read from the instance's template snapshot so an in-flight approval keeps
3606
+ * the ladder it was submitted under, exactly as the single-step
3607
+ * {@link EscalationConfig} already did.
3608
+ */
3609
+ async escalationLadder(instance) {
3610
+ const snapshot = instance.templateSnapshot;
3611
+ const steps = snapshot?.escalationSteps ?? (await this.registry.get(instance.templateName)).escalationSteps ?? [];
3612
+ return [...steps].sort(
3613
+ (a, b) => (a.afterHours ?? (a.afterDays ?? 0) * 24) - (b.afterHours ?? (b.afterDays ?? 0) * 24)
3614
+ );
3615
+ }
3616
+ /**
3617
+ * When a level started collecting decisions.
3618
+ *
3619
+ * Escalation rungs are measured from this, not from the previous rung, so a
3620
+ * ladder reads the way it is written. Recovered from the audit trail — the
3621
+ * `submitted` entry for the opening level, the `level_advanced` entry
3622
+ * otherwise — and falls back to `now` when no entry exists, which only leaves
3623
+ * the ladder no worse off than the single-step behaviour it replaces.
3624
+ */
3625
+ levelOpenedAt(instance, level, fallback) {
3626
+ for (let i = instance.auditLog.length - 1; i >= 0; i--) {
3627
+ const entry = instance.auditLog[i];
3628
+ if (!entry) continue;
3629
+ const opensThisLevel = (entry.action === "level_advanced" || entry.action === "submitted") && entry.level === level.level;
3630
+ if (opensThisLevel) return new Date(entry.timestamp);
3631
+ }
3632
+ return fallback;
3633
+ }
3634
+ /** Deadline for one rung, measured from when the level opened. */
3635
+ stepDueAt(from, step) {
3636
+ if (step.afterHours) return this.deadlineFromHours(from, step.afterHours);
3637
+ if (step.afterDays) return this.deadlineFrom(from, step.afterDays);
3638
+ return void 0;
3639
+ }
3540
3640
  async requireInstance(id) {
3541
3641
  const instance = await this.opts.adapter.getInstance(this.tenantId, id);
3542
3642
  if (!instance) throw new ApprovalNotFoundError("Instance", id);
@@ -3629,6 +3729,7 @@ var ApprovalEngine = class _ApprovalEngine {
3629
3729
  }
3630
3730
  /** Resolve approvers for a group, set its deadlines, and mark it pending. */
3631
3731
  async activateGroup(instance, group, now) {
3732
+ const firstRung = this.firstRungOf(instance.templateSnapshot?.escalationSteps);
3632
3733
  for (const lvl of group) {
3633
3734
  if (lvl.subWorkflowTemplate) {
3634
3735
  lvl.approverIds = [];
@@ -3642,9 +3743,8 @@ var ApprovalEngine = class _ApprovalEngine {
3642
3743
  now
3643
3744
  );
3644
3745
  }
3645
- if (lvl.escalationAfterDays) {
3646
- lvl.escalationDueAt = this.deadlineFrom(now, lvl.escalationAfterDays);
3647
- }
3746
+ lvl.escalationDueAt = this.levelEscalationDue(now, lvl, firstRung);
3747
+ lvl.escalationStep = 0;
3648
3748
  this.scheduleReminder(lvl, now);
3649
3749
  lvl.status = "pending";
3650
3750
  }