hierarchical-approval 0.9.0 → 1.1.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 +89 -0
- package/README.md +59 -0
- package/dist/{ApprovalEngine-mD-qyxcZ.d.ts → ApprovalEngine-BDnQwKi9.d.ts} +53 -7
- package/dist/{ApprovalEngine-pfQ2v-J9.d.cts → ApprovalEngine-qwImBe9A.d.cts} +53 -7
- package/dist/{IAuditAdapter-nlIK1em0.d.ts → IAuditAdapter-99JBU3Db.d.ts} +1 -1
- package/dist/{IAuditAdapter-0frLfzRq.d.cts → IAuditAdapter-CWdZxS3U.d.cts} +1 -1
- package/dist/{IAuthorizationPolicy-DXEMaoxF.d.cts → IAuthorizationPolicy-ChZIUJhQ.d.cts} +1 -1
- package/dist/{IAuthorizationPolicy-DG8vmD5j.d.ts → IAuthorizationPolicy-U-jZrl2F.d.ts} +1 -1
- package/dist/{IMetricsAdapter-t7QpfA1I.d.cts → IMetricsAdapter-QvyaKQCc.d.cts} +1 -1
- package/dist/{IMetricsAdapter-t7QpfA1I.d.ts → IMetricsAdapter-QvyaKQCc.d.ts} +1 -1
- package/dist/{INotificationAdapter-COeQ5kEv.d.ts → INotificationAdapter-BKwhxhe2.d.cts} +11 -2
- package/dist/{INotificationAdapter-CFedkDQe.d.cts → INotificationAdapter-CsSWiCZE.d.ts} +11 -2
- package/dist/{IOperationMiddleware-Bx4_U0ca.d.ts → IOperationMiddleware-CPkGSVfr.d.ts} +1 -1
- package/dist/{IOperationMiddleware-CYXTV_TI.d.cts → IOperationMiddleware-D162RiJs.d.cts} +1 -1
- package/dist/{IStorageAdapter-qKgtuTY1.d.cts → IStorageAdapter-BZarNxs9.d.cts} +1 -1
- package/dist/{IStorageAdapter-ecVYcA0z.d.ts → IStorageAdapter-DrY-rhhF.d.ts} +1 -1
- package/dist/adapters/MemoryAdapter.cjs +7 -3
- package/dist/adapters/MemoryAdapter.cjs.map +1 -1
- package/dist/adapters/MemoryAdapter.d.cts +2 -2
- package/dist/adapters/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/MemoryAdapter.js +7 -3
- package/dist/adapters/MemoryAdapter.js.map +1 -1
- package/dist/adapters/PostgresAdapter.cjs +6 -0
- package/dist/adapters/PostgresAdapter.cjs.map +1 -1
- package/dist/adapters/PostgresAdapter.d.cts +2 -2
- package/dist/adapters/PostgresAdapter.d.ts +2 -2
- package/dist/adapters/PostgresAdapter.js +6 -0
- package/dist/adapters/PostgresAdapter.js.map +1 -1
- package/dist/index.cjs +350 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -10
- package/dist/index.d.ts +13 -10
- package/dist/index.js +350 -55
- package/dist/index.js.map +1 -1
- package/dist/{instance-BxUwmrEi.d.cts → instance-DuEgSIIz.d.cts} +34 -1
- package/dist/{instance-BxUwmrEi.d.ts → instance-DuEgSIIz.d.ts} +34 -1
- package/dist/nestjs.cjs +343 -52
- package/dist/nestjs.cjs.map +1 -1
- package/dist/nestjs.d.cts +8 -8
- package/dist/nestjs.d.ts +8 -8
- package/dist/nestjs.js +343 -52
- 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/metrics.cjs +4 -3
- package/dist/plugins/metrics.cjs.map +1 -1
- package/dist/plugins/metrics.d.cts +1 -1
- package/dist/plugins/metrics.d.ts +1 -1
- package/dist/plugins/metrics.js +4 -3
- package/dist/plugins/metrics.js.map +1 -1
- 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 +350 -55
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +8 -8
- package/dist/testing.d.ts +8 -8
- package/dist/testing.js +350 -55
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,95 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
_Nothing yet._
|
|
9
9
|
|
|
10
|
+
## [1.1.0] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added — approval reminders
|
|
13
|
+
|
|
14
|
+
- **Levels can nudge their pending approvers before escalating.** Escalation
|
|
15
|
+
reassigns work, which is the wrong instrument for an approval that is merely
|
|
16
|
+
late; the only alternative was an out-of-band cron job reading the database.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
{ level: 1, name: 'Manager', ...,
|
|
20
|
+
reminderAfterDays: 2, // first nudge two days after the level opens
|
|
21
|
+
reminderEveryDays: 1, // then daily
|
|
22
|
+
maxReminders: 3 } // default 3
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Emits `approval:reminder` (`level`, `recipients`, `reminderNumber`), records a
|
|
26
|
+
`reminded` audit entry, and increments `approval.reminded`. Recipients exclude
|
|
27
|
+
anyone who has already voted, so a half-decided quorum level stops pestering
|
|
28
|
+
the approvers who did their part. Reminders stop when the level closes or the
|
|
29
|
+
cap is reached, and never change who can approve or when the level escalates.
|
|
30
|
+
|
|
31
|
+
`validateTemplate()` rejects non-positive intervals, a non-integer
|
|
32
|
+
`maxReminders`, and `reminderEveryDays` without `reminderAfterDays` — which
|
|
33
|
+
would configure a repeat for a reminder that never fires.
|
|
34
|
+
|
|
35
|
+
New export: `ReminderEvent`. `EscalationSchedulerOpts` gains `onRemind`.
|
|
36
|
+
|
|
37
|
+
### Fixed — escalation could not reach every branch of a parallel group
|
|
38
|
+
|
|
39
|
+
- **Only the lowest-numbered branch of a parallel group could escalate.** Both
|
|
40
|
+
the scheduler and `MemoryAdapter.getOverdueInstances` matched levels against
|
|
41
|
+
`instance.currentLevel`, which names just one level of a group. An overdue
|
|
42
|
+
upper branch was never fetched and never escalated — it would wait forever.
|
|
43
|
+
Introduced with parallel groups in 1.0.0.
|
|
44
|
+
|
|
45
|
+
Escalation now considers every open branch, and `escalateInternal` takes the
|
|
46
|
+
level to escalate rather than assuming the current one.
|
|
47
|
+
|
|
48
|
+
This also settles a pre-existing disagreement between the adapters:
|
|
49
|
+
`PostgresAdapter` already scanned *all* levels for `escalationDueAt` while
|
|
50
|
+
`MemoryAdapter` scanned one, so the same template escalated differently
|
|
51
|
+
depending on which adapter was in use. Both now scan every open branch.
|
|
52
|
+
|
|
53
|
+
- **`getOverdueInstances` also reports due reminders** in both adapters —
|
|
54
|
+
without which the scheduler would never see a reminder-due instance and no
|
|
55
|
+
reminder would ever be sent.
|
|
56
|
+
|
|
57
|
+
## [1.0.0] - 2026-09-04
|
|
58
|
+
|
|
59
|
+
First stable release. The public API is now considered settled: breaking changes
|
|
60
|
+
from here get a major version.
|
|
61
|
+
|
|
62
|
+
### Added — parallel branch groups
|
|
63
|
+
|
|
64
|
+
- **Levels sharing a `group` name activate together and join before the chain
|
|
65
|
+
advances.** Approvals were strictly sequential, so "Finance and Legal review
|
|
66
|
+
concurrently, then it goes to the CEO" could only be modelled by picking an
|
|
67
|
+
arbitrary order and making one wait on the other — inflating cycle time for no
|
|
68
|
+
business reason. This was the last item on the roadmap in `IMPROVEMENTS.md`.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
levels: [
|
|
72
|
+
{ level: 1, name: 'Manager', ... },
|
|
73
|
+
{ level: 2, name: 'Finance', group: 'review', ... },
|
|
74
|
+
{ level: 3, name: 'Legal', group: 'review', ... },
|
|
75
|
+
{ level: 4, name: 'CEO', ... },
|
|
76
|
+
]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Both branches open at once, decisions arrive in any order, and level 4 stays
|
|
80
|
+
`waiting` until every branch is approved. Rejecting any branch rejects the
|
|
81
|
+
instance, as a rejection always has. A group may also lead the chain, in which
|
|
82
|
+
case it opens at submit.
|
|
83
|
+
|
|
84
|
+
- **`approve()` and `reject()` take an optional `level`.** Inside a parallel
|
|
85
|
+
group one person can sit on several open branches; recording their decision
|
|
86
|
+
against a guessed branch would be silently wrong, so the engine throws and
|
|
87
|
+
asks which branch is meant. Sequential templates never see this.
|
|
88
|
+
|
|
89
|
+
- **`getCurrentApprovers()` returns the union across every open branch.**
|
|
90
|
+
`canApprove()` likewise considers any open branch the user is assigned to.
|
|
91
|
+
|
|
92
|
+
- **`validateTemplate()` rejects a non-contiguous group** — a group's levels must
|
|
93
|
+
occupy consecutive level numbers, so that "advance past the group" and
|
|
94
|
+
"advance past a level" cannot disagree about what comes next.
|
|
95
|
+
|
|
96
|
+
**Backward compatible.** A level without a `group` is its own group of one, and
|
|
97
|
+
all 673 pre-existing tests pass unchanged against the new engine.
|
|
98
|
+
|
|
10
99
|
## [0.9.0] - 2026-09-04
|
|
11
100
|
|
|
12
101
|
### Added — `updateData()`: edit a pending document and recompute its chain
|
package/README.md
CHANGED
|
@@ -399,6 +399,65 @@ engine.registerConditionOperator('between', (actual, expected) => {
|
|
|
399
399
|
});
|
|
400
400
|
```
|
|
401
401
|
|
|
402
|
+
### Reminders
|
|
403
|
+
|
|
404
|
+
Escalation reassigns work; a reminder only nudges, which is usually what an
|
|
405
|
+
overdue-but-not-yet-critical approval needs. Configure it per level:
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
{
|
|
409
|
+
level: 1,
|
|
410
|
+
name: 'Manager',
|
|
411
|
+
approvers: [{ type: 'user', userId: 'mgr-1' }],
|
|
412
|
+
mode: 'any',
|
|
413
|
+
reminderAfterDays: 2, // first nudge two days after the level opens
|
|
414
|
+
reminderEveryDays: 1, // then daily
|
|
415
|
+
maxReminders: 3, // default is 3
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Emits `approval:reminder` with `level`, `recipients` and `reminderNumber`, and
|
|
420
|
+
records a `reminded` audit entry. Recipients exclude anyone who has already
|
|
421
|
+
voted, so a half-decided quorum level stops pestering the approvers who did
|
|
422
|
+
their part. Reminders stop when the level closes or the cap is reached, and they
|
|
423
|
+
never change who can approve or when the level escalates.
|
|
424
|
+
|
|
425
|
+
Requires the escalation scheduler to be running (it is by default).
|
|
426
|
+
|
|
427
|
+
### Parallel branch groups
|
|
428
|
+
|
|
429
|
+
Give levels the same `group` name and they activate together, joining before
|
|
430
|
+
the chain moves on — "Finance and Legal review concurrently, then it goes to the
|
|
431
|
+
CEO":
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
levels: [
|
|
435
|
+
{ level: 1, name: 'Manager', approvers: [{ type: 'user', userId: 'mgr' }], mode: 'any' },
|
|
436
|
+
{ level: 2, name: 'Finance', group: 'review', approvers: [{ type: 'role', role: 'finance' }], mode: 'any' },
|
|
437
|
+
{ level: 3, name: 'Legal', group: 'review', approvers: [{ type: 'role', role: 'legal' }], mode: 'any' },
|
|
438
|
+
{ level: 4, name: 'CEO', approvers: [{ type: 'user', userId: 'ceo' }], mode: 'any' },
|
|
439
|
+
]
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Both branches open at once when the group is reached, decisions can arrive in
|
|
443
|
+
either order, and level 4 stays `waiting` until every branch is approved.
|
|
444
|
+
Rejecting any branch rejects the instance. A group may also lead the chain, in
|
|
445
|
+
which case it opens at submit.
|
|
446
|
+
|
|
447
|
+
`getCurrentApprovers()` returns the union across every open branch. A level
|
|
448
|
+
without a `group` is its own group of one, so existing sequential templates
|
|
449
|
+
behave exactly as before.
|
|
450
|
+
|
|
451
|
+
**When one person sits on two open branches**, `approve()` refuses to guess and
|
|
452
|
+
throws — pass `level` to say which branch the decision is for:
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
await engine.approve(id, { approverId: 'cfo', level: 2 });
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
A group's levels must occupy a contiguous run of level numbers;
|
|
459
|
+
`validateTemplate()` rejects an interleaved group.
|
|
460
|
+
|
|
402
461
|
### Editing a document mid-flight
|
|
403
462
|
|
|
404
463
|
Documents change after submission — a line item is corrected, a vendor
|
|
@@ -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-DrY-rhhF.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-DuEgSIIz.js';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-CsSWiCZE.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-
|
|
9
|
-
import { I as IMetricsAdapter } from './IMetricsAdapter-
|
|
7
|
+
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-CPkGSVfr.js';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-99JBU3Db.js';
|
|
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-U-jZrl2F.js';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -26,6 +26,7 @@ declare const SubmitOptionsSchema: z.ZodObject<{
|
|
|
26
26
|
declare const ApproveOptionsSchema: z.ZodObject<{
|
|
27
27
|
approverId: z.ZodString;
|
|
28
28
|
comment: z.ZodOptional<z.ZodString>;
|
|
29
|
+
level: z.ZodOptional<z.ZodNumber>;
|
|
29
30
|
}, z.core.$strip>;
|
|
30
31
|
declare const RejectOptionsSchema: z.ZodObject<{
|
|
31
32
|
approverId: z.ZodString;
|
|
@@ -34,6 +35,7 @@ declare const RejectOptionsSchema: z.ZodObject<{
|
|
|
34
35
|
originator: "originator";
|
|
35
36
|
previous: "previous";
|
|
36
37
|
}>>;
|
|
38
|
+
level: z.ZodOptional<z.ZodNumber>;
|
|
37
39
|
}, z.core.$strip>;
|
|
38
40
|
declare const DelegateOptionsSchema: z.ZodObject<{
|
|
39
41
|
fromApprover: z.ZodString;
|
|
@@ -489,7 +491,51 @@ declare class ApprovalEngine {
|
|
|
489
491
|
/** Compute a deadline `days` from `from`, honouring the business calendar if one is configured. */
|
|
490
492
|
private deadlineFrom;
|
|
491
493
|
private requireInstance;
|
|
494
|
+
/**
|
|
495
|
+
* Identity of the parallel branch group a level belongs to.
|
|
496
|
+
*
|
|
497
|
+
* An ungrouped level is its own group of one, keyed by its level number, so
|
|
498
|
+
* sequential and parallel levels can be handled by the same code paths. The
|
|
499
|
+
* `#` prefix keeps a synthetic key from ever colliding with a real group
|
|
500
|
+
* name a template author chose.
|
|
501
|
+
*/
|
|
502
|
+
private static groupKeyOf;
|
|
503
|
+
/** Every level currently collecting decisions — more than one inside a parallel group. */
|
|
504
|
+
private pendingLevels;
|
|
505
|
+
/** All levels sharing a group with the given level, including it. */
|
|
506
|
+
private groupMembers;
|
|
507
|
+
/**
|
|
508
|
+
* Pick which pending level an actor is operating on.
|
|
509
|
+
*
|
|
510
|
+
* With sequential levels there is only ever one candidate. Inside a parallel
|
|
511
|
+
* group an approver may sit on several branches at once, and guessing which
|
|
512
|
+
* one they meant would silently record the decision against the wrong branch —
|
|
513
|
+
* so that case demands an explicit `level`.
|
|
514
|
+
*/
|
|
515
|
+
private resolveActorLevel;
|
|
492
516
|
private currentLevelInstance;
|
|
517
|
+
/**
|
|
518
|
+
* The next group of levels to activate: every level sharing the group of the
|
|
519
|
+
* lowest-numbered waiting level.
|
|
520
|
+
*/
|
|
521
|
+
private findNextGroup;
|
|
522
|
+
/** Resolve approvers for a group, set its deadlines, and mark it pending. */
|
|
523
|
+
private activateGroup;
|
|
524
|
+
/**
|
|
525
|
+
* Set (or clear) the next reminder deadline for a level that has just opened.
|
|
526
|
+
*
|
|
527
|
+
* Called wherever a level becomes pending, so a level activated by submit,
|
|
528
|
+
* by advancing, or by a group activation all schedule alike.
|
|
529
|
+
*/
|
|
530
|
+
private scheduleReminder;
|
|
531
|
+
/**
|
|
532
|
+
* Nudge the approvers who still owe a decision on a pending level.
|
|
533
|
+
*
|
|
534
|
+
* Reminders never change who can approve or when the level escalates — they
|
|
535
|
+
* only notify. Recipients exclude anyone who has already voted, so a
|
|
536
|
+
* half-decided quorum level stops pestering the approvers who did their part.
|
|
537
|
+
*/
|
|
538
|
+
private sendReminder;
|
|
493
539
|
private findNextLevel;
|
|
494
540
|
private findPreviousLevel;
|
|
495
541
|
private guardBulkSize;
|
|
@@ -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-BZarNxs9.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-DuEgSIIz.cjs';
|
|
3
|
+
import { I as INotificationAdapter, b as ApprovalEventName, a as ApprovalEventMap } from './INotificationAdapter-BKwhxhe2.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-
|
|
9
|
-
import { I as IMetricsAdapter } from './IMetricsAdapter-
|
|
7
|
+
import { I as IOperationMiddleware, a as ApprovalError } from './IOperationMiddleware-D162RiJs.cjs';
|
|
8
|
+
import { I as IAuditAdapter } from './IAuditAdapter-CWdZxS3U.cjs';
|
|
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-ChZIUJhQ.cjs';
|
|
12
12
|
|
|
13
13
|
declare const SubmitOptionsSchema: z.ZodObject<{
|
|
14
14
|
templateName: z.ZodString;
|
|
@@ -26,6 +26,7 @@ declare const SubmitOptionsSchema: z.ZodObject<{
|
|
|
26
26
|
declare const ApproveOptionsSchema: z.ZodObject<{
|
|
27
27
|
approverId: z.ZodString;
|
|
28
28
|
comment: z.ZodOptional<z.ZodString>;
|
|
29
|
+
level: z.ZodOptional<z.ZodNumber>;
|
|
29
30
|
}, z.core.$strip>;
|
|
30
31
|
declare const RejectOptionsSchema: z.ZodObject<{
|
|
31
32
|
approverId: z.ZodString;
|
|
@@ -34,6 +35,7 @@ declare const RejectOptionsSchema: z.ZodObject<{
|
|
|
34
35
|
originator: "originator";
|
|
35
36
|
previous: "previous";
|
|
36
37
|
}>>;
|
|
38
|
+
level: z.ZodOptional<z.ZodNumber>;
|
|
37
39
|
}, z.core.$strip>;
|
|
38
40
|
declare const DelegateOptionsSchema: z.ZodObject<{
|
|
39
41
|
fromApprover: z.ZodString;
|
|
@@ -489,7 +491,51 @@ declare class ApprovalEngine {
|
|
|
489
491
|
/** Compute a deadline `days` from `from`, honouring the business calendar if one is configured. */
|
|
490
492
|
private deadlineFrom;
|
|
491
493
|
private requireInstance;
|
|
494
|
+
/**
|
|
495
|
+
* Identity of the parallel branch group a level belongs to.
|
|
496
|
+
*
|
|
497
|
+
* An ungrouped level is its own group of one, keyed by its level number, so
|
|
498
|
+
* sequential and parallel levels can be handled by the same code paths. The
|
|
499
|
+
* `#` prefix keeps a synthetic key from ever colliding with a real group
|
|
500
|
+
* name a template author chose.
|
|
501
|
+
*/
|
|
502
|
+
private static groupKeyOf;
|
|
503
|
+
/** Every level currently collecting decisions — more than one inside a parallel group. */
|
|
504
|
+
private pendingLevels;
|
|
505
|
+
/** All levels sharing a group with the given level, including it. */
|
|
506
|
+
private groupMembers;
|
|
507
|
+
/**
|
|
508
|
+
* Pick which pending level an actor is operating on.
|
|
509
|
+
*
|
|
510
|
+
* With sequential levels there is only ever one candidate. Inside a parallel
|
|
511
|
+
* group an approver may sit on several branches at once, and guessing which
|
|
512
|
+
* one they meant would silently record the decision against the wrong branch —
|
|
513
|
+
* so that case demands an explicit `level`.
|
|
514
|
+
*/
|
|
515
|
+
private resolveActorLevel;
|
|
492
516
|
private currentLevelInstance;
|
|
517
|
+
/**
|
|
518
|
+
* The next group of levels to activate: every level sharing the group of the
|
|
519
|
+
* lowest-numbered waiting level.
|
|
520
|
+
*/
|
|
521
|
+
private findNextGroup;
|
|
522
|
+
/** Resolve approvers for a group, set its deadlines, and mark it pending. */
|
|
523
|
+
private activateGroup;
|
|
524
|
+
/**
|
|
525
|
+
* Set (or clear) the next reminder deadline for a level that has just opened.
|
|
526
|
+
*
|
|
527
|
+
* Called wherever a level becomes pending, so a level activated by submit,
|
|
528
|
+
* by advancing, or by a group activation all schedule alike.
|
|
529
|
+
*/
|
|
530
|
+
private scheduleReminder;
|
|
531
|
+
/**
|
|
532
|
+
* Nudge the approvers who still owe a decision on a pending level.
|
|
533
|
+
*
|
|
534
|
+
* Reminders never change who can approve or when the level escalates — they
|
|
535
|
+
* only notify. Recipients exclude anyone who has already voted, so a
|
|
536
|
+
* half-decided quorum level stops pestering the approvers who did their part.
|
|
537
|
+
*/
|
|
538
|
+
private sendReminder;
|
|
493
539
|
private findNextLevel;
|
|
494
540
|
private findPreviousLevel;
|
|
495
541
|
private guardBulkSize;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-DuEgSIIz.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 ApprovalInstance, d as ApprovalLevelInstance } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, d as ApprovalLevelInstance } from './instance-DuEgSIIz.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
|
-
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
1
|
+
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
2
2
|
interface IMetricsAdapter {
|
|
3
3
|
/** Increment a counter. Synchronous — never awaited. */
|
|
4
4
|
increment(metric: MetricName, labels?: Record<string, string>): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
1
|
+
type MetricName = 'approval.submitted' | 'approval.approved' | 'approval.rejected' | 'approval.cancelled' | 'approval.expired' | 'approval.sla_breached' | 'approval.escalated' | 'approval.reassigned' | 'approval.overridden' | 'approval.data_updated' | 'approval.reminded' | 'approval.conflict_retry' | 'approval.operation_duration_ms';
|
|
2
2
|
interface IMetricsAdapter {
|
|
3
3
|
/** Increment a counter. Synchronous — never awaited. */
|
|
4
4
|
increment(metric: MetricName, labels?: Record<string, string>): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, b as AuditEntry } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, b as AuditEntry } from './instance-DuEgSIIz.cjs';
|
|
2
2
|
|
|
3
3
|
interface ApprovalEvent {
|
|
4
4
|
instanceId: string;
|
|
@@ -78,6 +78,14 @@ interface DataUpdatedEvent extends ApprovalEvent {
|
|
|
78
78
|
/** Level numbers removed from the future chain by re-evaluating conditions. */
|
|
79
79
|
removedLevels: number[];
|
|
80
80
|
}
|
|
81
|
+
/** Emitted when a pending level's approvers are nudged. */
|
|
82
|
+
interface ReminderEvent extends ApprovalEvent {
|
|
83
|
+
level: number;
|
|
84
|
+
/** Approvers who still owe a decision on this level. */
|
|
85
|
+
recipients: string[];
|
|
86
|
+
/** 1 for the first reminder on this level, 2 for the next, and so on. */
|
|
87
|
+
reminderNumber: number;
|
|
88
|
+
}
|
|
81
89
|
interface ApprovalEventMap {
|
|
82
90
|
'approval:submitted': SubmittedEvent;
|
|
83
91
|
'approval:approved': ApprovedEvent;
|
|
@@ -93,6 +101,7 @@ interface ApprovalEventMap {
|
|
|
93
101
|
'approval:expired': ExpiredEvent;
|
|
94
102
|
'approval:sla_breached': SlaBreachedEvent;
|
|
95
103
|
'approval:data_updated': DataUpdatedEvent;
|
|
104
|
+
'approval:reminder': ReminderEvent;
|
|
96
105
|
}
|
|
97
106
|
type ApprovalEventName = keyof ApprovalEventMap;
|
|
98
107
|
interface HistoryEntry extends AuditEntry {
|
|
@@ -116,4 +125,4 @@ interface INotificationAdapter {
|
|
|
116
125
|
notify(event: NotificationEvent): Promise<void>;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, RejectedEvent as f,
|
|
128
|
+
export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, RejectedEvent as f, ReminderEvent as g, ResubmittedEvent as h, SubmittedEvent as i };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ApprovalInstance, b as AuditEntry } from './instance-
|
|
1
|
+
import { a as ApprovalInstance, b as AuditEntry } from './instance-DuEgSIIz.js';
|
|
2
2
|
|
|
3
3
|
interface ApprovalEvent {
|
|
4
4
|
instanceId: string;
|
|
@@ -78,6 +78,14 @@ interface DataUpdatedEvent extends ApprovalEvent {
|
|
|
78
78
|
/** Level numbers removed from the future chain by re-evaluating conditions. */
|
|
79
79
|
removedLevels: number[];
|
|
80
80
|
}
|
|
81
|
+
/** Emitted when a pending level's approvers are nudged. */
|
|
82
|
+
interface ReminderEvent extends ApprovalEvent {
|
|
83
|
+
level: number;
|
|
84
|
+
/** Approvers who still owe a decision on this level. */
|
|
85
|
+
recipients: string[];
|
|
86
|
+
/** 1 for the first reminder on this level, 2 for the next, and so on. */
|
|
87
|
+
reminderNumber: number;
|
|
88
|
+
}
|
|
81
89
|
interface ApprovalEventMap {
|
|
82
90
|
'approval:submitted': SubmittedEvent;
|
|
83
91
|
'approval:approved': ApprovedEvent;
|
|
@@ -93,6 +101,7 @@ interface ApprovalEventMap {
|
|
|
93
101
|
'approval:expired': ExpiredEvent;
|
|
94
102
|
'approval:sla_breached': SlaBreachedEvent;
|
|
95
103
|
'approval:data_updated': DataUpdatedEvent;
|
|
104
|
+
'approval:reminder': ReminderEvent;
|
|
96
105
|
}
|
|
97
106
|
type ApprovalEventName = keyof ApprovalEventMap;
|
|
98
107
|
interface HistoryEntry extends AuditEntry {
|
|
@@ -116,4 +125,4 @@ interface INotificationAdapter {
|
|
|
116
125
|
notify(event: NotificationEvent): Promise<void>;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, RejectedEvent as f,
|
|
128
|
+
export type { ApprovalEvent as A, CancelledEvent as C, DataUpdatedEvent as D, EscalatedEvent as E, HistoryEntry as H, INotificationAdapter as I, LevelAdvancedEvent as L, NotificationEvent as N, OverriddenEvent as O, ReassignedEvent as R, SlaBreachedEvent as S, ApprovalEventMap as a, ApprovalEventName as b, ApprovedEvent as c, DelegatedEvent as d, ExpiredEvent as e, RejectedEvent as f, ReminderEvent as g, ResubmittedEvent as h, SubmittedEvent as i };
|
|
@@ -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-DuEgSIIz.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-DuEgSIIz.js';
|
|
2
2
|
|
|
3
3
|
interface PaginationOpts {
|
|
4
4
|
limit: number;
|
|
@@ -129,14 +129,18 @@ var MemoryAdapter = class {
|
|
|
129
129
|
if (i.tenantId !== tenantId || i.status !== "pending") return false;
|
|
130
130
|
if (filter.documentType && i.documentType !== filter.documentType) return false;
|
|
131
131
|
if (filter.submittedBy && i.submittedBy !== filter.submittedBy) return false;
|
|
132
|
-
const
|
|
133
|
-
|
|
132
|
+
const hasOverdueEscalation = i.levels.some(
|
|
133
|
+
(l) => l.escalationDueAt != null && new Date(l.escalationDueAt) <= asOf
|
|
134
|
+
);
|
|
134
135
|
const isExpired = i.expiresAt != null && new Date(i.expiresAt) <= asOf;
|
|
135
136
|
const hasSLABreach = i.slaDeadlineAt != null && new Date(i.slaDeadlineAt) <= asOf && !i.slaBreachedAt;
|
|
136
137
|
const hasDelegationExpiry = i.levels.some(
|
|
137
138
|
(l) => l.status === "pending" && l.delegatedUntil != null && new Date(l.delegatedUntil) <= asOf && l.delegatedFrom != null
|
|
138
139
|
);
|
|
139
|
-
|
|
140
|
+
const hasDueReminder = i.levels.some(
|
|
141
|
+
(l) => l.status === "pending" && l.reminderDueAt != null && new Date(l.reminderDueAt) <= asOf
|
|
142
|
+
);
|
|
143
|
+
return hasOverdueEscalation || isExpired || hasSLABreach || hasDelegationExpiry || hasDueReminder;
|
|
140
144
|
}).map((i) => reviveDates(deepClone(i)));
|
|
141
145
|
}
|
|
142
146
|
async getInstancesByCursor(tenantId, filter, opts) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/errors.ts","../../src/adapters/MemoryAdapter.ts"],"names":[],"mappings":"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","file":"MemoryAdapter.cjs","sourcesContent":["export class ApprovalError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'ApprovalError';\n }\n\n toJSON(): { code: string; message: string; name: string } {\n return { code: this.code, message: this.message, name: this.name };\n }\n\n toHttpStatus(): number {\n const map: Record<string, number> = {\n NOT_FOUND: 404,\n CONFLICT: 409,\n FORBIDDEN: 403,\n VALIDATION: 422,\n TEMPLATE_NOT_FOUND: 404,\n };\n return map[this.code] ?? 500;\n }\n}\n\nexport class ApprovalNotFoundError extends ApprovalError {\n constructor(resource: string, id: string) {\n super(`${resource} \"${id}\" not found.`, 'NOT_FOUND');\n this.name = 'ApprovalNotFoundError';\n }\n}\n\nexport class ApprovalConflictError extends ApprovalError {\n constructor(instanceId: string) {\n super(\n `Concurrent modification detected on instance \"${instanceId}\". The record was updated by another process. Please retry.`,\n 'CONFLICT',\n );\n this.name = 'ApprovalConflictError';\n }\n}\n\nexport class ApprovalForbiddenError extends ApprovalError {\n constructor(message: string) {\n super(message, 'FORBIDDEN');\n this.name = 'ApprovalForbiddenError';\n }\n}\n\nexport class ApprovalValidationError extends ApprovalError {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message, 'VALIDATION');\n this.name = 'ApprovalValidationError';\n }\n}\n\nexport class ApprovalTemplateNotFoundError extends ApprovalError {\n constructor(name: string) {\n super(`Template \"${name}\" not found.`, 'TEMPLATE_NOT_FOUND');\n this.name = 'ApprovalTemplateNotFoundError';\n }\n}\n","import type {\n IStorageAdapter,\n PaginationOpts,\n PaginatedResult,\n InstanceFilter,\n CursorPaginationOpts,\n CursorPaginatedResult,\n} from './IStorageAdapter.js';\nimport type { ApprovalTemplate, ApprovalInstance, AuditEntry } from '../types/index.js';\nimport { ApprovalConflictError } from '../errors.js';\n\nfunction deepClone<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\n\nfunction reviveDates(instance: ApprovalInstance): ApprovalInstance {\n return {\n ...instance,\n createdAt: new Date(instance.createdAt),\n updatedAt: new Date(instance.updatedAt),\n expiresAt: instance.expiresAt ? new Date(instance.expiresAt) : undefined,\n slaDeadlineAt: instance.slaDeadlineAt ? new Date(instance.slaDeadlineAt) : undefined,\n slaBreachedAt: instance.slaBreachedAt ? new Date(instance.slaBreachedAt) : undefined,\n auditLog: instance.auditLog.map((e) => ({ ...e, timestamp: new Date(e.timestamp) })),\n levels: instance.levels.map((l) => {\n const level: typeof l = { ...l };\n if (l.escalationDueAt) level.escalationDueAt = new Date(l.escalationDueAt);\n if (l.delegatedUntil) level.delegatedUntil = new Date(l.delegatedUntil);\n return level;\n }),\n };\n}\n\nfunction reviveTemplateDates(template: ApprovalTemplate): ApprovalTemplate {\n return { ...template, createdAt: new Date(template.createdAt) };\n}\n\nfunction applyFilter(instance: ApprovalInstance, filter: InstanceFilter): boolean {\n if (filter.status && instance.status !== filter.status) return false;\n if (filter.documentType && instance.documentType !== filter.documentType) return false;\n if (filter.submittedBy && instance.submittedBy !== filter.submittedBy) return false;\n if (filter.templateName && instance.templateName !== filter.templateName) return false;\n // Stored instances carry string dates (deepClone JSON round-trip), so wrap before comparing.\n if (filter.fromDate && new Date(instance.createdAt) < filter.fromDate) return false;\n if (filter.toDate && new Date(instance.createdAt) > filter.toDate) return false;\n return true;\n}\n\nexport class MemoryAdapter implements IStorageAdapter {\n // keyed by `${tenantId}:${template.name}`\n private templates = new Map<string, ApprovalTemplate>();\n // keyed by `${tenantId}:${instance.id}`\n private instances = new Map<string, ApprovalInstance>();\n\n async saveTemplate(template: ApprovalTemplate): Promise<void> {\n this.templates.set(`${template.tenantId}:${template.name}`, deepClone(template));\n }\n\n async getTemplate(tenantId: string, name: string): Promise<ApprovalTemplate | null> {\n const template = this.templates.get(`${tenantId}:${name}`);\n return template ? reviveTemplateDates(deepClone(template)) : null;\n }\n\n async listTemplates(tenantId: string): Promise<ApprovalTemplate[]> {\n const result: ApprovalTemplate[] = [];\n for (const [key, template] of this.templates) {\n if (key.startsWith(`${tenantId}:`)) {\n result.push(reviveTemplateDates(deepClone(template)));\n }\n }\n return result;\n }\n\n async saveInstance(instance: ApprovalInstance): Promise<void> {\n this.instances.set(`${instance.tenantId}:${instance.id}`, deepClone(instance));\n }\n\n async updateInstance(instance: ApprovalInstance, expectedVersion: number): Promise<void> {\n const key = `${instance.tenantId}:${instance.id}`;\n const stored = this.instances.get(key);\n if (!stored) throw new ApprovalConflictError(instance.id);\n if (stored.version !== expectedVersion) throw new ApprovalConflictError(instance.id);\n const updated = deepClone(instance);\n updated.version = expectedVersion + 1;\n this.instances.set(key, updated);\n }\n\n async getInstance(tenantId: string, id: string): Promise<ApprovalInstance | null> {\n const raw = this.instances.get(`${tenantId}:${id}`);\n if (!raw) return null;\n return reviveDates(deepClone(raw));\n }\n\n async getInstancesByApprover(\n tenantId: string,\n approverId: string,\n opts?: PaginationOpts,\n ): Promise<PaginatedResult<ApprovalInstance>> {\n const all = [...this.instances.values()].filter((i) => {\n if (i.tenantId !== tenantId || i.status !== 'pending') return false;\n // Use .find() by level number, not array index (level numbers may not be consecutive)\n const currentLevel = i.levels.find((l) => l.level === i.currentLevel);\n return currentLevel?.approverIds.includes(approverId) ?? false;\n });\n return paginate(\n all.map((i) => reviveDates(deepClone(i))),\n opts,\n );\n }\n\n async getInstancesByFilter(\n tenantId: string,\n filter: InstanceFilter,\n opts?: PaginationOpts,\n ): Promise<PaginatedResult<ApprovalInstance>> {\n const all = [...this.instances.values()].filter(\n (i) => i.tenantId === tenantId && applyFilter(i, filter),\n );\n return paginate(\n all.map((i) => reviveDates(deepClone(i))),\n opts,\n );\n }\n\n async getOverdueInstances(\n tenantId: string,\n asOf: Date,\n filter: InstanceFilter = {},\n ): Promise<ApprovalInstance[]> {\n return [...this.instances.values()]\n .filter((i) => {\n if (i.tenantId !== tenantId || i.status !== 'pending') return false;\n if (filter.documentType && i.documentType !== filter.documentType) return false;\n if (filter.submittedBy && i.submittedBy !== filter.submittedBy) return false;\n // Escalation overdue\n const currentLevel = i.levels.find((l) => l.level === i.currentLevel);\n const hasOverdueEscalation =\n currentLevel?.escalationDueAt != null && new Date(currentLevel.escalationDueAt) <= asOf;\n // Instance deadline expired\n const isExpired = i.expiresAt != null && new Date(i.expiresAt) <= asOf;\n // SLA breach (not yet recorded)\n const hasSLABreach =\n i.slaDeadlineAt != null && new Date(i.slaDeadlineAt) <= asOf && !i.slaBreachedAt;\n // Delegation expiry on any pending level\n const hasDelegationExpiry = i.levels.some(\n (l) =>\n l.status === 'pending' &&\n l.delegatedUntil != null &&\n new Date(l.delegatedUntil) <= asOf &&\n l.delegatedFrom != null,\n );\n return hasOverdueEscalation || isExpired || hasSLABreach || hasDelegationExpiry;\n })\n .map((i) => reviveDates(deepClone(i)));\n }\n\n async getInstancesByCursor(\n tenantId: string,\n filter: InstanceFilter,\n opts: CursorPaginationOpts,\n ): Promise<CursorPaginatedResult<ApprovalInstance>> {\n const all = [...this.instances.values()]\n .filter((i) => i.tenantId === tenantId && applyFilter(i, filter))\n .map((i) => reviveDates(deepClone(i)))\n .sort((a, b) => {\n const ta = a.updatedAt.getTime();\n const tb = b.updatedAt.getTime();\n return ta !== tb ? ta - tb : a.id.localeCompare(b.id);\n });\n\n const { cursor, limit, direction = 'forward' } = opts;\n let startIdx = 0;\n\n if (cursor) {\n const [ts, id] = decodeCursor(cursor);\n const idx = all.findIndex(\n (i) => i.updatedAt.getTime() > ts || (i.updatedAt.getTime() === ts && i.id > id),\n );\n startIdx = idx === -1 ? all.length : idx;\n }\n\n if (direction === 'backward' && startIdx > 0) {\n startIdx = Math.max(0, startIdx - limit - 1);\n }\n\n const slice = all.slice(startIdx, startIdx + limit);\n const hasMore = startIdx + limit < all.length;\n const nextCursor = hasMore ? encodeCursor(slice[slice.length - 1]!) : undefined;\n const prevCursor = startIdx > 0 ? encodeCursor(all[startIdx - 1]!) : undefined;\n\n return { items: slice, nextCursor, prevCursor, hasMore };\n }\n\n async getIdempotentInstance(\n tenantId: string,\n idempotencyKey: string,\n ): Promise<ApprovalInstance | null> {\n for (const instance of this.instances.values()) {\n if (instance.tenantId === tenantId && instance.idempotencyKey === idempotencyKey) {\n return reviveDates(deepClone(instance));\n }\n }\n return null;\n }\n\n async appendAuditEntry(\n _tenantId: string,\n _instanceId: string,\n _entry: AuditEntry,\n ): Promise<void> {\n // No-op by design. The engine already pushes each entry onto instance.auditLog\n // and persists the whole instance via saveInstance()/updateInstance(), so the\n // embedded log is the source of truth for this adapter. Pushing here too would\n // duplicate every entry (as a dedicated, append-only audit table would in\n // PostgresAdapter, this method exists to satisfy that separate-sink contract).\n }\n\n /** Test helper — total stored instances across all tenants. */\n get size(): number {\n return this.instances.size;\n }\n}\n\nfunction paginate<T>(items: T[], opts?: PaginationOpts): PaginatedResult<T> {\n const total = items.length;\n if (!opts) return { items, total };\n return { items: items.slice(opts.offset, opts.offset + opts.limit), total };\n}\n\nfunction encodeCursor(instance: ApprovalInstance): string {\n return Buffer.from(`${instance.updatedAt.getTime()}|${instance.id}`).toString('base64');\n}\n\nfunction decodeCursor(cursor: string): [number, string] {\n const decoded = Buffer.from(cursor, 'base64').toString('utf8');\n const pipeIdx = decoded.indexOf('|');\n return [Number(decoded.slice(0, pipeIdx)), decoded.slice(pipeIdx + 1)];\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/errors.ts","../../src/adapters/MemoryAdapter.ts"],"names":[],"mappings":"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","file":"MemoryAdapter.cjs","sourcesContent":["export class ApprovalError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n ) {\n super(message);\n this.name = 'ApprovalError';\n }\n\n toJSON(): { code: string; message: string; name: string } {\n return { code: this.code, message: this.message, name: this.name };\n }\n\n toHttpStatus(): number {\n const map: Record<string, number> = {\n NOT_FOUND: 404,\n CONFLICT: 409,\n FORBIDDEN: 403,\n VALIDATION: 422,\n TEMPLATE_NOT_FOUND: 404,\n };\n return map[this.code] ?? 500;\n }\n}\n\nexport class ApprovalNotFoundError extends ApprovalError {\n constructor(resource: string, id: string) {\n super(`${resource} \"${id}\" not found.`, 'NOT_FOUND');\n this.name = 'ApprovalNotFoundError';\n }\n}\n\nexport class ApprovalConflictError extends ApprovalError {\n constructor(instanceId: string) {\n super(\n `Concurrent modification detected on instance \"${instanceId}\". The record was updated by another process. Please retry.`,\n 'CONFLICT',\n );\n this.name = 'ApprovalConflictError';\n }\n}\n\nexport class ApprovalForbiddenError extends ApprovalError {\n constructor(message: string) {\n super(message, 'FORBIDDEN');\n this.name = 'ApprovalForbiddenError';\n }\n}\n\nexport class ApprovalValidationError extends ApprovalError {\n constructor(\n message: string,\n public readonly cause?: unknown,\n ) {\n super(message, 'VALIDATION');\n this.name = 'ApprovalValidationError';\n }\n}\n\nexport class ApprovalTemplateNotFoundError extends ApprovalError {\n constructor(name: string) {\n super(`Template \"${name}\" not found.`, 'TEMPLATE_NOT_FOUND');\n this.name = 'ApprovalTemplateNotFoundError';\n }\n}\n","import type {\n IStorageAdapter,\n PaginationOpts,\n PaginatedResult,\n InstanceFilter,\n CursorPaginationOpts,\n CursorPaginatedResult,\n} from './IStorageAdapter.js';\nimport type { ApprovalTemplate, ApprovalInstance, AuditEntry } from '../types/index.js';\nimport { ApprovalConflictError } from '../errors.js';\n\nfunction deepClone<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\n\nfunction reviveDates(instance: ApprovalInstance): ApprovalInstance {\n return {\n ...instance,\n createdAt: new Date(instance.createdAt),\n updatedAt: new Date(instance.updatedAt),\n expiresAt: instance.expiresAt ? new Date(instance.expiresAt) : undefined,\n slaDeadlineAt: instance.slaDeadlineAt ? new Date(instance.slaDeadlineAt) : undefined,\n slaBreachedAt: instance.slaBreachedAt ? new Date(instance.slaBreachedAt) : undefined,\n auditLog: instance.auditLog.map((e) => ({ ...e, timestamp: new Date(e.timestamp) })),\n levels: instance.levels.map((l) => {\n const level: typeof l = { ...l };\n if (l.escalationDueAt) level.escalationDueAt = new Date(l.escalationDueAt);\n if (l.delegatedUntil) level.delegatedUntil = new Date(l.delegatedUntil);\n return level;\n }),\n };\n}\n\nfunction reviveTemplateDates(template: ApprovalTemplate): ApprovalTemplate {\n return { ...template, createdAt: new Date(template.createdAt) };\n}\n\nfunction applyFilter(instance: ApprovalInstance, filter: InstanceFilter): boolean {\n if (filter.status && instance.status !== filter.status) return false;\n if (filter.documentType && instance.documentType !== filter.documentType) return false;\n if (filter.submittedBy && instance.submittedBy !== filter.submittedBy) return false;\n if (filter.templateName && instance.templateName !== filter.templateName) return false;\n // Stored instances carry string dates (deepClone JSON round-trip), so wrap before comparing.\n if (filter.fromDate && new Date(instance.createdAt) < filter.fromDate) return false;\n if (filter.toDate && new Date(instance.createdAt) > filter.toDate) return false;\n return true;\n}\n\nexport class MemoryAdapter implements IStorageAdapter {\n // keyed by `${tenantId}:${template.name}`\n private templates = new Map<string, ApprovalTemplate>();\n // keyed by `${tenantId}:${instance.id}`\n private instances = new Map<string, ApprovalInstance>();\n\n async saveTemplate(template: ApprovalTemplate): Promise<void> {\n this.templates.set(`${template.tenantId}:${template.name}`, deepClone(template));\n }\n\n async getTemplate(tenantId: string, name: string): Promise<ApprovalTemplate | null> {\n const template = this.templates.get(`${tenantId}:${name}`);\n return template ? reviveTemplateDates(deepClone(template)) : null;\n }\n\n async listTemplates(tenantId: string): Promise<ApprovalTemplate[]> {\n const result: ApprovalTemplate[] = [];\n for (const [key, template] of this.templates) {\n if (key.startsWith(`${tenantId}:`)) {\n result.push(reviveTemplateDates(deepClone(template)));\n }\n }\n return result;\n }\n\n async saveInstance(instance: ApprovalInstance): Promise<void> {\n this.instances.set(`${instance.tenantId}:${instance.id}`, deepClone(instance));\n }\n\n async updateInstance(instance: ApprovalInstance, expectedVersion: number): Promise<void> {\n const key = `${instance.tenantId}:${instance.id}`;\n const stored = this.instances.get(key);\n if (!stored) throw new ApprovalConflictError(instance.id);\n if (stored.version !== expectedVersion) throw new ApprovalConflictError(instance.id);\n const updated = deepClone(instance);\n updated.version = expectedVersion + 1;\n this.instances.set(key, updated);\n }\n\n async getInstance(tenantId: string, id: string): Promise<ApprovalInstance | null> {\n const raw = this.instances.get(`${tenantId}:${id}`);\n if (!raw) return null;\n return reviveDates(deepClone(raw));\n }\n\n async getInstancesByApprover(\n tenantId: string,\n approverId: string,\n opts?: PaginationOpts,\n ): Promise<PaginatedResult<ApprovalInstance>> {\n const all = [...this.instances.values()].filter((i) => {\n if (i.tenantId !== tenantId || i.status !== 'pending') return false;\n // Use .find() by level number, not array index (level numbers may not be consecutive)\n const currentLevel = i.levels.find((l) => l.level === i.currentLevel);\n return currentLevel?.approverIds.includes(approverId) ?? false;\n });\n return paginate(\n all.map((i) => reviveDates(deepClone(i))),\n opts,\n );\n }\n\n async getInstancesByFilter(\n tenantId: string,\n filter: InstanceFilter,\n opts?: PaginationOpts,\n ): Promise<PaginatedResult<ApprovalInstance>> {\n const all = [...this.instances.values()].filter(\n (i) => i.tenantId === tenantId && applyFilter(i, filter),\n );\n return paginate(\n all.map((i) => reviveDates(deepClone(i))),\n opts,\n );\n }\n\n async getOverdueInstances(\n tenantId: string,\n asOf: Date,\n filter: InstanceFilter = {},\n ): Promise<ApprovalInstance[]> {\n return [...this.instances.values()]\n .filter((i) => {\n if (i.tenantId !== tenantId || i.status !== 'pending') return false;\n if (filter.documentType && i.documentType !== filter.documentType) return false;\n if (filter.submittedBy && i.submittedBy !== filter.submittedBy) return false;\n // Escalation overdue on ANY open branch. Filtering to i.currentLevel\n // would miss the upper branches of a parallel group entirely, and left\n // this adapter disagreeing with PostgresAdapter, which already scans\n // every level.\n const hasOverdueEscalation = i.levels.some(\n (l) => l.escalationDueAt != null && new Date(l.escalationDueAt) <= asOf,\n );\n // Instance deadline expired\n const isExpired = i.expiresAt != null && new Date(i.expiresAt) <= asOf;\n // SLA breach (not yet recorded)\n const hasSLABreach =\n i.slaDeadlineAt != null && new Date(i.slaDeadlineAt) <= asOf && !i.slaBreachedAt;\n // Delegation expiry on any pending level\n const hasDelegationExpiry = i.levels.some(\n (l) =>\n l.status === 'pending' &&\n l.delegatedUntil != null &&\n new Date(l.delegatedUntil) <= asOf &&\n l.delegatedFrom != null,\n );\n // Reminder due on any open branch. Without this the scheduler never\n // sees the instance and no reminder is ever sent.\n const hasDueReminder = i.levels.some(\n (l) =>\n l.status === 'pending' && l.reminderDueAt != null && new Date(l.reminderDueAt) <= asOf,\n );\n return (\n hasOverdueEscalation || isExpired || hasSLABreach || hasDelegationExpiry || hasDueReminder\n );\n })\n .map((i) => reviveDates(deepClone(i)));\n }\n\n async getInstancesByCursor(\n tenantId: string,\n filter: InstanceFilter,\n opts: CursorPaginationOpts,\n ): Promise<CursorPaginatedResult<ApprovalInstance>> {\n const all = [...this.instances.values()]\n .filter((i) => i.tenantId === tenantId && applyFilter(i, filter))\n .map((i) => reviveDates(deepClone(i)))\n .sort((a, b) => {\n const ta = a.updatedAt.getTime();\n const tb = b.updatedAt.getTime();\n return ta !== tb ? ta - tb : a.id.localeCompare(b.id);\n });\n\n const { cursor, limit, direction = 'forward' } = opts;\n let startIdx = 0;\n\n if (cursor) {\n const [ts, id] = decodeCursor(cursor);\n const idx = all.findIndex(\n (i) => i.updatedAt.getTime() > ts || (i.updatedAt.getTime() === ts && i.id > id),\n );\n startIdx = idx === -1 ? all.length : idx;\n }\n\n if (direction === 'backward' && startIdx > 0) {\n startIdx = Math.max(0, startIdx - limit - 1);\n }\n\n const slice = all.slice(startIdx, startIdx + limit);\n const hasMore = startIdx + limit < all.length;\n const nextCursor = hasMore ? encodeCursor(slice[slice.length - 1]!) : undefined;\n const prevCursor = startIdx > 0 ? encodeCursor(all[startIdx - 1]!) : undefined;\n\n return { items: slice, nextCursor, prevCursor, hasMore };\n }\n\n async getIdempotentInstance(\n tenantId: string,\n idempotencyKey: string,\n ): Promise<ApprovalInstance | null> {\n for (const instance of this.instances.values()) {\n if (instance.tenantId === tenantId && instance.idempotencyKey === idempotencyKey) {\n return reviveDates(deepClone(instance));\n }\n }\n return null;\n }\n\n async appendAuditEntry(\n _tenantId: string,\n _instanceId: string,\n _entry: AuditEntry,\n ): Promise<void> {\n // No-op by design. The engine already pushes each entry onto instance.auditLog\n // and persists the whole instance via saveInstance()/updateInstance(), so the\n // embedded log is the source of truth for this adapter. Pushing here too would\n // duplicate every entry (as a dedicated, append-only audit table would in\n // PostgresAdapter, this method exists to satisfy that separate-sink contract).\n }\n\n /** Test helper — total stored instances across all tenants. */\n get size(): number {\n return this.instances.size;\n }\n}\n\nfunction paginate<T>(items: T[], opts?: PaginationOpts): PaginatedResult<T> {\n const total = items.length;\n if (!opts) return { items, total };\n return { items: items.slice(opts.offset, opts.offset + opts.limit), total };\n}\n\nfunction encodeCursor(instance: ApprovalInstance): string {\n return Buffer.from(`${instance.updatedAt.getTime()}|${instance.id}`).toString('base64');\n}\n\nfunction decodeCursor(cursor: string): [number, string] {\n const decoded = Buffer.from(cursor, 'base64').toString('utf8');\n const pipeIdx = decoded.indexOf('|');\n return [Number(decoded.slice(0, pipeIdx)), decoded.slice(pipeIdx + 1)];\n}\n"]}
|
|
@@ -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-BZarNxs9.cjs';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DuEgSIIz.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-DrY-rhhF.js';
|
|
2
|
+
import { A as ApprovalTemplate, a as ApprovalInstance, b as AuditEntry } from '../instance-DuEgSIIz.js';
|
|
3
3
|
|
|
4
4
|
declare class MemoryAdapter implements IStorageAdapter {
|
|
5
5
|
private templates;
|