hierarchical-approval 1.1.0 → 1.3.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 +75 -0
- package/README.md +61 -0
- package/dist/{ApprovalEngine-qwImBe9A.d.cts → ApprovalEngine-CfySjeuy.d.cts} +29 -7
- package/dist/{ApprovalEngine-BDnQwKi9.d.ts → ApprovalEngine-Dhu5mlCG.d.ts} +29 -7
- package/dist/{IAuditAdapter-CWdZxS3U.d.cts → IAuditAdapter-B2I3CjaJ.d.cts} +1 -1
- package/dist/{IAuditAdapter-99JBU3Db.d.ts → IAuditAdapter-GixC9zS1.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-U-jZrl2F.d.ts → IAuthorizationPolicy-4ge9Gnxj.d.ts} +1 -1
- package/dist/{IAuthorizationPolicy-ChZIUJhQ.d.cts → IAuthorizationPolicy-BHDZK9QL.d.cts} +1 -1
- package/dist/{INotificationAdapter-BKwhxhe2.d.cts → INotificationAdapter-BKMeGmaG.d.cts} +1 -1
- package/dist/{INotificationAdapter-CsSWiCZE.d.ts → INotificationAdapter-BmSeB7FZ.d.ts} +1 -1
- package/dist/{IOperationMiddleware-D162RiJs.d.cts → IOperationMiddleware-BLw9Oc-S.d.cts} +1 -1
- package/dist/{IOperationMiddleware-CPkGSVfr.d.ts → IOperationMiddleware-BW9t8wPn.d.ts} +1 -1
- package/dist/{IStorageAdapter-BZarNxs9.d.cts → IStorageAdapter-CKV_FZFV.d.cts} +1 -1
- package/dist/{IStorageAdapter-DrY-rhhF.d.ts → IStorageAdapter-DQJmR6Mt.d.ts} +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 +126 -14
- 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 +126 -14
- package/dist/index.js.map +1 -1
- package/dist/{instance-DuEgSIIz.d.cts → instance-BsM0DUPf.d.cts} +15 -0
- package/dist/{instance-DuEgSIIz.d.ts → instance-BsM0DUPf.d.ts} +15 -0
- package/dist/nestjs.cjs +126 -14
- 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 +126 -14
- 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 +126 -14
- 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 +126 -14
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,81 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
_Nothing yet._
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added — out-of-office cover
|
|
13
|
+
|
|
14
|
+
- **`outOfOfficeProvider` swaps absent approvers for their stand-in.** An
|
|
15
|
+
approver on leave stalled the chain until somebody noticed and reassigned by
|
|
16
|
+
hand; `delegate()` needed the absent person to initiate it, and `reassign()`
|
|
17
|
+
needed an administrator to spot the problem first.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
new ApprovalEngine({
|
|
21
|
+
adapter,
|
|
22
|
+
outOfOfficeProvider: {
|
|
23
|
+
getDelegateFor: async (userId, at) => hr.coverFor(userId, at), // null when available
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Applied wherever approvers are resolved — at submit, when a level activates,
|
|
29
|
+
on escalation, and in `previewApprovalChain()`, so a preview cannot disagree
|
|
30
|
+
with what `submit()` goes on to do.
|
|
31
|
+
|
|
32
|
+
Kept as an injected provider rather than engine-owned state because absence
|
|
33
|
+
lives in the HR or directory system that already tracks leave; storing it here
|
|
34
|
+
too would guarantee the two disagree. The resolution time is passed to the
|
|
35
|
+
provider, so cover can be date-bound.
|
|
36
|
+
|
|
37
|
+
- **Substitution is transitive but bounded.** An A→B→C chain of absences lands
|
|
38
|
+
on whoever is actually present, up to five hops. A cover *cycle* (A covers B
|
|
39
|
+
while B covers A) stops and leaves the original approver assigned — visible and
|
|
40
|
+
fixable, unlike a hang. A provider that throws is treated as "no cover known",
|
|
41
|
+
because an HR lookup failing must not stop an approval being routed at all.
|
|
42
|
+
|
|
43
|
+
New export: `OutOfOfficeProvider`.
|
|
44
|
+
|
|
45
|
+
## [1.2.0] - 2026-09-04
|
|
46
|
+
|
|
47
|
+
### Added — template inheritance
|
|
48
|
+
|
|
49
|
+
- **`extends` derives one template from another.** ERP tenants run many
|
|
50
|
+
near-identical workflows — one per region, legal entity or document class —
|
|
51
|
+
that share a spine and differ in a level or two. Each had to be written out in
|
|
52
|
+
full, so a change to the shared part meant editing every copy and hoping none
|
|
53
|
+
were missed.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
await engine.defineTemplate({
|
|
57
|
+
name: 'PO-EU',
|
|
58
|
+
extends: 'PO-base',
|
|
59
|
+
documentType: 'purchase_order',
|
|
60
|
+
removeLevels: [2],
|
|
61
|
+
levels: [{ level: 3, approvers: [{ type: 'user', userId: 'eu-fin' }] }],
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Levels are keyed by number: a child level overrides the base level *field by
|
|
66
|
+
field* (so a derived template can swap just the approvers and inherit name,
|
|
67
|
+
mode and deadlines), child-only levels are appended, and `removeLevels` drops
|
|
68
|
+
inherited ones. `conditions` are replaced wholesale when supplied — merging
|
|
69
|
+
two rule lists would produce a chain neither author intended. `escalation`,
|
|
70
|
+
`slaDeadlineDays` and `allowOverride` are inherited unless the child sets them.
|
|
71
|
+
|
|
72
|
+
- **Resolution happens once, at define time, and the flattened result is what
|
|
73
|
+
gets stored.** Resolving lazily on read would mean editing a base silently
|
|
74
|
+
reshapes every derived template — and every in-flight instance running against
|
|
75
|
+
one — which is exactly the surprise `templateSnapshot` exists to prevent
|
|
76
|
+
elsewhere in the engine. A base that itself extends something is already
|
|
77
|
+
flattened, so chains resolve naturally and cycles cannot form.
|
|
78
|
+
|
|
79
|
+
- **Validation runs on the flattened template**, not the fragment: a derived
|
|
80
|
+
config carrying no levels of its own is valid (it inherits the chain), while
|
|
81
|
+
one whose `removeLevels` strips the chain entirely is rejected at definition
|
|
82
|
+
time. `extends` and `removeLevels` are directives for the call and are not
|
|
83
|
+
persisted.
|
|
84
|
+
|
|
10
85
|
## [1.1.0] - 2026-09-04
|
|
11
86
|
|
|
12
87
|
### Added — approval reminders
|
package/README.md
CHANGED
|
@@ -399,6 +399,67 @@ engine.registerConditionOperator('between', (actual, expected) => {
|
|
|
399
399
|
});
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
+
### Out-of-office cover
|
|
403
|
+
|
|
404
|
+
Approvers go on leave, and a chain that waits on an absent person stalls.
|
|
405
|
+
Configure a provider and absent approvers are swapped for their stand-in
|
|
406
|
+
wherever approvers are resolved — at submit, when a level activates, on
|
|
407
|
+
escalation, and in `previewApprovalChain()`:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
const engine = new ApprovalEngine({
|
|
411
|
+
adapter,
|
|
412
|
+
outOfOfficeProvider: {
|
|
413
|
+
// Return the stand-in, or null when the approver is available.
|
|
414
|
+
getDelegateFor: async (userId, at) => hr.coverFor(userId, at),
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Absence lives in the HR or directory system that already tracks leave, so this
|
|
420
|
+
is an injected provider rather than engine-owned state — duplicating that data
|
|
421
|
+
here would guarantee the two disagree. The resolution time is passed in, so
|
|
422
|
+
cover can be date-bound.
|
|
423
|
+
|
|
424
|
+
Substitution is transitive (A away → B, B away → C lands on C) up to five hops.
|
|
425
|
+
A cover *cycle* stops rather than looping, and a provider that throws is treated
|
|
426
|
+
as "no cover known" — an HR lookup failing must not stop an approval being
|
|
427
|
+
routed at all.
|
|
428
|
+
|
|
429
|
+
### Template inheritance
|
|
430
|
+
|
|
431
|
+
ERP tenants run many near-identical workflows — one per region, entity or
|
|
432
|
+
document class — that share a spine and differ in a level or two. `extends`
|
|
433
|
+
derives one template from another:
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
await engine.defineTemplate({
|
|
437
|
+
name: 'PO-EU',
|
|
438
|
+
extends: 'PO-base',
|
|
439
|
+
documentType: 'purchase_order',
|
|
440
|
+
removeLevels: [2], // drop an inherited level
|
|
441
|
+
levels: [
|
|
442
|
+
{ level: 3, approvers: [{ type: 'user', userId: 'eu-fin' }] }, // override just the approvers
|
|
443
|
+
{ level: 4, name: 'EU Compliance', approvers: [...], mode: 'any' }, // add a new level
|
|
444
|
+
],
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
| Field | Merge rule |
|
|
449
|
+
|---|---|
|
|
450
|
+
| `levels` | Keyed by level number: a child level overrides the base level *field by field*, child-only levels are appended, `removeLevels` drops inherited ones |
|
|
451
|
+
| `conditions` | Replaced wholesale when the child supplies them |
|
|
452
|
+
| `escalation`, `slaDeadlineDays`, `allowOverride` | Child value if set, otherwise inherited |
|
|
453
|
+
|
|
454
|
+
**Resolution happens once, at define time, and the flattened result is stored.**
|
|
455
|
+
Editing a base later never silently reshapes a derived template or an instance
|
|
456
|
+
already running against one — the same insulation `templateSnapshot` gives
|
|
457
|
+
in-flight instances. A base that itself extends something is already flattened,
|
|
458
|
+
so chains resolve naturally and cycles cannot form.
|
|
459
|
+
|
|
460
|
+
Validation runs on the *flattened* template, so a derived config that strips its
|
|
461
|
+
whole chain is rejected at definition time.
|
|
462
|
+
|
|
402
463
|
### Reminders
|
|
403
464
|
|
|
404
465
|
Escalation reassigns work; a reminder only nudges, which is usually what an
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-
|
|
2
|
-
import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-
|
|
3
|
-
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-
|
|
1
|
+
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-CKV_FZFV.cjs';
|
|
2
|
+
import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BsM0DUPf.cjs';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BKMeGmaG.cjs';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { L as Logger } from './Logger-BplhlU7l.cjs';
|
|
6
6
|
import { C as Clock } from './Clock-3FnOczFJ.cjs';
|
|
7
|
-
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-
|
|
8
|
-
import { I as IAuditAdapter } from './IAuditAdapter-
|
|
7
|
+
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-BLw9Oc-S.cjs';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-B2I3CjaJ.cjs';
|
|
9
9
|
import { I as IMetricsAdapter } from './IMetricsAdapter-QvyaKQCc.cjs';
|
|
10
10
|
import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.cjs';
|
|
11
|
-
import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-
|
|
11
|
+
import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-BHDZK9QL.cjs';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -136,6 +136,23 @@ interface OrgProvider {
|
|
|
136
136
|
/** Optional: resolve users matching a custom attribute/value pair. */
|
|
137
137
|
getUsersByAttribute?(attr: string, value: unknown, tenantId?: string): Promise<string[]> | string[];
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Supplies out-of-office cover so an approver on leave does not stall a chain.
|
|
141
|
+
*
|
|
142
|
+
* Consulted whenever a level's approvers are resolved — at submit, when a level
|
|
143
|
+
* activates, and when a chain is previewed. Kept as an injected provider rather
|
|
144
|
+
* than engine-owned state because absence lives in the HR or directory system
|
|
145
|
+
* that already knows about leave; duplicating it here would guarantee the two
|
|
146
|
+
* disagree.
|
|
147
|
+
*/
|
|
148
|
+
interface OutOfOfficeProvider {
|
|
149
|
+
/**
|
|
150
|
+
* @param userId - The approver about to be assigned.
|
|
151
|
+
* @param at - The moment cover is being resolved for.
|
|
152
|
+
* @returns The user to stand in, or null/undefined when the approver is available.
|
|
153
|
+
*/
|
|
154
|
+
getDelegateFor(userId: string, at: Date): Promise<string | null | undefined> | string | null | undefined;
|
|
155
|
+
}
|
|
139
156
|
type ApproverResolverFn = (config: Record<string, unknown>, ctx: {
|
|
140
157
|
submittedBy: string;
|
|
141
158
|
data: Record<string, unknown>;
|
|
@@ -296,6 +313,11 @@ interface ApprovalEngineOptions {
|
|
|
296
313
|
adapter: IStorageAdapter;
|
|
297
314
|
tenantId?: string;
|
|
298
315
|
orgProvider?: OrgProvider;
|
|
316
|
+
/**
|
|
317
|
+
* Supplies stand-ins for approvers who are away, so leave does not stall a
|
|
318
|
+
* chain. Consulted every time approvers are resolved.
|
|
319
|
+
*/
|
|
320
|
+
outOfOfficeProvider?: OutOfOfficeProvider;
|
|
299
321
|
logger?: Logger;
|
|
300
322
|
escalationPollIntervalMs?: number;
|
|
301
323
|
/** Maximum number of instances allowed in a single bulk operation. Default: 200. */
|
|
@@ -546,4 +568,4 @@ declare class ApprovalEngine {
|
|
|
546
568
|
private runExternalAudit;
|
|
547
569
|
}
|
|
548
570
|
|
|
549
|
-
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 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 BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type
|
|
571
|
+
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 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 BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OutOfOfficeProvider as k, type OverrideOptions as l, type PreviewResult as m, type RejectOptions as n, type ResubmitOptions as o, type RetryPolicy as p, defaultIdGenerator as q, toComparableNumber as t, validateConditionExpression as v, weekendCalendar as w };
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-
|
|
2
|
-
import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-
|
|
3
|
-
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-
|
|
1
|
+
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from './IStorageAdapter-DQJmR6Mt.js';
|
|
2
|
+
import { k as ConditionExpression, o as ResolverFn, g as ApprovalTemplateConfig, A as ApprovalTemplate, j as AuditContext, a as ApprovalInstance, e as ApprovalMode, b as AuditEntry } from './instance-BsM0DUPf.js';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BmSeB7FZ.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { L as Logger } from './Logger-BplhlU7l.js';
|
|
6
6
|
import { C as Clock } from './Clock-3FnOczFJ.js';
|
|
7
|
-
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-
|
|
8
|
-
import { I as IAuditAdapter } from './IAuditAdapter-
|
|
7
|
+
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-BW9t8wPn.js';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-GixC9zS1.js';
|
|
9
9
|
import { I as IMetricsAdapter } from './IMetricsAdapter-QvyaKQCc.js';
|
|
10
10
|
import { I as ISchedulerAdapter } from './ISchedulerAdapter-DKv_QjVN.js';
|
|
11
|
-
import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-
|
|
11
|
+
import { I as IAuthorizationPolicy } from './IAuthorizationPolicy-4ge9Gnxj.js';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -136,6 +136,23 @@ interface OrgProvider {
|
|
|
136
136
|
/** Optional: resolve users matching a custom attribute/value pair. */
|
|
137
137
|
getUsersByAttribute?(attr: string, value: unknown, tenantId?: string): Promise<string[]> | string[];
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Supplies out-of-office cover so an approver on leave does not stall a chain.
|
|
141
|
+
*
|
|
142
|
+
* Consulted whenever a level's approvers are resolved — at submit, when a level
|
|
143
|
+
* activates, and when a chain is previewed. Kept as an injected provider rather
|
|
144
|
+
* than engine-owned state because absence lives in the HR or directory system
|
|
145
|
+
* that already knows about leave; duplicating it here would guarantee the two
|
|
146
|
+
* disagree.
|
|
147
|
+
*/
|
|
148
|
+
interface OutOfOfficeProvider {
|
|
149
|
+
/**
|
|
150
|
+
* @param userId - The approver about to be assigned.
|
|
151
|
+
* @param at - The moment cover is being resolved for.
|
|
152
|
+
* @returns The user to stand in, or null/undefined when the approver is available.
|
|
153
|
+
*/
|
|
154
|
+
getDelegateFor(userId: string, at: Date): Promise<string | null | undefined> | string | null | undefined;
|
|
155
|
+
}
|
|
139
156
|
type ApproverResolverFn = (config: Record<string, unknown>, ctx: {
|
|
140
157
|
submittedBy: string;
|
|
141
158
|
data: Record<string, unknown>;
|
|
@@ -296,6 +313,11 @@ interface ApprovalEngineOptions {
|
|
|
296
313
|
adapter: IStorageAdapter;
|
|
297
314
|
tenantId?: string;
|
|
298
315
|
orgProvider?: OrgProvider;
|
|
316
|
+
/**
|
|
317
|
+
* Supplies stand-ins for approvers who are away, so leave does not stall a
|
|
318
|
+
* chain. Consulted every time approvers are resolved.
|
|
319
|
+
*/
|
|
320
|
+
outOfOfficeProvider?: OutOfOfficeProvider;
|
|
299
321
|
logger?: Logger;
|
|
300
322
|
escalationPollIntervalMs?: number;
|
|
301
323
|
/** Maximum number of instances allowed in a single bulk operation. Default: 200. */
|
|
@@ -546,4 +568,4 @@ declare class ApprovalEngine {
|
|
|
546
568
|
private runExternalAudit;
|
|
547
569
|
}
|
|
548
570
|
|
|
549
|
-
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 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 BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type
|
|
571
|
+
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 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 BusinessCalendar as f, type CancelOptions as g, type ConditionOperatorFn as h, type CycleTimeStats as i, type IdempotencyKeyFn as j, type OutOfOfficeProvider as k, type OverrideOptions as l, type PreviewResult as m, type RejectOptions as n, type ResubmitOptions as o, type RetryPolicy as p, defaultIdGenerator as q, toComparableNumber as t, validateConditionExpression as v, weekendCalendar as w };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BsM0DUPf.js';
|
|
2
2
|
|
|
3
3
|
interface AuthorizationContext {
|
|
4
4
|
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-BsM0DUPf.cjs';
|
|
2
2
|
|
|
3
3
|
interface AuthorizationContext {
|
|
4
4
|
operation: 'submit' | 'approve' | 'reject' | 'delegate' | 'reassign' | 'cancel' | 'escalate' | 'override' | 'resubmit' | 'addComment' | 'updateData';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-
|
|
1
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BsM0DUPf.cjs';
|
|
2
2
|
|
|
3
3
|
interface PaginationOpts {
|
|
4
4
|
limit: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-
|
|
1
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, f as ApprovalStatus, b as AuditEntry } from './instance-BsM0DUPf.js';
|
|
2
2
|
|
|
3
3
|
interface PaginationOpts {
|
|
4
4
|
limit: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-
|
|
2
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
1
|
+
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-CKV_FZFV.cjs';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.cjs';
|
|
3
3
|
|
|
4
4
|
declare class MemoryAdapter implements IStorageAdapter {
|
|
5
5
|
private templates;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-
|
|
2
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
1
|
+
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-DQJmR6Mt.js';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.js';
|
|
3
3
|
|
|
4
4
|
declare class MemoryAdapter implements IStorageAdapter {
|
|
5
5
|
private templates;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as tls from 'tls';
|
|
2
2
|
import * as pg from 'pg';
|
|
3
|
-
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-
|
|
4
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
3
|
+
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-CKV_FZFV.cjs';
|
|
4
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.cjs';
|
|
5
5
|
|
|
6
6
|
interface PostgresAdapterOptions {
|
|
7
7
|
connectionString?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as tls from 'tls';
|
|
2
2
|
import * as pg from 'pg';
|
|
3
|
-
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-
|
|
4
|
-
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-
|
|
3
|
+
import { I as IStorageAdapter, P as PaginationOpts, a as PaginatedResult, b as InstanceFilter, C as CursorPaginationOpts, c as CursorPaginatedResult } from '../IStorageAdapter-DQJmR6Mt.js';
|
|
4
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-BsM0DUPf.js';
|
|
5
5
|
|
|
6
6
|
interface PostgresAdapterOptions {
|
|
7
7
|
connectionString?: string;
|
package/dist/index.cjs
CHANGED
|
@@ -224,6 +224,66 @@ var TemplateRegistry = class {
|
|
|
224
224
|
this.clock = opts?.clock ?? systemClock;
|
|
225
225
|
this.generateId = opts?.generateId ?? defaultIdGenerator;
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Flatten a template that declares `extends` against its base.
|
|
229
|
+
*
|
|
230
|
+
* Resolution is done once, at define time, and the flattened result is what
|
|
231
|
+
* gets stored. Resolving lazily on read would mean editing a base silently
|
|
232
|
+
* reshapes every derived template — and every in-flight instance running
|
|
233
|
+
* against one — which is exactly the surprise `templateSnapshot` exists to
|
|
234
|
+
* prevent elsewhere in the engine.
|
|
235
|
+
*
|
|
236
|
+
* Merge rules:
|
|
237
|
+
* - **Levels** are keyed by level number. A child level with the same number
|
|
238
|
+
* overrides the base level field by field, so a derived template can change
|
|
239
|
+
* just the approvers and inherit the rest. Child-only levels are appended,
|
|
240
|
+
* and `removeLevels` drops inherited ones.
|
|
241
|
+
* - **conditions** are replaced wholesale when the child supplies them.
|
|
242
|
+
* Concatenating two rule lists would produce a chain neither author
|
|
243
|
+
* intended, and there is no meaningful way to "merge" boolean rules.
|
|
244
|
+
* - **escalation**, **slaDeadlineDays** and **allowOverride** are taken from
|
|
245
|
+
* the child when set, otherwise inherited.
|
|
246
|
+
*
|
|
247
|
+
* A base that itself extends something is already flattened in storage, so
|
|
248
|
+
* chains resolve naturally and cycles cannot form: the base must exist before
|
|
249
|
+
* the child can name it.
|
|
250
|
+
*
|
|
251
|
+
* @param config - The raw config as supplied by the caller.
|
|
252
|
+
* @returns The config with inheritance applied, or unchanged if no `extends`.
|
|
253
|
+
*/
|
|
254
|
+
async resolveInheritance(config) {
|
|
255
|
+
if (!config.extends) return config;
|
|
256
|
+
if (config.extends === config.name) {
|
|
257
|
+
throw new ApprovalValidationError(`Template "${config.name}" cannot extend itself.`);
|
|
258
|
+
}
|
|
259
|
+
const base = await this.adapter.getTemplate(this.tenantId, config.extends);
|
|
260
|
+
if (!base) {
|
|
261
|
+
throw new ApprovalTemplateNotFoundError(
|
|
262
|
+
`${config.extends} (referenced by "${config.name}" via extends)`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const removed = new Set(config.removeLevels ?? []);
|
|
266
|
+
const byLevel = /* @__PURE__ */ new Map();
|
|
267
|
+
for (const lvl of base.levels) {
|
|
268
|
+
if (!removed.has(lvl.level)) byLevel.set(lvl.level, lvl);
|
|
269
|
+
}
|
|
270
|
+
for (const lvl of config.levels ?? []) {
|
|
271
|
+
const inherited = byLevel.get(lvl.level);
|
|
272
|
+
byLevel.set(lvl.level, inherited ? { ...inherited, ...lvl } : lvl);
|
|
273
|
+
}
|
|
274
|
+
const levels = [...byLevel.values()].sort((a, b) => a.level - b.level);
|
|
275
|
+
const resolved = {
|
|
276
|
+
...config,
|
|
277
|
+
levels,
|
|
278
|
+
conditions: config.conditions ?? base.conditions,
|
|
279
|
+
escalation: config.escalation ?? base.escalation,
|
|
280
|
+
slaDeadlineDays: config.slaDeadlineDays ?? base.slaDeadlineDays,
|
|
281
|
+
allowOverride: config.allowOverride ?? base.allowOverride
|
|
282
|
+
};
|
|
283
|
+
delete resolved.extends;
|
|
284
|
+
delete resolved.removeLevels;
|
|
285
|
+
return resolved;
|
|
286
|
+
}
|
|
227
287
|
async define(config) {
|
|
228
288
|
const existing = await this.adapter.getTemplate(this.tenantId, config.name);
|
|
229
289
|
if (existing) {
|
|
@@ -271,6 +331,7 @@ var TemplateRegistry = class {
|
|
|
271
331
|
};
|
|
272
332
|
|
|
273
333
|
// src/engine/LevelResolver.ts
|
|
334
|
+
var MAX_OOO_HOPS = 5;
|
|
274
335
|
var LevelResolver = class {
|
|
275
336
|
constructor() {
|
|
276
337
|
this.resolvers = /* @__PURE__ */ new Map();
|
|
@@ -282,7 +343,7 @@ var LevelResolver = class {
|
|
|
282
343
|
registerApproverType(typeName, fn) {
|
|
283
344
|
this.approverTypes.set(typeName, fn);
|
|
284
345
|
}
|
|
285
|
-
async resolveApprovers(approvers, submittedBy, data, orgProvider) {
|
|
346
|
+
async resolveApprovers(approvers, submittedBy, data, orgProvider, outOfOffice, at) {
|
|
286
347
|
const resolved = [];
|
|
287
348
|
for (const approver of approvers) {
|
|
288
349
|
switch (approver.type) {
|
|
@@ -302,7 +363,9 @@ var LevelResolver = class {
|
|
|
302
363
|
break;
|
|
303
364
|
}
|
|
304
365
|
case "dynamic": {
|
|
305
|
-
const fn = this.resolvers.get(
|
|
366
|
+
const fn = this.resolvers.get(
|
|
367
|
+
approver.resolver
|
|
368
|
+
);
|
|
306
369
|
if (!fn) {
|
|
307
370
|
throw new Error(
|
|
308
371
|
`No resolver registered for "${approver.resolver}". Call engine.registerResolver("${approver.resolver}", fn) first.`
|
|
@@ -319,7 +382,11 @@ var LevelResolver = class {
|
|
|
319
382
|
`Unknown approver type "${approver.type}". Register it with engine.registerApproverType("${approver.type}", fn) first.`
|
|
320
383
|
);
|
|
321
384
|
}
|
|
322
|
-
const ids = await customFn(approver, {
|
|
385
|
+
const ids = await customFn(approver, {
|
|
386
|
+
submittedBy,
|
|
387
|
+
data,
|
|
388
|
+
orgProvider
|
|
389
|
+
});
|
|
323
390
|
resolved.push(...ids);
|
|
324
391
|
break;
|
|
325
392
|
}
|
|
@@ -331,7 +398,40 @@ var LevelResolver = class {
|
|
|
331
398
|
"No approvers resolved for this level. Check your approver configuration \u2014 role may have no members or dynamic resolver returned empty."
|
|
332
399
|
);
|
|
333
400
|
}
|
|
334
|
-
return result;
|
|
401
|
+
return this.applyOutOfOffice(result, outOfOffice, at);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Replace approvers who are away with their cover.
|
|
405
|
+
*
|
|
406
|
+
* Substitution is transitive up to {@link MAX_OOO_HOPS} so an A→B→C chain of
|
|
407
|
+
* absences still lands on someone present, but a cycle (A covers B while B
|
|
408
|
+
* covers A) simply stops rather than looping — leaving the original approver
|
|
409
|
+
* assigned, which is visible and fixable, unlike a hang.
|
|
410
|
+
*
|
|
411
|
+
* A provider that throws is treated as "no cover known": an HR lookup failing
|
|
412
|
+
* must not block an approval from being routed at all.
|
|
413
|
+
*/
|
|
414
|
+
async applyOutOfOffice(userIds, provider, at) {
|
|
415
|
+
if (!provider) return userIds;
|
|
416
|
+
const asOf = at ?? /* @__PURE__ */ new Date();
|
|
417
|
+
const covered = [];
|
|
418
|
+
for (const original of userIds) {
|
|
419
|
+
let current = original;
|
|
420
|
+
const seen = /* @__PURE__ */ new Set([current]);
|
|
421
|
+
for (let hop = 0; hop < MAX_OOO_HOPS; hop++) {
|
|
422
|
+
let delegate;
|
|
423
|
+
try {
|
|
424
|
+
delegate = await provider.getDelegateFor(current, asOf);
|
|
425
|
+
} catch {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
if (!delegate || delegate === current || seen.has(delegate)) break;
|
|
429
|
+
seen.add(delegate);
|
|
430
|
+
current = delegate;
|
|
431
|
+
}
|
|
432
|
+
covered.push(current);
|
|
433
|
+
}
|
|
434
|
+
return [...new Set(covered)];
|
|
335
435
|
}
|
|
336
436
|
};
|
|
337
437
|
|
|
@@ -855,9 +955,11 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
855
955
|
/** Validate a template config without persisting. Synchronous; never throws. */
|
|
856
956
|
validateTemplate(config) {
|
|
857
957
|
const errors = [];
|
|
858
|
-
|
|
958
|
+
const hasOwnLevels = Boolean(config.levels && config.levels.length > 0);
|
|
959
|
+
if (!hasOwnLevels && config.extends === void 0) {
|
|
859
960
|
errors.push({ field: "levels", message: "Template must have at least one level." });
|
|
860
|
-
}
|
|
961
|
+
}
|
|
962
|
+
if (hasOwnLevels) {
|
|
861
963
|
const levelNums = /* @__PURE__ */ new Set();
|
|
862
964
|
config.levels.forEach((l, i) => {
|
|
863
965
|
if (levelNums.has(l.level)) {
|
|
@@ -986,25 +1088,27 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
986
1088
|
return { valid: errors.length === 0, errors };
|
|
987
1089
|
}
|
|
988
1090
|
async defineTemplate(config) {
|
|
989
|
-
const
|
|
1091
|
+
const resolved = await this.registry.resolveInheritance(config);
|
|
1092
|
+
const validation = this.validateTemplate(resolved);
|
|
990
1093
|
if (!validation.valid) {
|
|
991
1094
|
const first = validation.errors[0];
|
|
992
1095
|
throw new ApprovalValidationError(
|
|
993
1096
|
`Invalid template configuration: ${first?.message ?? "unknown error"}`
|
|
994
1097
|
);
|
|
995
1098
|
}
|
|
996
|
-
return this.registry.define(
|
|
1099
|
+
return this.registry.define(resolved);
|
|
997
1100
|
}
|
|
998
1101
|
/** Update an existing template, incrementing its version. In-flight instances are protected by their templateSnapshot. */
|
|
999
1102
|
async updateTemplate(config) {
|
|
1000
|
-
const
|
|
1103
|
+
const resolved = await this.registry.resolveInheritance(config);
|
|
1104
|
+
const validation = this.validateTemplate(resolved);
|
|
1001
1105
|
if (!validation.valid) {
|
|
1002
1106
|
const first = validation.errors[0];
|
|
1003
1107
|
throw new ApprovalValidationError(
|
|
1004
1108
|
`Invalid template configuration: ${first?.message ?? "unknown error"}`
|
|
1005
1109
|
);
|
|
1006
1110
|
}
|
|
1007
|
-
return this.registry.update(
|
|
1111
|
+
return this.registry.update(resolved);
|
|
1008
1112
|
}
|
|
1009
1113
|
async getTemplate(name) {
|
|
1010
1114
|
return this.registry.get(name);
|
|
@@ -1085,7 +1189,9 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
1085
1189
|
lvl.approverConfigs,
|
|
1086
1190
|
opts.submittedBy,
|
|
1087
1191
|
opts.data,
|
|
1088
|
-
this.opts.orgProvider
|
|
1192
|
+
this.opts.orgProvider,
|
|
1193
|
+
this.opts.outOfOfficeProvider,
|
|
1194
|
+
now
|
|
1089
1195
|
);
|
|
1090
1196
|
}
|
|
1091
1197
|
const auditEntry = {
|
|
@@ -2088,7 +2194,9 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
2088
2194
|
cfg.approvers,
|
|
2089
2195
|
submittedBy,
|
|
2090
2196
|
data,
|
|
2091
|
-
this.opts.orgProvider
|
|
2197
|
+
this.opts.orgProvider,
|
|
2198
|
+
this.opts.outOfOfficeProvider,
|
|
2199
|
+
this.clock.now()
|
|
2092
2200
|
);
|
|
2093
2201
|
levels.push({ level: cfg.level, name: cfg.name, resolvedApprovers, mode: cfg.mode });
|
|
2094
2202
|
} catch {
|
|
@@ -2467,7 +2575,9 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
2467
2575
|
[escalationConfig.escalateTo],
|
|
2468
2576
|
instance.submittedBy,
|
|
2469
2577
|
instance.data,
|
|
2470
|
-
this.opts.orgProvider
|
|
2578
|
+
this.opts.orgProvider,
|
|
2579
|
+
this.opts.outOfOfficeProvider,
|
|
2580
|
+
this.clock.now()
|
|
2471
2581
|
);
|
|
2472
2582
|
const filteredApprovers = newApprovers.filter((id) => id !== instance.submittedBy);
|
|
2473
2583
|
if (filteredApprovers.length === 0) {
|
|
@@ -2752,7 +2862,9 @@ var ApprovalEngine = class _ApprovalEngine {
|
|
|
2752
2862
|
lvl.approverConfigs,
|
|
2753
2863
|
instance.submittedBy,
|
|
2754
2864
|
instance.data,
|
|
2755
|
-
this.opts.orgProvider
|
|
2865
|
+
this.opts.orgProvider,
|
|
2866
|
+
this.opts.outOfOfficeProvider,
|
|
2867
|
+
now
|
|
2756
2868
|
);
|
|
2757
2869
|
if (lvl.escalationAfterDays) {
|
|
2758
2870
|
lvl.escalationDueAt = this.deadlineFrom(now, lvl.escalationAfterDays);
|