hierarchical-approval 2.5.0 → 2.7.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.
- package/CHANGELOG.md +72 -0
- package/README.md +58 -0
- package/dist/{ApprovalEngine-CW7Uu1bV.d.cts → ApprovalEngine-BH9GvzMI.d.cts} +97 -7
- package/dist/{ApprovalEngine-BHWls8-z.d.ts → ApprovalEngine-DkQcLaeT.d.ts} +97 -7
- package/dist/{IAuditAdapter-Dwx8ZP4J.d.cts → IAuditAdapter-2UU5edKG.d.cts} +1 -1
- package/dist/{IAuditAdapter-CMCirvY-.d.ts → IAuditAdapter-DSor54iY.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-BryQIKko.d.ts → IAuthorizationPolicy-BOWg-3AH.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-B3vaV69v.d.cts → IAuthorizationPolicy-DPpiKzyh.d.cts} +1 -1
- package/dist/{INotificationAdapter-BBq2czpO.d.ts → INotificationAdapter-BNGivLgU.d.ts} +1 -1
- package/dist/{INotificationAdapter-AwbhZEqz.d.cts → INotificationAdapter-C6HPrzh6.d.cts} +1 -1
- package/dist/{IOperationMiddleware-CqzFklMn.d.ts → IOperationMiddleware-CXtg2wzF.d.ts} +1 -1
- package/dist/{IOperationMiddleware-3J-_NSGq.d.cts → IOperationMiddleware-CxnPzpqL.d.cts} +1 -1
- package/dist/{IStorageAdapter-Dw2xXP9B.d.ts → IStorageAdapter-ChPT7ZDp.d.ts} +1 -1
- package/dist/{IStorageAdapter-Pq9J60xv.d.cts → IStorageAdapter-yX9ERfQE.d.cts} +1 -1
- package/dist/adapters/MemoryAdapter.d.cts +2 -2
- package/dist/adapters/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/PostgresAdapter.d.cts +2 -2
- package/dist/adapters/PostgresAdapter.d.ts +2 -2
- package/dist/index.cjs +157 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +157 -8
- package/dist/index.js.map +1 -1
- package/dist/{instance-BO-i9-nq.d.cts → instance-weh2w7Ji.d.cts} +26 -1
- package/dist/{instance-BO-i9-nq.d.ts → instance-weh2w7Ji.d.ts} +26 -1
- package/dist/nestjs.cjs +157 -8
- package/dist/nestjs.cjs.map +1 -1
- package/dist/nestjs.d.cts +7 -7
- package/dist/nestjs.d.ts +7 -7
- package/dist/nestjs.js +157 -8
- package/dist/nestjs.js.map +1 -1
- package/dist/plugins/audit.d.cts +2 -2
- package/dist/plugins/audit.d.ts +2 -2
- package/dist/plugins/notify.d.cts +2 -2
- package/dist/plugins/notify.d.ts +2 -2
- package/dist/plugins/resilience.d.cts +3 -3
- package/dist/plugins/resilience.d.ts +3 -3
- package/dist/plugins/tracing.d.cts +2 -2
- package/dist/plugins/tracing.d.ts +2 -2
- package/dist/plugins/webhook.d.cts +2 -2
- package/dist/plugins/webhook.d.ts +2 -2
- package/dist/testing.cjs +157 -8
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +7 -7
- package/dist/testing.d.ts +7 -7
- package/dist/testing.js +157 -8
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1273,7 +1273,8 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
1273
1273
|
weights: cfg.weights,
|
|
1274
1274
|
escalationAfterDays: cfg.escalationAfterDays,
|
|
1275
1275
|
escalationAfterHours: cfg.escalationAfterHours,
|
|
1276
|
-
|
|
1276
|
+
escalationStep: 0,
|
|
1277
|
+
escalationDueAt: inFirstGroup ? this.levelEscalationDue(now, cfg, this.firstRungOf(template.escalationSteps)) : void 0,
|
|
1277
1278
|
subWorkflowTemplate: cfg.subWorkflow?.templateName,
|
|
1278
1279
|
reminderAfterDays: cfg.reminderAfterDays,
|
|
1279
1280
|
reminderEveryDays: cfg.reminderEveryDays,
|
|
@@ -1330,6 +1331,7 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
1330
1331
|
slaDeadlineAt,
|
|
1331
1332
|
templateSnapshot: {
|
|
1332
1333
|
escalation: template.escalation,
|
|
1334
|
+
escalationSteps: template.escalationSteps,
|
|
1333
1335
|
slaDeadlineDays: template.slaDeadlineDays,
|
|
1334
1336
|
slaDeadlineHours: template.slaDeadlineHours,
|
|
1335
1337
|
allowOverride: template.allowOverride
|
|
@@ -2625,6 +2627,7 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
2625
2627
|
slaDeadlineAt,
|
|
2626
2628
|
templateSnapshot: {
|
|
2627
2629
|
escalation: template.escalation,
|
|
2630
|
+
escalationSteps: template.escalationSteps,
|
|
2628
2631
|
slaDeadlineDays: template.slaDeadlineDays,
|
|
2629
2632
|
slaDeadlineHours: template.slaDeadlineHours,
|
|
2630
2633
|
allowOverride: template.allowOverride
|
|
@@ -2690,6 +2693,98 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
2690
2693
|
return { levels, conditionsApplied };
|
|
2691
2694
|
}
|
|
2692
2695
|
/** Check whether a user is eligible to approve a specific instance. Never throws. */
|
|
2696
|
+
/**
|
|
2697
|
+
* Explain why a chain resolves the way it does for a given document.
|
|
2698
|
+
*
|
|
2699
|
+
* `previewApprovalChain()` answers *what* the chain will be. This answers
|
|
2700
|
+
* *why*: which rule added a level, which rule removed one, which rules were
|
|
2701
|
+
* evaluated and did not match, and where each level's approvers came from —
|
|
2702
|
+
* the question behind "why does this purchase order have a CFO level?", which
|
|
2703
|
+
* previously meant reading the template and re-evaluating the conditions by
|
|
2704
|
+
* hand.
|
|
2705
|
+
*
|
|
2706
|
+
* A rule that throws — an operator nobody registered, a malformed group — is
|
|
2707
|
+
* reported against that rule rather than failing the whole explanation. The
|
|
2708
|
+
* explanation is a diagnostic tool, and it is least useful at exactly the
|
|
2709
|
+
* moment a broken rule makes it throw.
|
|
2710
|
+
*
|
|
2711
|
+
* Reads nothing and writes nothing; safe to expose to a support UI.
|
|
2712
|
+
*
|
|
2713
|
+
* @param templateName - Template to explain.
|
|
2714
|
+
* @param data - Document data the conditions are evaluated against.
|
|
2715
|
+
* @param submittedBy - Submitter, used for approver resolution.
|
|
2716
|
+
*/
|
|
2717
|
+
async explainChain(templateName, data, submittedBy) {
|
|
2718
|
+
const template = await this.registry.get(templateName);
|
|
2719
|
+
const conditions = template.conditions ?? [];
|
|
2720
|
+
const rules = [];
|
|
2721
|
+
const addedBy = /* @__PURE__ */ new Map();
|
|
2722
|
+
const skippedBy = /* @__PURE__ */ new Map();
|
|
2723
|
+
conditions.forEach((rule, index) => {
|
|
2724
|
+
const addsLevels = (rule.addLevels ?? []).map((l) => l.level);
|
|
2725
|
+
const skipsLevels = rule.skipLevels ?? [];
|
|
2726
|
+
try {
|
|
2727
|
+
const outcome = evaluateConditions([rule], data);
|
|
2728
|
+
const matched = outcome.addLevels.length > 0 || outcome.skipLevels.size > 0;
|
|
2729
|
+
rules.push({ index, matched, addsLevels, skipsLevels });
|
|
2730
|
+
if (matched) {
|
|
2731
|
+
for (const l of outcome.addLevels) if (!addedBy.has(l.level)) addedBy.set(l.level, index);
|
|
2732
|
+
for (const l of outcome.skipLevels) if (!skippedBy.has(l)) skippedBy.set(l, index);
|
|
2733
|
+
}
|
|
2734
|
+
} catch (err) {
|
|
2735
|
+
rules.push({
|
|
2736
|
+
index,
|
|
2737
|
+
matched: false,
|
|
2738
|
+
addsLevels,
|
|
2739
|
+
skipsLevels,
|
|
2740
|
+
error: err.message
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
let mutations;
|
|
2745
|
+
try {
|
|
2746
|
+
mutations = evaluateConditions(conditions, data);
|
|
2747
|
+
} catch {
|
|
2748
|
+
mutations = { addLevels: [], skipLevels: /* @__PURE__ */ new Set() };
|
|
2749
|
+
}
|
|
2750
|
+
const active = [...template.levels, ...mutations.addLevels].filter((l) => !mutations.skipLevels.has(l.level)).sort((a, b) => a.level - b.level);
|
|
2751
|
+
const levels = [];
|
|
2752
|
+
for (const cfg of active) {
|
|
2753
|
+
const fromCondition = addedBy.has(cfg.level);
|
|
2754
|
+
const base = {
|
|
2755
|
+
level: cfg.level,
|
|
2756
|
+
name: cfg.name,
|
|
2757
|
+
mode: cfg.mode,
|
|
2758
|
+
source: fromCondition ? "condition" : "template",
|
|
2759
|
+
...fromCondition ? { addedByRule: addedBy.get(cfg.level) } : {},
|
|
2760
|
+
resolvedApprovers: [],
|
|
2761
|
+
...cfg.subWorkflow ? { subWorkflowTemplate: cfg.subWorkflow.templateName } : {}
|
|
2762
|
+
};
|
|
2763
|
+
if (cfg.subWorkflow) {
|
|
2764
|
+
levels.push(base);
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
try {
|
|
2768
|
+
base.resolvedApprovers = await this.resolver.resolveApprovers(
|
|
2769
|
+
cfg.approvers,
|
|
2770
|
+
submittedBy,
|
|
2771
|
+
data,
|
|
2772
|
+
this.opts.orgProvider,
|
|
2773
|
+
this.opts.outOfOfficeProvider,
|
|
2774
|
+
this.clock.now()
|
|
2775
|
+
);
|
|
2776
|
+
} catch (err) {
|
|
2777
|
+
base.resolutionError = err.message;
|
|
2778
|
+
}
|
|
2779
|
+
levels.push(base);
|
|
2780
|
+
}
|
|
2781
|
+
const skipped = template.levels.filter((l) => mutations.skipLevels.has(l.level)).map((l) => ({
|
|
2782
|
+
level: l.level,
|
|
2783
|
+
name: l.name,
|
|
2784
|
+
skippedByRule: skippedBy.get(l.level) ?? -1
|
|
2785
|
+
})).sort((a, b) => a.level - b.level);
|
|
2786
|
+
return { templateName: template.name, levels, skipped, rules };
|
|
2787
|
+
}
|
|
2693
2788
|
async canApprove(instanceId, userId) {
|
|
2694
2789
|
let instance;
|
|
2695
2790
|
try {
|
|
@@ -3373,10 +3468,14 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3373
3468
|
async escalateInternal(instanceId, escalatedBy = "system", auditCtx, levelNumber) {
|
|
3374
3469
|
return this.withOptimisticRetry(instanceId, async (instance) => {
|
|
3375
3470
|
if (instance.status !== "pending") return instance;
|
|
3376
|
-
const
|
|
3377
|
-
|
|
3471
|
+
const ladder = await this.escalationLadder(instance);
|
|
3472
|
+
const targetLevel = levelNumber === void 0 ? this.currentLevelInstance(instance) : instance.levels.find((l) => l.level === levelNumber) ?? this.currentLevelInstance(instance);
|
|
3473
|
+
const rungIndex = targetLevel.escalationStep ?? 0;
|
|
3474
|
+
const rung = ladder[rungIndex];
|
|
3475
|
+
const escalateTo = rung?.escalateTo ?? (instance.templateSnapshot?.escalation ?? (await this.registry.get(instance.templateName)).escalation)?.escalateTo;
|
|
3476
|
+
if (!escalateTo) return instance;
|
|
3378
3477
|
const newApprovers = await this.resolver.resolveApprovers(
|
|
3379
|
-
[
|
|
3478
|
+
[escalateTo],
|
|
3380
3479
|
instance.submittedBy,
|
|
3381
3480
|
instance.data,
|
|
3382
3481
|
this.opts.orgProvider,
|
|
@@ -3394,10 +3493,13 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3394
3493
|
);
|
|
3395
3494
|
return instance;
|
|
3396
3495
|
}
|
|
3397
|
-
const level =
|
|
3496
|
+
const level = targetLevel;
|
|
3398
3497
|
level.approverIds = [.../* @__PURE__ */ new Set([...level.approverIds, ...filteredApprovers])];
|
|
3399
|
-
level.escalationDueAt = void 0;
|
|
3400
3498
|
const now = this.clock.now();
|
|
3499
|
+
level.escalationStep = rungIndex + 1;
|
|
3500
|
+
const nextRung = ladder[rungIndex + 1];
|
|
3501
|
+
const levelOpenedAt = this.levelOpenedAt(instance, level, now);
|
|
3502
|
+
level.escalationDueAt = nextRung ? this.stepDueAt(levelOpenedAt, nextRung) : void 0;
|
|
3401
3503
|
const escalatedTo = filteredApprovers[0] ?? "unknown";
|
|
3402
3504
|
const auditEntry = {
|
|
3403
3505
|
action: "escalated",
|
|
@@ -3582,9 +3684,54 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3582
3684
|
return addHours ? addHours(from, hours) : new Date(from.getTime() + hours * 36e5);
|
|
3583
3685
|
}
|
|
3584
3686
|
/** Level deadline from whichever of days/hours the template configured. */
|
|
3585
|
-
levelEscalationDue(from, level) {
|
|
3687
|
+
levelEscalationDue(from, level, firstRung) {
|
|
3586
3688
|
if (level.escalationAfterHours) return this.deadlineFromHours(from, level.escalationAfterHours);
|
|
3587
3689
|
if (level.escalationAfterDays) return this.deadlineFrom(from, level.escalationAfterDays);
|
|
3690
|
+
return firstRung ? this.stepDueAt(from, firstRung) : void 0;
|
|
3691
|
+
}
|
|
3692
|
+
/** First rung of a ladder, sorted by delay, or undefined when there is none. */
|
|
3693
|
+
firstRungOf(steps) {
|
|
3694
|
+
if (!steps || steps.length === 0) return void 0;
|
|
3695
|
+
return [...steps].sort(
|
|
3696
|
+
(a, b) => (a.afterHours ?? (a.afterDays ?? 0) * 24) - (b.afterHours ?? (b.afterDays ?? 0) * 24)
|
|
3697
|
+
)[0];
|
|
3698
|
+
}
|
|
3699
|
+
/**
|
|
3700
|
+
* The escalation ladder for an instance, sorted by delay.
|
|
3701
|
+
*
|
|
3702
|
+
* Read from the instance's template snapshot so an in-flight approval keeps
|
|
3703
|
+
* the ladder it was submitted under, exactly as the single-step
|
|
3704
|
+
* {@link EscalationConfig} already did.
|
|
3705
|
+
*/
|
|
3706
|
+
async escalationLadder(instance) {
|
|
3707
|
+
const snapshot = instance.templateSnapshot;
|
|
3708
|
+
const steps = snapshot?.escalationSteps ?? (await this.registry.get(instance.templateName)).escalationSteps ?? [];
|
|
3709
|
+
return [...steps].sort(
|
|
3710
|
+
(a, b) => (a.afterHours ?? (a.afterDays ?? 0) * 24) - (b.afterHours ?? (b.afterDays ?? 0) * 24)
|
|
3711
|
+
);
|
|
3712
|
+
}
|
|
3713
|
+
/**
|
|
3714
|
+
* When a level started collecting decisions.
|
|
3715
|
+
*
|
|
3716
|
+
* Escalation rungs are measured from this, not from the previous rung, so a
|
|
3717
|
+
* ladder reads the way it is written. Recovered from the audit trail — the
|
|
3718
|
+
* `submitted` entry for the opening level, the `level_advanced` entry
|
|
3719
|
+
* otherwise — and falls back to `now` when no entry exists, which only leaves
|
|
3720
|
+
* the ladder no worse off than the single-step behaviour it replaces.
|
|
3721
|
+
*/
|
|
3722
|
+
levelOpenedAt(instance, level, fallback) {
|
|
3723
|
+
for (let i = instance.auditLog.length - 1; i >= 0; i--) {
|
|
3724
|
+
const entry = instance.auditLog[i];
|
|
3725
|
+
if (!entry) continue;
|
|
3726
|
+
const opensThisLevel = (entry.action === "level_advanced" || entry.action === "submitted") && entry.level === level.level;
|
|
3727
|
+
if (opensThisLevel) return new Date(entry.timestamp);
|
|
3728
|
+
}
|
|
3729
|
+
return fallback;
|
|
3730
|
+
}
|
|
3731
|
+
/** Deadline for one rung, measured from when the level opened. */
|
|
3732
|
+
stepDueAt(from, step) {
|
|
3733
|
+
if (step.afterHours) return this.deadlineFromHours(from, step.afterHours);
|
|
3734
|
+
if (step.afterDays) return this.deadlineFrom(from, step.afterDays);
|
|
3588
3735
|
return void 0;
|
|
3589
3736
|
}
|
|
3590
3737
|
async requireInstance(id) {
|
|
@@ -3679,6 +3826,7 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3679
3826
|
}
|
|
3680
3827
|
/** Resolve approvers for a group, set its deadlines, and mark it pending. */
|
|
3681
3828
|
async activateGroup(instance, group, now) {
|
|
3829
|
+
const firstRung = this.firstRungOf(instance.templateSnapshot?.escalationSteps);
|
|
3682
3830
|
for (const lvl of group) {
|
|
3683
3831
|
if (lvl.subWorkflowTemplate) {
|
|
3684
3832
|
lvl.approverIds = [];
|
|
@@ -3692,7 +3840,8 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
3692
3840
|
now
|
|
3693
3841
|
);
|
|
3694
3842
|
}
|
|
3695
|
-
lvl.escalationDueAt = this.levelEscalationDue(now, lvl);
|
|
3843
|
+
lvl.escalationDueAt = this.levelEscalationDue(now, lvl, firstRung);
|
|
3844
|
+
lvl.escalationStep = 0;
|
|
3696
3845
|
this.scheduleReminder(lvl, now);
|
|
3697
3846
|
lvl.status = "pending";
|
|
3698
3847
|
}
|