hierarchical-approval 4.0.1 → 4.0.2
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 +18 -5
- package/README.md +1 -1
- package/dist/{ApprovalEngine-BfKL6iGQ.d.ts → ApprovalEngine-Dqe-rC6K.d.ts} +188 -1
- package/dist/{ApprovalEngine-DcD6s_jB.d.cts → ApprovalEngine-XgGBQLC_.d.cts} +188 -1
- package/dist/adapters/MemoryAdapter.cjs.map +1 -1
- package/dist/adapters/MemoryAdapter.d.cts +18 -0
- package/dist/adapters/MemoryAdapter.d.ts +18 -0
- package/dist/adapters/MemoryAdapter.js.map +1 -1
- package/dist/adapters/PostgresAdapter.cjs.map +1 -1
- package/dist/adapters/PostgresAdapter.d.cts +16 -0
- package/dist/adapters/PostgresAdapter.d.ts +16 -0
- package/dist/adapters/PostgresAdapter.js.map +1 -1
- package/dist/index.cjs +238 -18
- 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 +238 -18
- package/dist/index.js.map +1 -1
- package/dist/nestjs.cjs +238 -18
- 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 +238 -18
- package/dist/nestjs.js.map +1 -1
- package/dist/plugins/notify.cjs +3 -1
- package/dist/plugins/notify.cjs.map +1 -1
- package/dist/plugins/notify.d.cts +2 -0
- package/dist/plugins/notify.d.ts +2 -0
- package/dist/plugins/notify.js +3 -1
- package/dist/plugins/notify.js.map +1 -1
- package/dist/testing.cjs +250 -18
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +22 -1
- package/dist/testing.d.ts +22 -1
- package/dist/testing.js +250 -18
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
## 4.0.
|
|
1
|
+
## 4.0.2 - 2026-09-20
|
|
2
2
|
|
|
3
|
-
- fix(
|
|
4
|
-
- fix(
|
|
5
|
-
- fix(
|
|
6
|
-
- fix(postgres): let FakePool pass an explicit null rowCount through
|
|
3
|
+
- fix(notify): name the offending value in DigestNotificationAdapter's error
|
|
4
|
+
- fix(engine): stop leaking a listener when on() double-registers it
|
|
5
|
+
- fix(webhook): give the real-timer sleep test slack for CI jitter
|
|
7
6
|
|
|
8
7
|
# Changelog
|
|
9
8
|
|
|
@@ -14,9 +13,23 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
14
13
|
|
|
15
14
|
### Fixed
|
|
16
15
|
|
|
16
|
+
- `EventBus` (and therefore `engine.on()`/`off()`) no longer leaks a listener
|
|
17
|
+
when the same function is registered twice with `on()`: each registration
|
|
18
|
+
now needs its own `off()` call to undo, instead of the second registration
|
|
19
|
+
silently overwriting the bookkeeping for the first and leaving it permanently
|
|
20
|
+
attached.
|
|
21
|
+
- `DigestNotificationAdapter`'s `intervalMs` validation error now names the
|
|
22
|
+
offending value, instead of a message that gave no clue which value failed.
|
|
23
|
+
|
|
24
|
+
## [4.0.1] - 2026-09-14
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
17
28
|
- `InMemorySchedulerAdapter.scheduleAt()`'s post-shutdown error now names the
|
|
18
29
|
rejected job id, so a caller can tell which scheduled job was dropped
|
|
19
30
|
without adding their own logging around every call.
|
|
31
|
+
- `RateLimitMiddleware` errors now include the offending value that tripped
|
|
32
|
+
the limit, instead of a message that named the limit but not the input.
|
|
20
33
|
|
|
21
34
|
## [4.0.0] - 2026-09-04
|
|
22
35
|
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Multi-tenant · audit-ready · fully pluggable · zero runtime dependencies you
|
|
|
11
11
|
[](https://www.typescriptlang.org/)
|
|
12
12
|
[](https://bundlephobia.com/package/hierarchical-approval)
|
|
13
13
|
[](./LICENSE)
|
|
14
|
-
[](./tests)
|
|
15
15
|
|
|
16
16
|
[**Documentation**](https://hierarchical-approval.matthewswong.com) ·
|
|
17
17
|
[**npm**](https://www.npmjs.com/package/hierarchical-approval) ·
|
|
@@ -625,18 +625,71 @@ declare class ApprovalEngine {
|
|
|
625
625
|
/** Set by {@link shutdown}; stops the self-rescheduling loop from scheduling another tick. */
|
|
626
626
|
private schedulerStopped;
|
|
627
627
|
constructor(opts: ApprovalEngineOptions);
|
|
628
|
+
/**
|
|
629
|
+
* Subscribe to a lifecycle event (see {@link ApprovalEventMap} for the full
|
|
630
|
+
* catalog and each payload's shape). A listener that throws, or returns a
|
|
631
|
+
* rejecting promise, cannot abort the operation that emitted the event or
|
|
632
|
+
* block the remaining listeners — the failure is reported via the engine's
|
|
633
|
+
* logger instead.
|
|
634
|
+
*
|
|
635
|
+
* @returns `this`, for chaining multiple subscriptions.
|
|
636
|
+
*/
|
|
628
637
|
on<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
638
|
+
/**
|
|
639
|
+
* Unsubscribe a listener previously registered with {@link on}. A no-op if
|
|
640
|
+
* `listener` was never registered for `event`.
|
|
641
|
+
*
|
|
642
|
+
* @returns `this`, for chaining.
|
|
643
|
+
*/
|
|
629
644
|
off<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
645
|
+
/**
|
|
646
|
+
* Register a dynamic-approver resolver under `name`, for use by a level whose
|
|
647
|
+
* approver config is `{ type: 'dynamic', resolver: name }`. Overwrites any
|
|
648
|
+
* resolver already registered under the same name.
|
|
649
|
+
*/
|
|
630
650
|
registerResolver(name: string, fn: ResolverFn): void;
|
|
651
|
+
/**
|
|
652
|
+
* Register a custom approver type under `typeName`, for use by a level whose
|
|
653
|
+
* approver config carries `{ type: typeName, ... }`. Overwrites any approver
|
|
654
|
+
* type already registered under the same type name.
|
|
655
|
+
*/
|
|
631
656
|
registerApproverType(typeName: string, fn: ApproverResolverFn): void;
|
|
657
|
+
/**
|
|
658
|
+
* Register a custom condition operator under `name`, for use by template
|
|
659
|
+
* condition rules whose `operator` is `name`. Operators are shared process-wide
|
|
660
|
+
* across every engine instance, not scoped to this tenant.
|
|
661
|
+
*
|
|
662
|
+
* @throws {@link ApprovalValidationError} if `name` is already taken by a
|
|
663
|
+
* built-in operator (`>`, `<`, `>=`, `<=`, `==`, `!=`, `in`, `not_in`).
|
|
664
|
+
*/
|
|
632
665
|
registerConditionOperator(name: string, fn: ConditionOperatorFn): void;
|
|
633
666
|
/** Validate a template config without persisting. Synchronous; never throws. */
|
|
634
667
|
validateTemplate(config: ApprovalTemplateConfig): ValidationResult;
|
|
635
668
|
defineTemplate(config: ApprovalTemplateConfig): Promise<string>;
|
|
636
669
|
/** Update an existing template, incrementing its version. In-flight instances are protected by their templateSnapshot. */
|
|
637
670
|
updateTemplate(config: ApprovalTemplateConfig): Promise<string>;
|
|
671
|
+
/**
|
|
672
|
+
* Fetch the current version of a defined template.
|
|
673
|
+
*
|
|
674
|
+
* @throws {@link ApprovalTemplateNotFoundError} if no template is registered under `name`.
|
|
675
|
+
*/
|
|
638
676
|
getTemplate(name: string): Promise<ApprovalTemplate>;
|
|
677
|
+
/** List every template currently defined for this tenant. */
|
|
639
678
|
listTemplates(): Promise<ApprovalTemplate[]>;
|
|
679
|
+
/**
|
|
680
|
+
* Start a new approval instance from a defined template.
|
|
681
|
+
*
|
|
682
|
+
* Evaluates the template's conditions against `raw.data` first, so the
|
|
683
|
+
* levels an instance opens with can differ from the template's static
|
|
684
|
+
* declaration (added, skipped, or reordered). If a prior call with the
|
|
685
|
+
* same tenant, document, template and data produced a non-terminal
|
|
686
|
+
* instance, that instance is returned unchanged instead of creating a
|
|
687
|
+
* duplicate — safe to retry on network failure.
|
|
688
|
+
*
|
|
689
|
+
* @throws {@link ApprovalTemplateNotFoundError} if `raw.templateName` is not defined.
|
|
690
|
+
* @throws {@link ApprovalValidationError} if `raw` fails schema validation, or if
|
|
691
|
+
* condition evaluation leaves no levels (or a duplicate level number) to open.
|
|
692
|
+
*/
|
|
640
693
|
submit(raw: SubmitOptions, auditCtx?: AuditContext,
|
|
641
694
|
/**
|
|
642
695
|
* @internal Set only when the engine spawns a sub-workflow child. Passed at
|
|
@@ -649,8 +702,63 @@ declare class ApprovalEngine {
|
|
|
649
702
|
parentLevel: number;
|
|
650
703
|
depth: number;
|
|
651
704
|
}): Promise<ApprovalInstance>;
|
|
705
|
+
/**
|
|
706
|
+
* Record `opts.approverId`'s approval of their open level on `instanceId`.
|
|
707
|
+
*
|
|
708
|
+
* When more than one level is open in parallel and the approver sits on
|
|
709
|
+
* exactly one of them, that level is inferred; pass `opts.level` explicitly
|
|
710
|
+
* when it must be disambiguated (e.g. the approver sits on several).
|
|
711
|
+
*
|
|
712
|
+
* @throws {@link ApprovalNotFoundError} if `instanceId` does not exist.
|
|
713
|
+
* @throws {@link ApprovalError} (code `INVALID_STATUS`) if the instance is not `pending`,
|
|
714
|
+
* (code `INVALID_LEVEL`) if no level (or the explicit `opts.level`) is awaiting a decision,
|
|
715
|
+
* or (code `ALREADY_ACTED`) if this approver already approved or rejected that level.
|
|
716
|
+
* @throws {@link ApprovalForbiddenError} if `opts.approverId` submitted the instance
|
|
717
|
+
* (self-approval), is not an approver on the resolved level, or is denied by the
|
|
718
|
+
* configured {@link IAuthorizationPolicy}.
|
|
719
|
+
* @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
|
|
720
|
+
* open parallel level and `opts.level` was not supplied to disambiguate.
|
|
721
|
+
*/
|
|
652
722
|
approve(instanceId: string, raw: ApproveOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
723
|
+
/**
|
|
724
|
+
* Record `opts.approverId`'s rejection of their open level on `instanceId`.
|
|
725
|
+
*
|
|
726
|
+
* A single rejection does not necessarily fail the level — under `'quorum'`
|
|
727
|
+
* or `'weighted'` mode it only fails once the remaining approvers can no
|
|
728
|
+
* longer reach threshold. When the level does fail, `opts.returnTo`
|
|
729
|
+
* controls what happens next: `'originator'` (default) fails the whole
|
|
730
|
+
* instance; `'previous'` resets every level after the prior one and sends
|
|
731
|
+
* the instance back there to replay.
|
|
732
|
+
*
|
|
733
|
+
* @throws {@link ApprovalNotFoundError} if `instanceId` does not exist.
|
|
734
|
+
* @throws {@link ApprovalError} (code `INVALID_STATUS`) if the instance is not `pending`,
|
|
735
|
+
* (code `INVALID_LEVEL`) if no level (or the explicit `opts.level`) is awaiting a decision,
|
|
736
|
+
* or (code `ALREADY_ACTED`) if this approver already approved or rejected that level.
|
|
737
|
+
* @throws {@link ApprovalForbiddenError} if `opts.approverId` submitted the instance
|
|
738
|
+
* (self-rejection), is not an approver on the resolved level, or is denied by the
|
|
739
|
+
* configured {@link IAuthorizationPolicy}.
|
|
740
|
+
* @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
|
|
741
|
+
* open parallel level with `opts.level` unset, or if `opts.returnTo === 'previous'`
|
|
742
|
+
* but the failing level is already the first level in the chain.
|
|
743
|
+
*/
|
|
653
744
|
reject(instanceId: string, raw: RejectOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
745
|
+
/**
|
|
746
|
+
* Reassign `opts.fromApprover`'s seat on their open level to `opts.toApprover`,
|
|
747
|
+
* carrying across any 'weighted'-mode vote weight. When `opts.until` is set,
|
|
748
|
+
* the level records `delegatedFrom`/`delegatedTo`/`delegatedUntil`, and the
|
|
749
|
+
* scheduled escalation tick reassigns the seat back to `opts.fromApprover`
|
|
750
|
+
* once that time passes (still pending); omit `opts.until` for a permanent
|
|
751
|
+
* handoff.
|
|
752
|
+
*
|
|
753
|
+
* @throws {@link ApprovalNotFoundError} if `instanceId` does not exist.
|
|
754
|
+
* @throws {@link ApprovalError} (code `INVALID_STATUS`) if the instance is not `pending`,
|
|
755
|
+
* or (code `INVALID_LEVEL`) if no level (or the explicit `opts.level`) is awaiting a decision.
|
|
756
|
+
* @throws {@link ApprovalForbiddenError} if `opts.fromApprover` equals `opts.toApprover`,
|
|
757
|
+
* is not an approver on the resolved level, already acted on it, or is denied by the
|
|
758
|
+
* configured {@link IAuthorizationPolicy}.
|
|
759
|
+
* @throws {@link ApprovalValidationError} if `opts.fromApprover` sits on more than one
|
|
760
|
+
* open parallel level and `opts.level` was not supplied to disambiguate.
|
|
761
|
+
*/
|
|
654
762
|
delegate(instanceId: string, raw: DelegateOptions, auditCtx?: AuditContext): Promise<void>;
|
|
655
763
|
/**
|
|
656
764
|
* Administratively replace an approver on the current level. Unlike delegate(),
|
|
@@ -660,7 +768,36 @@ declare class ApprovalEngine {
|
|
|
660
768
|
* cannot be reassigned.
|
|
661
769
|
*/
|
|
662
770
|
reassign(instanceId: string, raw: ReassignOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
771
|
+
/**
|
|
772
|
+
* Withdraw an approval before it reaches a final decision.
|
|
773
|
+
*
|
|
774
|
+
* Cancelling cascades: any sub-workflow children spawned by this instance
|
|
775
|
+
* are cancelled in turn (an orphaned child left running would eventually
|
|
776
|
+
* report back to a parent that already moved on), and if this instance is
|
|
777
|
+
* itself a sub-workflow child, its own parent is unblocked as if this
|
|
778
|
+
* approval had been rejected.
|
|
779
|
+
*
|
|
780
|
+
* @param instanceId - The instance to cancel.
|
|
781
|
+
* @param raw - Who is cancelling and why.
|
|
782
|
+
* @param auditCtx - Optional compliance context recorded on the audit entry.
|
|
783
|
+
* @returns The cancelled instance.
|
|
784
|
+
* @throws ApprovalError if the instance is already `approved` or `rejected`.
|
|
785
|
+
*/
|
|
663
786
|
cancel(instanceId: string, raw: CancelOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
787
|
+
/**
|
|
788
|
+
* Manually escalate the instance's current (lowest pending) level to the
|
|
789
|
+
* next rung of its escalation ladder, adding whatever the rung's
|
|
790
|
+
* `escalateTo` resolves to as extra approvers on that level. This is the
|
|
791
|
+
* same path the scheduled tick uses for a deadline-driven escalation — call
|
|
792
|
+
* it directly to escalate on demand instead of waiting for the ladder timer.
|
|
793
|
+
*
|
|
794
|
+
* A no-op (returns the instance unchanged, does not throw) when: the
|
|
795
|
+
* instance is not `pending`; the ladder has no rung configured for the
|
|
796
|
+
* current step; or every resolved approver turns out to be the submitter
|
|
797
|
+
* (nobody new to add).
|
|
798
|
+
*
|
|
799
|
+
* @throws {@link ApprovalNotFoundError} if `instanceId` does not exist.
|
|
800
|
+
*/
|
|
664
801
|
escalate(instanceId: string, raw: EscalateOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
665
802
|
/**
|
|
666
803
|
* Change an instance's document data while it is still pending, and
|
|
@@ -822,6 +959,17 @@ declare class ApprovalEngine {
|
|
|
822
959
|
submittedBy: string;
|
|
823
960
|
decisions?: SimulatedDecision[];
|
|
824
961
|
}): Promise<SimulationResult>;
|
|
962
|
+
/**
|
|
963
|
+
* Check whether a user could approve an instance right now, without acting.
|
|
964
|
+
*
|
|
965
|
+
* The read-only counterpart to calling {@link approve} and handling the
|
|
966
|
+
* rejection — useful for deciding whether to show an approve button at all.
|
|
967
|
+
* `eligible: false` never throws; the reason names why: the instance is not
|
|
968
|
+
* `pending`, the user submitted it, they hold no open level (or delegated
|
|
969
|
+
* theirs away), or they already voted on it.
|
|
970
|
+
*
|
|
971
|
+
* @returns `{ eligible: true }`, or `{ eligible: false, reason }`.
|
|
972
|
+
*/
|
|
825
973
|
canApprove(instanceId: string, userId: string): Promise<CanApproveResult>;
|
|
826
974
|
/** Emergency bypass — completes the instance as 'approved', skipping remaining levels. Requires template.allowOverride = true. */
|
|
827
975
|
override(instanceId: string, raw: OverrideOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
@@ -848,13 +996,34 @@ declare class ApprovalEngine {
|
|
|
848
996
|
* @returns What moved, what did not, and why. With `dryRun` nothing is written.
|
|
849
997
|
*/
|
|
850
998
|
transferApprovals(raw: TransferApprovalsOptions, auditCtx?: AuditContext): Promise<TransferResult>;
|
|
999
|
+
/** Approve multiple instances in one call. Never throws — failures collected in result.failed. */
|
|
851
1000
|
bulkApprove(instanceIds: string[], raw: ApproveOptions, auditCtx?: AuditContext): Promise<BulkResult>;
|
|
852
1001
|
/** Reject multiple instances in one call. Never throws — failures collected in result.failed. */
|
|
853
1002
|
bulkReject(instanceIds: string[], raw: RejectOptions, auditCtx?: AuditContext): Promise<BulkResult>;
|
|
1003
|
+
/**
|
|
1004
|
+
* Fetch a single instance by id.
|
|
1005
|
+
*
|
|
1006
|
+
* @throws {@link ApprovalNotFoundError} if no instance exists with that id.
|
|
1007
|
+
*/
|
|
854
1008
|
getInstance(instanceId: string): Promise<ApprovalInstance>;
|
|
1009
|
+
/** Page through every instance with an open level assigned to one approver. */
|
|
855
1010
|
getPendingFor(approverId: string, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
1011
|
+
/** Page through instances matching an arbitrary filter (status, document type, date range, ...). */
|
|
856
1012
|
queryInstances(filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
1013
|
+
/**
|
|
1014
|
+
* Cursor-paginated variant of {@link queryInstances}, for stable paging
|
|
1015
|
+
* through large or actively-changing result sets that offset pagination
|
|
1016
|
+
* would skip or duplicate rows in.
|
|
1017
|
+
*
|
|
1018
|
+
* @throws {@link ApprovalError} (`NOT_SUPPORTED`) if the configured storage
|
|
1019
|
+
* adapter does not implement `getInstancesByCursor`.
|
|
1020
|
+
*/
|
|
857
1021
|
queryInstancesByCursor(filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
|
|
1022
|
+
/**
|
|
1023
|
+
* The full audit trail for an instance, in the order it was recorded.
|
|
1024
|
+
*
|
|
1025
|
+
* @throws {@link ApprovalNotFoundError} if no instance exists with that id.
|
|
1026
|
+
*/
|
|
858
1027
|
getHistory(instanceId: string): Promise<AuditEntry[]>;
|
|
859
1028
|
/**
|
|
860
1029
|
* The levels currently collecting decisions, ascending.
|
|
@@ -867,6 +1036,16 @@ declare class ApprovalEngine {
|
|
|
867
1036
|
* Empty once the instance is terminal.
|
|
868
1037
|
*/
|
|
869
1038
|
getOpenLevels(instanceId: string): Promise<number[]>;
|
|
1039
|
+
/**
|
|
1040
|
+
* Everyone who currently owes a decision on an instance.
|
|
1041
|
+
*
|
|
1042
|
+
* A union across every open branch, so a parallel group with more than one
|
|
1043
|
+
* level collecting decisions at once still names every approver, not just
|
|
1044
|
+
* the one at {@link ApprovalInstance.currentLevel}. Empty once the instance
|
|
1045
|
+
* is no longer `pending`.
|
|
1046
|
+
*
|
|
1047
|
+
* @throws {@link ApprovalNotFoundError} if no instance exists with that id.
|
|
1048
|
+
*/
|
|
870
1049
|
getCurrentApprovers(instanceId: string): Promise<string[]>;
|
|
871
1050
|
/** Check adapter connectivity and escalation scheduler health. */
|
|
872
1051
|
healthCheck(): Promise<HealthResult>;
|
|
@@ -956,6 +1135,15 @@ declare class ApprovalEngine {
|
|
|
956
1135
|
limit?: number;
|
|
957
1136
|
dryRun?: boolean;
|
|
958
1137
|
}): Promise<PurgeResult>;
|
|
1138
|
+
/**
|
|
1139
|
+
* Aggregate reporting numbers for a tenant: counts by status, approval
|
|
1140
|
+
* rate, cycle-time stats, and a per-template breakdown.
|
|
1141
|
+
*
|
|
1142
|
+
* `filter.status` is ignored — the breakdown covers every status by
|
|
1143
|
+
* design, so scoping it to one would make the counts misleading.
|
|
1144
|
+
*
|
|
1145
|
+
* @param filter - Optional scoping by document type, submitter, or date range.
|
|
1146
|
+
*/
|
|
959
1147
|
getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
|
|
960
1148
|
/** Stops the escalation scheduler and any scheduler-adapter background loop; call once when tearing the engine down. */
|
|
961
1149
|
shutdown(): Promise<void>;
|
|
@@ -1206,7 +1394,6 @@ declare class ApprovalEngine {
|
|
|
1206
1394
|
* approved document that one branch had rejected and another had never seen.
|
|
1207
1395
|
*/
|
|
1208
1396
|
private assertFullyApproved;
|
|
1209
|
-
private findNextLevel;
|
|
1210
1397
|
/**
|
|
1211
1398
|
* The level to send an instance back to, given the level being rejected.
|
|
1212
1399
|
*
|
|
@@ -625,18 +625,71 @@ declare class ApprovalEngine {
|
|
|
625
625
|
/** Set by {@link shutdown}; stops the self-rescheduling loop from scheduling another tick. */
|
|
626
626
|
private schedulerStopped;
|
|
627
627
|
constructor(opts: ApprovalEngineOptions);
|
|
628
|
+
/**
|
|
629
|
+
* Subscribe to a lifecycle event (see {@link ApprovalEventMap} for the full
|
|
630
|
+
* catalog and each payload's shape). A listener that throws, or returns a
|
|
631
|
+
* rejecting promise, cannot abort the operation that emitted the event or
|
|
632
|
+
* block the remaining listeners — the failure is reported via the engine's
|
|
633
|
+
* logger instead.
|
|
634
|
+
*
|
|
635
|
+
* @returns `this`, for chaining multiple subscriptions.
|
|
636
|
+
*/
|
|
628
637
|
on<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
638
|
+
/**
|
|
639
|
+
* Unsubscribe a listener previously registered with {@link on}. A no-op if
|
|
640
|
+
* `listener` was never registered for `event`.
|
|
641
|
+
*
|
|
642
|
+
* @returns `this`, for chaining.
|
|
643
|
+
*/
|
|
629
644
|
off<K extends ApprovalEventName>(event: K, listener: (payload: ApprovalEventMap[K]) => void): this;
|
|
645
|
+
/**
|
|
646
|
+
* Register a dynamic-approver resolver under `name`, for use by a level whose
|
|
647
|
+
* approver config is `{ type: 'dynamic', resolver: name }`. Overwrites any
|
|
648
|
+
* resolver already registered under the same name.
|
|
649
|
+
*/
|
|
630
650
|
registerResolver(name: string, fn: ResolverFn): void;
|
|
651
|
+
/**
|
|
652
|
+
* Register a custom approver type under `typeName`, for use by a level whose
|
|
653
|
+
* approver config carries `{ type: typeName, ... }`. Overwrites any approver
|
|
654
|
+
* type already registered under the same type name.
|
|
655
|
+
*/
|
|
631
656
|
registerApproverType(typeName: string, fn: ApproverResolverFn): void;
|
|
657
|
+
/**
|
|
658
|
+
* Register a custom condition operator under `name`, for use by template
|
|
659
|
+
* condition rules whose `operator` is `name`. Operators are shared process-wide
|
|
660
|
+
* across every engine instance, not scoped to this tenant.
|
|
661
|
+
*
|
|
662
|
+
* @throws {@link ApprovalValidationError} if `name` is already taken by a
|
|
663
|
+
* built-in operator (`>`, `<`, `>=`, `<=`, `==`, `!=`, `in`, `not_in`).
|
|
664
|
+
*/
|
|
632
665
|
registerConditionOperator(name: string, fn: ConditionOperatorFn): void;
|
|
633
666
|
/** Validate a template config without persisting. Synchronous; never throws. */
|
|
634
667
|
validateTemplate(config: ApprovalTemplateConfig): ValidationResult;
|
|
635
668
|
defineTemplate(config: ApprovalTemplateConfig): Promise<string>;
|
|
636
669
|
/** Update an existing template, incrementing its version. In-flight instances are protected by their templateSnapshot. */
|
|
637
670
|
updateTemplate(config: ApprovalTemplateConfig): Promise<string>;
|
|
671
|
+
/**
|
|
672
|
+
* Fetch the current version of a defined template.
|
|
673
|
+
*
|
|
674
|
+
* @throws {@link ApprovalTemplateNotFoundError} if no template is registered under `name`.
|
|
675
|
+
*/
|
|
638
676
|
getTemplate(name: string): Promise<ApprovalTemplate>;
|
|
677
|
+
/** List every template currently defined for this tenant. */
|
|
639
678
|
listTemplates(): Promise<ApprovalTemplate[]>;
|
|
679
|
+
/**
|
|
680
|
+
* Start a new approval instance from a defined template.
|
|
681
|
+
*
|
|
682
|
+
* Evaluates the template's conditions against `raw.data` first, so the
|
|
683
|
+
* levels an instance opens with can differ from the template's static
|
|
684
|
+
* declaration (added, skipped, or reordered). If a prior call with the
|
|
685
|
+
* same tenant, document, template and data produced a non-terminal
|
|
686
|
+
* instance, that instance is returned unchanged instead of creating a
|
|
687
|
+
* duplicate — safe to retry on network failure.
|
|
688
|
+
*
|
|
689
|
+
* @throws {@link ApprovalTemplateNotFoundError} if `raw.templateName` is not defined.
|
|
690
|
+
* @throws {@link ApprovalValidationError} if `raw` fails schema validation, or if
|
|
691
|
+
* condition evaluation leaves no levels (or a duplicate level number) to open.
|
|
692
|
+
*/
|
|
640
693
|
submit(raw: SubmitOptions, auditCtx?: AuditContext,
|
|
641
694
|
/**
|
|
642
695
|
* @internal Set only when the engine spawns a sub-workflow child. Passed at
|
|
@@ -649,8 +702,63 @@ declare class ApprovalEngine {
|
|
|
649
702
|
parentLevel: number;
|
|
650
703
|
depth: number;
|
|
651
704
|
}): Promise<ApprovalInstance>;
|
|
705
|
+
/**
|
|
706
|
+
* Record `opts.approverId`'s approval of their open level on `instanceId`.
|
|
707
|
+
*
|
|
708
|
+
* When more than one level is open in parallel and the approver sits on
|
|
709
|
+
* exactly one of them, that level is inferred; pass `opts.level` explicitly
|
|
710
|
+
* when it must be disambiguated (e.g. the approver sits on several).
|
|
711
|
+
*
|
|
712
|
+
* @throws {@link ApprovalNotFoundError} if `instanceId` does not exist.
|
|
713
|
+
* @throws {@link ApprovalError} (code `INVALID_STATUS`) if the instance is not `pending`,
|
|
714
|
+
* (code `INVALID_LEVEL`) if no level (or the explicit `opts.level`) is awaiting a decision,
|
|
715
|
+
* or (code `ALREADY_ACTED`) if this approver already approved or rejected that level.
|
|
716
|
+
* @throws {@link ApprovalForbiddenError} if `opts.approverId` submitted the instance
|
|
717
|
+
* (self-approval), is not an approver on the resolved level, or is denied by the
|
|
718
|
+
* configured {@link IAuthorizationPolicy}.
|
|
719
|
+
* @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
|
|
720
|
+
* open parallel level and `opts.level` was not supplied to disambiguate.
|
|
721
|
+
*/
|
|
652
722
|
approve(instanceId: string, raw: ApproveOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
723
|
+
/**
|
|
724
|
+
* Record `opts.approverId`'s rejection of their open level on `instanceId`.
|
|
725
|
+
*
|
|
726
|
+
* A single rejection does not necessarily fail the level — under `'quorum'`
|
|
727
|
+
* or `'weighted'` mode it only fails once the remaining approvers can no
|
|
728
|
+
* longer reach threshold. When the level does fail, `opts.returnTo`
|
|
729
|
+
* controls what happens next: `'originator'` (default) fails the whole
|
|
730
|
+
* instance; `'previous'` resets every level after the prior one and sends
|
|
731
|
+
* the instance back there to replay.
|
|
732
|
+
*
|
|
733
|
+
* @throws {@link ApprovalNotFoundError} if `instanceId` does not exist.
|
|
734
|
+
* @throws {@link ApprovalError} (code `INVALID_STATUS`) if the instance is not `pending`,
|
|
735
|
+
* (code `INVALID_LEVEL`) if no level (or the explicit `opts.level`) is awaiting a decision,
|
|
736
|
+
* or (code `ALREADY_ACTED`) if this approver already approved or rejected that level.
|
|
737
|
+
* @throws {@link ApprovalForbiddenError} if `opts.approverId` submitted the instance
|
|
738
|
+
* (self-rejection), is not an approver on the resolved level, or is denied by the
|
|
739
|
+
* configured {@link IAuthorizationPolicy}.
|
|
740
|
+
* @throws {@link ApprovalValidationError} if `opts.approverId` sits on more than one
|
|
741
|
+
* open parallel level with `opts.level` unset, or if `opts.returnTo === 'previous'`
|
|
742
|
+
* but the failing level is already the first level in the chain.
|
|
743
|
+
*/
|
|
653
744
|
reject(instanceId: string, raw: RejectOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
745
|
+
/**
|
|
746
|
+
* Reassign `opts.fromApprover`'s seat on their open level to `opts.toApprover`,
|
|
747
|
+
* carrying across any 'weighted'-mode vote weight. When `opts.until` is set,
|
|
748
|
+
* the level records `delegatedFrom`/`delegatedTo`/`delegatedUntil`, and the
|
|
749
|
+
* scheduled escalation tick reassigns the seat back to `opts.fromApprover`
|
|
750
|
+
* once that time passes (still pending); omit `opts.until` for a permanent
|
|
751
|
+
* handoff.
|
|
752
|
+
*
|
|
753
|
+
* @throws {@link ApprovalNotFoundError} if `instanceId` does not exist.
|
|
754
|
+
* @throws {@link ApprovalError} (code `INVALID_STATUS`) if the instance is not `pending`,
|
|
755
|
+
* or (code `INVALID_LEVEL`) if no level (or the explicit `opts.level`) is awaiting a decision.
|
|
756
|
+
* @throws {@link ApprovalForbiddenError} if `opts.fromApprover` equals `opts.toApprover`,
|
|
757
|
+
* is not an approver on the resolved level, already acted on it, or is denied by the
|
|
758
|
+
* configured {@link IAuthorizationPolicy}.
|
|
759
|
+
* @throws {@link ApprovalValidationError} if `opts.fromApprover` sits on more than one
|
|
760
|
+
* open parallel level and `opts.level` was not supplied to disambiguate.
|
|
761
|
+
*/
|
|
654
762
|
delegate(instanceId: string, raw: DelegateOptions, auditCtx?: AuditContext): Promise<void>;
|
|
655
763
|
/**
|
|
656
764
|
* Administratively replace an approver on the current level. Unlike delegate(),
|
|
@@ -660,7 +768,36 @@ declare class ApprovalEngine {
|
|
|
660
768
|
* cannot be reassigned.
|
|
661
769
|
*/
|
|
662
770
|
reassign(instanceId: string, raw: ReassignOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
771
|
+
/**
|
|
772
|
+
* Withdraw an approval before it reaches a final decision.
|
|
773
|
+
*
|
|
774
|
+
* Cancelling cascades: any sub-workflow children spawned by this instance
|
|
775
|
+
* are cancelled in turn (an orphaned child left running would eventually
|
|
776
|
+
* report back to a parent that already moved on), and if this instance is
|
|
777
|
+
* itself a sub-workflow child, its own parent is unblocked as if this
|
|
778
|
+
* approval had been rejected.
|
|
779
|
+
*
|
|
780
|
+
* @param instanceId - The instance to cancel.
|
|
781
|
+
* @param raw - Who is cancelling and why.
|
|
782
|
+
* @param auditCtx - Optional compliance context recorded on the audit entry.
|
|
783
|
+
* @returns The cancelled instance.
|
|
784
|
+
* @throws ApprovalError if the instance is already `approved` or `rejected`.
|
|
785
|
+
*/
|
|
663
786
|
cancel(instanceId: string, raw: CancelOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
787
|
+
/**
|
|
788
|
+
* Manually escalate the instance's current (lowest pending) level to the
|
|
789
|
+
* next rung of its escalation ladder, adding whatever the rung's
|
|
790
|
+
* `escalateTo` resolves to as extra approvers on that level. This is the
|
|
791
|
+
* same path the scheduled tick uses for a deadline-driven escalation — call
|
|
792
|
+
* it directly to escalate on demand instead of waiting for the ladder timer.
|
|
793
|
+
*
|
|
794
|
+
* A no-op (returns the instance unchanged, does not throw) when: the
|
|
795
|
+
* instance is not `pending`; the ladder has no rung configured for the
|
|
796
|
+
* current step; or every resolved approver turns out to be the submitter
|
|
797
|
+
* (nobody new to add).
|
|
798
|
+
*
|
|
799
|
+
* @throws {@link ApprovalNotFoundError} if `instanceId` does not exist.
|
|
800
|
+
*/
|
|
664
801
|
escalate(instanceId: string, raw: EscalateOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
665
802
|
/**
|
|
666
803
|
* Change an instance's document data while it is still pending, and
|
|
@@ -822,6 +959,17 @@ declare class ApprovalEngine {
|
|
|
822
959
|
submittedBy: string;
|
|
823
960
|
decisions?: SimulatedDecision[];
|
|
824
961
|
}): Promise<SimulationResult>;
|
|
962
|
+
/**
|
|
963
|
+
* Check whether a user could approve an instance right now, without acting.
|
|
964
|
+
*
|
|
965
|
+
* The read-only counterpart to calling {@link approve} and handling the
|
|
966
|
+
* rejection — useful for deciding whether to show an approve button at all.
|
|
967
|
+
* `eligible: false` never throws; the reason names why: the instance is not
|
|
968
|
+
* `pending`, the user submitted it, they hold no open level (or delegated
|
|
969
|
+
* theirs away), or they already voted on it.
|
|
970
|
+
*
|
|
971
|
+
* @returns `{ eligible: true }`, or `{ eligible: false, reason }`.
|
|
972
|
+
*/
|
|
825
973
|
canApprove(instanceId: string, userId: string): Promise<CanApproveResult>;
|
|
826
974
|
/** Emergency bypass — completes the instance as 'approved', skipping remaining levels. Requires template.allowOverride = true. */
|
|
827
975
|
override(instanceId: string, raw: OverrideOptions, auditCtx?: AuditContext): Promise<ApprovalInstance>;
|
|
@@ -848,13 +996,34 @@ declare class ApprovalEngine {
|
|
|
848
996
|
* @returns What moved, what did not, and why. With `dryRun` nothing is written.
|
|
849
997
|
*/
|
|
850
998
|
transferApprovals(raw: TransferApprovalsOptions, auditCtx?: AuditContext): Promise<TransferResult>;
|
|
999
|
+
/** Approve multiple instances in one call. Never throws — failures collected in result.failed. */
|
|
851
1000
|
bulkApprove(instanceIds: string[], raw: ApproveOptions, auditCtx?: AuditContext): Promise<BulkResult>;
|
|
852
1001
|
/** Reject multiple instances in one call. Never throws — failures collected in result.failed. */
|
|
853
1002
|
bulkReject(instanceIds: string[], raw: RejectOptions, auditCtx?: AuditContext): Promise<BulkResult>;
|
|
1003
|
+
/**
|
|
1004
|
+
* Fetch a single instance by id.
|
|
1005
|
+
*
|
|
1006
|
+
* @throws {@link ApprovalNotFoundError} if no instance exists with that id.
|
|
1007
|
+
*/
|
|
854
1008
|
getInstance(instanceId: string): Promise<ApprovalInstance>;
|
|
1009
|
+
/** Page through every instance with an open level assigned to one approver. */
|
|
855
1010
|
getPendingFor(approverId: string, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
1011
|
+
/** Page through instances matching an arbitrary filter (status, document type, date range, ...). */
|
|
856
1012
|
queryInstances(filter: InstanceFilter, opts?: PaginationOpts): Promise<PaginatedResult<ApprovalInstance>>;
|
|
1013
|
+
/**
|
|
1014
|
+
* Cursor-paginated variant of {@link queryInstances}, for stable paging
|
|
1015
|
+
* through large or actively-changing result sets that offset pagination
|
|
1016
|
+
* would skip or duplicate rows in.
|
|
1017
|
+
*
|
|
1018
|
+
* @throws {@link ApprovalError} (`NOT_SUPPORTED`) if the configured storage
|
|
1019
|
+
* adapter does not implement `getInstancesByCursor`.
|
|
1020
|
+
*/
|
|
857
1021
|
queryInstancesByCursor(filter: InstanceFilter, opts: CursorPaginationOpts): Promise<CursorPaginatedResult<ApprovalInstance>>;
|
|
1022
|
+
/**
|
|
1023
|
+
* The full audit trail for an instance, in the order it was recorded.
|
|
1024
|
+
*
|
|
1025
|
+
* @throws {@link ApprovalNotFoundError} if no instance exists with that id.
|
|
1026
|
+
*/
|
|
858
1027
|
getHistory(instanceId: string): Promise<AuditEntry[]>;
|
|
859
1028
|
/**
|
|
860
1029
|
* The levels currently collecting decisions, ascending.
|
|
@@ -867,6 +1036,16 @@ declare class ApprovalEngine {
|
|
|
867
1036
|
* Empty once the instance is terminal.
|
|
868
1037
|
*/
|
|
869
1038
|
getOpenLevels(instanceId: string): Promise<number[]>;
|
|
1039
|
+
/**
|
|
1040
|
+
* Everyone who currently owes a decision on an instance.
|
|
1041
|
+
*
|
|
1042
|
+
* A union across every open branch, so a parallel group with more than one
|
|
1043
|
+
* level collecting decisions at once still names every approver, not just
|
|
1044
|
+
* the one at {@link ApprovalInstance.currentLevel}. Empty once the instance
|
|
1045
|
+
* is no longer `pending`.
|
|
1046
|
+
*
|
|
1047
|
+
* @throws {@link ApprovalNotFoundError} if no instance exists with that id.
|
|
1048
|
+
*/
|
|
870
1049
|
getCurrentApprovers(instanceId: string): Promise<string[]>;
|
|
871
1050
|
/** Check adapter connectivity and escalation scheduler health. */
|
|
872
1051
|
healthCheck(): Promise<HealthResult>;
|
|
@@ -956,6 +1135,15 @@ declare class ApprovalEngine {
|
|
|
956
1135
|
limit?: number;
|
|
957
1136
|
dryRun?: boolean;
|
|
958
1137
|
}): Promise<PurgeResult>;
|
|
1138
|
+
/**
|
|
1139
|
+
* Aggregate reporting numbers for a tenant: counts by status, approval
|
|
1140
|
+
* rate, cycle-time stats, and a per-template breakdown.
|
|
1141
|
+
*
|
|
1142
|
+
* `filter.status` is ignored — the breakdown covers every status by
|
|
1143
|
+
* design, so scoping it to one would make the counts misleading.
|
|
1144
|
+
*
|
|
1145
|
+
* @param filter - Optional scoping by document type, submitter, or date range.
|
|
1146
|
+
*/
|
|
959
1147
|
getStatistics(filter?: Omit<InstanceFilter, 'status'>): Promise<ApprovalStatistics>;
|
|
960
1148
|
/** Stops the escalation scheduler and any scheduler-adapter background loop; call once when tearing the engine down. */
|
|
961
1149
|
shutdown(): Promise<void>;
|
|
@@ -1206,7 +1394,6 @@ declare class ApprovalEngine {
|
|
|
1206
1394
|
* approved document that one branch had rejected and another had never seen.
|
|
1207
1395
|
*/
|
|
1208
1396
|
private assertFullyApproved;
|
|
1209
|
-
private findNextLevel;
|
|
1210
1397
|
/**
|
|
1211
1398
|
* The level to send an instance back to, given the level being rejected.
|
|
1212
1399
|
*
|