hierarchical-approval 1.8.0 → 1.9.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 +32 -0
- package/README.md +24 -0
- package/dist/{ApprovalEngine-cI4LP2Mx.d.cts → ApprovalEngine-BDI4r_KB.d.cts} +43 -1
- package/dist/{ApprovalEngine-CElfKXOA.d.ts → ApprovalEngine-a1jXglT3.d.ts} +43 -1
- package/dist/index.cjs +60 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -1
- package/dist/nestjs.cjs +60 -0
- package/dist/nestjs.cjs.map +1 -1
- package/dist/nestjs.d.cts +1 -1
- package/dist/nestjs.d.ts +1 -1
- package/dist/nestjs.js +60 -0
- package/dist/nestjs.js.map +1 -1
- package/dist/testing.cjs +60 -0
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +60 -0
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,38 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
_Nothing yet._
|
|
9
9
|
|
|
10
|
+
## [1.9.0] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added — `getWorkload()`
|
|
13
|
+
|
|
14
|
+
- **Reports who currently owes a decision, and how overdue they are.**
|
|
15
|
+
`getStatistics()` answers how the tenant is doing; nothing answered who is
|
|
16
|
+
holding it up — the question behind rebalancing a queue, spotting the approver
|
|
17
|
+
who has been on leave for a week, or deciding whom to `transferApprovals()` a
|
|
18
|
+
departing colleague's work to.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
await engine.getWorkload({ documentType: 'purchase_order' });
|
|
22
|
+
// [{ approverId: 'alice', pending: 12, instances: 11, overdue: 3, onHold: 1,
|
|
23
|
+
// oldestPendingAt: …, oldestAgeMs: 604800000 }, …]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Sorted busiest first. `pending` counts open **levels** while `instances`
|
|
27
|
+
counts distinct documents — they differ when one person holds several branches
|
|
28
|
+
of a parallel group. `overdue` measures against each level's escalation
|
|
29
|
+
deadline, and `onHold` counts work paused by a clarification request.
|
|
30
|
+
|
|
31
|
+
- **An approver who has already voted is not counted**, even while the level
|
|
32
|
+
stays open collecting other votes: they owe nothing more, and counting them
|
|
33
|
+
would overstate the queue of every quorum and weighted level.
|
|
34
|
+
|
|
35
|
+
- Computed from pending instances rather than a dedicated index, so it works on
|
|
36
|
+
any storage adapter with no new adapter methods. That means it reads every
|
|
37
|
+
pending instance in the tenant — fine for the volumes an approval queue
|
|
38
|
+
reaches, but it is a reporting call, not something for a hot path.
|
|
39
|
+
|
|
40
|
+
New export: `ApproverWorkload`.
|
|
41
|
+
|
|
10
42
|
## [1.8.0] - 2026-09-04
|
|
11
43
|
|
|
12
44
|
### Added — `transferApprovals()`
|
package/README.md
CHANGED
|
@@ -399,6 +399,30 @@ engine.registerConditionOperator('between', (actual, expected) => {
|
|
|
399
399
|
});
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
+
### Who is holding things up
|
|
403
|
+
|
|
404
|
+
`getStatistics()` answers how the tenant is doing; `getWorkload()` answers who
|
|
405
|
+
is holding it up — the question behind rebalancing a queue or deciding whom to
|
|
406
|
+
hand a departing colleague's work to:
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
const workload = await engine.getWorkload({ documentType: 'purchase_order' });
|
|
410
|
+
// [
|
|
411
|
+
// { approverId: 'alice', pending: 12, instances: 11, overdue: 3, onHold: 1,
|
|
412
|
+
// oldestPendingAt: 2026-01-02T…, oldestAgeMs: 604800000 },
|
|
413
|
+
// ...
|
|
414
|
+
// ]
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Busiest queue first. `pending` counts open **levels**, `instances` counts
|
|
418
|
+
distinct documents — they differ when one person holds several branches of a
|
|
419
|
+
parallel group. An approver who has already voted on a level that is still
|
|
420
|
+
collecting other votes owes nothing more and is not counted.
|
|
421
|
+
|
|
422
|
+
Computed from pending instances rather than a dedicated index, so it works on
|
|
423
|
+
any adapter with no new adapter methods — but it reads every pending instance in
|
|
424
|
+
the tenant. It is a reporting call, not something for a hot path.
|
|
425
|
+
|
|
402
426
|
### Transferring a person's queue
|
|
403
427
|
|
|
404
428
|
Someone leaves, changes team, or goes on long-term leave, and their open
|
|
@@ -284,6 +284,27 @@ interface TransferResult {
|
|
|
284
284
|
/** True when nothing was written. */
|
|
285
285
|
dryRun: boolean;
|
|
286
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* What one approver currently owes a decision on.
|
|
289
|
+
*
|
|
290
|
+
* Durations are milliseconds. An approver appears only while they hold at least
|
|
291
|
+
* one open level.
|
|
292
|
+
*/
|
|
293
|
+
interface ApproverWorkload {
|
|
294
|
+
approverId: string;
|
|
295
|
+
/** Open levels assigned to them. One instance can contribute several across parallel branches. */
|
|
296
|
+
pending: number;
|
|
297
|
+
/** Distinct documents involved — usually, but not always, equal to {@link pending}. */
|
|
298
|
+
instances: number;
|
|
299
|
+
/** Open levels already past their escalation deadline. */
|
|
300
|
+
overdue: number;
|
|
301
|
+
/** Open levels currently paused by a clarification request. */
|
|
302
|
+
onHold: number;
|
|
303
|
+
/** When the oldest of their open items was submitted. */
|
|
304
|
+
oldestPendingAt?: Date;
|
|
305
|
+
/** Age of that oldest item. `0` when they hold nothing. */
|
|
306
|
+
oldestAgeMs: number;
|
|
307
|
+
}
|
|
287
308
|
interface ApprovalStatistics {
|
|
288
309
|
/** Total instances matching the filter (across all statuses). */
|
|
289
310
|
total: number;
|
|
@@ -607,6 +628,27 @@ declare class ApprovalEngine {
|
|
|
607
628
|
* submittedBy, date range) — `status` is ignored since every status is counted.
|
|
608
629
|
* Adapter-agnostic: issues one cheap count query per status plus an overdue scan.
|
|
609
630
|
*/
|
|
631
|
+
/**
|
|
632
|
+
* Who currently owes a decision, and how overdue they are.
|
|
633
|
+
*
|
|
634
|
+
* `getStatistics()` answers how the tenant is doing; this answers who is
|
|
635
|
+
* holding it up — the question behind rebalancing a queue, spotting the
|
|
636
|
+
* approver who has been on leave for a week, or deciding whom to
|
|
637
|
+
* {@link transferApprovals} a departing colleague's work to.
|
|
638
|
+
*
|
|
639
|
+
* Computed from pending instances rather than a dedicated index, so it works
|
|
640
|
+
* on any storage adapter with no new adapter methods. That means it reads
|
|
641
|
+
* every pending instance in the tenant: fine for the operational volumes an
|
|
642
|
+
* approval queue reaches, but it is a reporting call, not something to put on
|
|
643
|
+
* a hot path.
|
|
644
|
+
*
|
|
645
|
+
* Rows are sorted by {@link ApproverWorkload.pending} descending, so the
|
|
646
|
+
* busiest queue is first.
|
|
647
|
+
*
|
|
648
|
+
* @param filter - Optional scoping; `status` is ignored, since only pending work counts.
|
|
649
|
+
* @returns One row per approver holding at least one open level.
|
|
650
|
+
*/
|
|
651
|
+
getWorkload(filter?: Omit<InstanceFilter, 'status'>): Promise<ApproverWorkload[]>;
|
|
610
652
|
getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
|
|
611
653
|
shutdown(): Promise<void>;
|
|
612
654
|
/**
|
|
@@ -704,4 +746,4 @@ declare class ApprovalEngine {
|
|
|
704
746
|
private runExternalAudit;
|
|
705
747
|
}
|
|
706
748
|
|
|
707
|
-
export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type TransferResult as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type
|
|
749
|
+
export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type TransferResult as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type ApproverWorkload as f, type BusinessCalendar as g, type CancelOptions as h, type ConditionOperatorFn as i, type CycleTimeStats as j, type IdempotencyKeyFn as k, type OutOfOfficeProvider as l, type OverrideOptions as m, type PreviewResult as n, type ProvideInfoOptions as o, type RejectOptions as p, type RequestInfoOptions as q, type ResubmitOptions as r, type RetryPolicy as s, defaultIdGenerator as t, toComparableNumber as u, validateConditionExpression as v, weekendCalendar as w };
|
|
@@ -284,6 +284,27 @@ interface TransferResult {
|
|
|
284
284
|
/** True when nothing was written. */
|
|
285
285
|
dryRun: boolean;
|
|
286
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* What one approver currently owes a decision on.
|
|
289
|
+
*
|
|
290
|
+
* Durations are milliseconds. An approver appears only while they hold at least
|
|
291
|
+
* one open level.
|
|
292
|
+
*/
|
|
293
|
+
interface ApproverWorkload {
|
|
294
|
+
approverId: string;
|
|
295
|
+
/** Open levels assigned to them. One instance can contribute several across parallel branches. */
|
|
296
|
+
pending: number;
|
|
297
|
+
/** Distinct documents involved — usually, but not always, equal to {@link pending}. */
|
|
298
|
+
instances: number;
|
|
299
|
+
/** Open levels already past their escalation deadline. */
|
|
300
|
+
overdue: number;
|
|
301
|
+
/** Open levels currently paused by a clarification request. */
|
|
302
|
+
onHold: number;
|
|
303
|
+
/** When the oldest of their open items was submitted. */
|
|
304
|
+
oldestPendingAt?: Date;
|
|
305
|
+
/** Age of that oldest item. `0` when they hold nothing. */
|
|
306
|
+
oldestAgeMs: number;
|
|
307
|
+
}
|
|
287
308
|
interface ApprovalStatistics {
|
|
288
309
|
/** Total instances matching the filter (across all statuses). */
|
|
289
310
|
total: number;
|
|
@@ -607,6 +628,27 @@ declare class ApprovalEngine {
|
|
|
607
628
|
* submittedBy, date range) — `status` is ignored since every status is counted.
|
|
608
629
|
* Adapter-agnostic: issues one cheap count query per status plus an overdue scan.
|
|
609
630
|
*/
|
|
631
|
+
/**
|
|
632
|
+
* Who currently owes a decision, and how overdue they are.
|
|
633
|
+
*
|
|
634
|
+
* `getStatistics()` answers how the tenant is doing; this answers who is
|
|
635
|
+
* holding it up — the question behind rebalancing a queue, spotting the
|
|
636
|
+
* approver who has been on leave for a week, or deciding whom to
|
|
637
|
+
* {@link transferApprovals} a departing colleague's work to.
|
|
638
|
+
*
|
|
639
|
+
* Computed from pending instances rather than a dedicated index, so it works
|
|
640
|
+
* on any storage adapter with no new adapter methods. That means it reads
|
|
641
|
+
* every pending instance in the tenant: fine for the operational volumes an
|
|
642
|
+
* approval queue reaches, but it is a reporting call, not something to put on
|
|
643
|
+
* a hot path.
|
|
644
|
+
*
|
|
645
|
+
* Rows are sorted by {@link ApproverWorkload.pending} descending, so the
|
|
646
|
+
* busiest queue is first.
|
|
647
|
+
*
|
|
648
|
+
* @param filter - Optional scoping; `status` is ignored, since only pending work counts.
|
|
649
|
+
* @returns One row per approver holding at least one open level.
|
|
650
|
+
*/
|
|
651
|
+
getWorkload(filter?: Omit<InstanceFilter, 'status'>): Promise<ApproverWorkload[]>;
|
|
610
652
|
getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
|
|
611
653
|
shutdown(): Promise<void>;
|
|
612
654
|
/**
|
|
@@ -704,4 +746,4 @@ declare class ApprovalEngine {
|
|
|
704
746
|
private runExternalAudit;
|
|
705
747
|
}
|
|
706
748
|
|
|
707
|
-
export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type TransferResult as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type
|
|
749
|
+
export { type AddCommentOptions as A, type BulkResult as B, type CanApproveResult as C, type DelegateOptions as D, type EscalateOptions as E, type HealthResult as H, type IdGeneratorFn as I, type OrgProvider as O, type PreviewChainLevel as P, type ReassignOptions as R, type SubmitOptions as S, type TransferResult as T, type UpdateDataOptions as U, type ValidationResult as V, type WeekendCalendarOptions as W, ApprovalEngine as a, type ApprovalEngineOptions as b, type ApprovalStatistics as c, type ApproveOptions as d, type ApproverResolverFn as e, type ApproverWorkload as f, type BusinessCalendar as g, type CancelOptions as h, type ConditionOperatorFn as i, type CycleTimeStats as j, type IdempotencyKeyFn as k, type OutOfOfficeProvider as l, type OverrideOptions as m, type PreviewResult as n, type ProvideInfoOptions as o, type RejectOptions as p, type RequestInfoOptions as q, type ResubmitOptions as r, type RetryPolicy as s, defaultIdGenerator as t, toComparableNumber as u, validateConditionExpression as v, weekendCalendar as w };
|
package/dist/index.cjs
CHANGED
|
@@ -2914,6 +2914,66 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
2914
2914
|
* submittedBy, date range) — `status` is ignored since every status is counted.
|
|
2915
2915
|
* Adapter-agnostic: issues one cheap count query per status plus an overdue scan.
|
|
2916
2916
|
*/
|
|
2917
|
+
/**
|
|
2918
|
+
* Who currently owes a decision, and how overdue they are.
|
|
2919
|
+
*
|
|
2920
|
+
* `getStatistics()` answers how the tenant is doing; this answers who is
|
|
2921
|
+
* holding it up — the question behind rebalancing a queue, spotting the
|
|
2922
|
+
* approver who has been on leave for a week, or deciding whom to
|
|
2923
|
+
* {@link transferApprovals} a departing colleague's work to.
|
|
2924
|
+
*
|
|
2925
|
+
* Computed from pending instances rather than a dedicated index, so it works
|
|
2926
|
+
* on any storage adapter with no new adapter methods. That means it reads
|
|
2927
|
+
* every pending instance in the tenant: fine for the operational volumes an
|
|
2928
|
+
* approval queue reaches, but it is a reporting call, not something to put on
|
|
2929
|
+
* a hot path.
|
|
2930
|
+
*
|
|
2931
|
+
* Rows are sorted by {@link ApproverWorkload.pending} descending, so the
|
|
2932
|
+
* busiest queue is first.
|
|
2933
|
+
*
|
|
2934
|
+
* @param filter - Optional scoping; `status` is ignored, since only pending work counts.
|
|
2935
|
+
* @returns One row per approver holding at least one open level.
|
|
2936
|
+
*/
|
|
2937
|
+
async getWorkload(filter = {}) {
|
|
2938
|
+
const pending = await this.fetchAllByFilter({ ...filter, status: "pending" });
|
|
2939
|
+
const now = this.clock.now();
|
|
2940
|
+
const byApprover = /* @__PURE__ */ new Map();
|
|
2941
|
+
for (const instance of pending) {
|
|
2942
|
+
const submittedAt = new Date(instance.createdAt).getTime();
|
|
2943
|
+
const held = Boolean(instance.infoRequest);
|
|
2944
|
+
for (const level of instance.levels) {
|
|
2945
|
+
if (level.status !== "pending") continue;
|
|
2946
|
+
const isOverdue = level.escalationDueAt !== void 0 && new Date(level.escalationDueAt) <= now;
|
|
2947
|
+
for (const approverId of level.approverIds) {
|
|
2948
|
+
if (level.approvedBy.includes(approverId) || level.rejectedBy.includes(approverId)) {
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
const row = byApprover.get(approverId) ?? {
|
|
2952
|
+
pending: 0,
|
|
2953
|
+
instances: /* @__PURE__ */ new Set(),
|
|
2954
|
+
overdue: 0,
|
|
2955
|
+
onHold: 0,
|
|
2956
|
+
oldest: Number.POSITIVE_INFINITY
|
|
2957
|
+
};
|
|
2958
|
+
row.pending++;
|
|
2959
|
+
row.instances.add(instance.id);
|
|
2960
|
+
if (isOverdue) row.overdue++;
|
|
2961
|
+
if (held) row.onHold++;
|
|
2962
|
+
row.oldest = Math.min(row.oldest, submittedAt);
|
|
2963
|
+
byApprover.set(approverId, row);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
return [...byApprover.entries()].map(([approverId, row]) => ({
|
|
2968
|
+
approverId,
|
|
2969
|
+
pending: row.pending,
|
|
2970
|
+
instances: row.instances.size,
|
|
2971
|
+
overdue: row.overdue,
|
|
2972
|
+
onHold: row.onHold,
|
|
2973
|
+
oldestPendingAt: Number.isFinite(row.oldest) ? new Date(row.oldest) : void 0,
|
|
2974
|
+
oldestAgeMs: Number.isFinite(row.oldest) ? now.getTime() - row.oldest : 0
|
|
2975
|
+
})).sort((a, b) => b.pending - a.pending || a.approverId.localeCompare(b.approverId));
|
|
2976
|
+
}
|
|
2917
2977
|
async getStatistics(filter = {}) {
|
|
2918
2978
|
const statuses = [
|
|
2919
2979
|
"pending",
|